Искаженный отложенный шаблон для защиты неинициализированной базы данных

#python #twisted #deferred

#python #скрученный #отложенный

Вопрос:

Как я должен защитить базу данных от доступа до ее инициализации?

У меня есть база данных. Его необходимо инициализировать. Это может занять время, и поэтому инициализация возвращает отложенный. Давайте назовем это отложенным d_db_ready .

У меня есть другие методы, скажем read_a_value_from_the_database() , которые хотели бы получить доступ к базе данных, но только после ее инициализации.

Легко! Я добавляю обратный вызов в d_db_ready :

 d_value = d_db_ready.addCallback(read_a_value_from_the_database)
  

Теперь значение, которое я хотел получить из базы данных, можно найти в отложенном d_value .

Давайте попробуем сделать это снова:

 d_value2 = d_db_ready.addCallback(read_a_value_from_the_database)
  

На этот раз это не сработает. Это потому, что цепочка обратного вызова для d_db_ready теперь заканчивается значением, которое мы считываем из базы данных. Он больше не предоставляет доступ к базе данных.

Какой шаблон следует использовать здесь вместо этого? Как я должен защитить базу данных от доступа до ее инициализации?

Одним из вариантов было бы read_a_value_from_the_database вернуть базу данных… но тогда он не возвращал бы значение. Кроме того, такой подход является определенным источником ужасной потенциальной ошибки: когда я забываю вернуть базу данных, мне пришлось бы выяснять, какая функция была вызвана ранее, когда все, что у меня было бы, — это функция, которая вызывается в данный момент.

Ниже приведен некоторый исполняемый код, демонстрирующий мой вопрос:

 from twisted.internet.defer import Deferred

database = "uninitialized"

d_db_ready = Deferred()

def init_database():
    global database
    print("Initializing database")
    database = { "value": 1 }
    d_db_ready.callback(database)

def read_a_value_from_the_database(db):
    value = db["value"]
    print("value:", value)
    return value

d_db_ready.addCallback(read_a_value_from_the_database)

d_db_ready.addCallback(read_a_value_from_the_database) # <--- errors

init_database()

  

Ответ №1:

Шаблон, который вы показали, уже хорошо справляется с предотвращением использования базы данных до тех пор, пока база данных не будет готова. Проблема в том, что он не поддерживает многократное использование. Второй обратный вызов не завершится ошибкой, потому что он получил неинициализированную базу данных. Сбой происходит только после инициализации базы данных, поскольку вместо базы данных он получает результат предыдущей операции.

Существует два распространенных варианта. Одним из них является прекращение многократного использования. Это не так абсурдно, как может показаться. Например:

 def main():
    ...
    d_db_ready.addCallback(run_the_program)
    ...

def run_the_program(database):
    d_a = read_a_value_from_the_database(database)
    d_b = read_a_value_from_the_database(database)
    ...
  

Теперь единственным обратным вызовом d_db_ready является run_the_program и run_the_program , который заботится о передаче базы данных туда, куда ей нужно.

Второй вариант — сделать ваш readiness API поддерживающим многократное использование. Здесь вы можете использовать множество различных подходов к реализации, но все они сосредоточены на нарушении функции цепочки результатов Deferred. Например, вы могли бы сделать это:

 def wait_for_db(callback):
    def safe_callback(database):
        try:
            callback(database)
        except:
            # logging or support an errback or something
        return database
    d_db_ready.addCallback(safe_callback)
  

safe_callback гарантирует, что результатом d_db_ready всегда будет база данных, а не результат какого-то случайного обратного вызова.

Возможны гораздо лучшие реализации этой идеи (например, та, которая все еще основана на Deferred вместо того, чтобы отказываться от своих возможностей для передачи простых обратных вызовов). Надеюсь, этот простой шаблон дает вам общее представление.

Комментарии:

1. Большое спасибо за ваш подробный ответ @Jean-PaulCalderone.

2. Два шаблона, которые вы описали, являются отличными ссылками. Знаете ли вы, есть ли где-нибудь коллекция рекомендуемых отложенных шаблонов? Что-то вроде «Шаблонов проектирования» en.wikipedia.org/wiki/Design_Patterns

3. Я добавил отложенную реализацию вашего второго варианта в свой собственный ответ ниже. Я не решался добавить это в ваш ответ, поскольку я не был уверен, что это сделано в stackoverflow. Я был бы очень признателен за ваш отзыв, если у вас есть время, и я рад опубликовать свой ответ в вики сообщества, если, по вашему мнению, это хорошая идея.

4. К сожалению, я не знаю хорошего списка отложенных шаблонов. Я часто мечтал о такой вещи, поскольку я часто вижу, как либо одни и те же шаблоны изобретаются заново в разных местах, либо принимаются одни и те же антишаблоны.

Ответ №2:

Основываясь на втором варианте в ответе @Jean-PaulCalderone, вот реализация, основанная на отсрочках:

 def get_database():
    d = Deferred()
    def when_ready(db):
        d.callback(db)
        return db
    d_db_ready.addCallback(when_ready)
    
    return d

get_database().addCallback(read_a_value_from_the_database)
get_database().addCallback(read_a_value_from_the_database) # <--- works correctly

init_database()
  

Обратите внимание, что d_db_ready теперь это должно считаться закрытым для функции get_database() , и пользователь больше не должен к нему обращаться.

Комментарии:

1.Неплохо. Два предложения. (1) Посмотрите на Deferred.chainDeferred и посмотрите, сможете ли вы выяснить, как добавить в get_database хорошее поведение при обработке ошибок. (2) Подумайте, было бы выгодно скрыть init_database и d_db_ready внутри get_database — или, возможно, внутри состояния экземпляра класса.