
Хабрапитонерам привет!
Время от времени я сталкиваюсь с паттернами разработки, которые существуют не потому что они хорошо решают какую-то проблему, а потому что так сделано в популярном фреймворке X, следовательно, думают многие — это хорошо.
Сейчас я хочу понегодовать на паттерн «все настройки — в settings.py». Понятно, что популярность он набрал благодаря Django. Я то и дело встречаю в проектах, никак не завязанных на этот фреймворк ту же самую историю: большая кодовая база, маленькие, хорошенькие никак не связанные друг с другом компоненты, и нате вам: все дружно из произвольных мест лезут в волшебный недомодуль settings за своими константами.
Итак, почему же такой подход на мой взгляд отвратителен.
Проблемы с каскадными настройками
В проектах из реальной жизни, как правило, нужно минимум три набора настроек: чтобы погонять проект на localhost, чтобы запускать unittest'ы, и чтобы крутить всё на боевых серверах. При этом большая часть настроек обычно совпадает во всех случаях, а некоторые различаются.
Например, у вас используется MongoDB в качестве хранилища. В общем случае, коннектиться к ней нужно на localhost и использовать DB с именем
my_project
. Однако для запуска unittest'ов нужно брать DB с другим именем, чтобы не задеть боевые данные: скажем, unittests
. А в случае продакшена коннектиться нужно не на localhost, а на вполне определённый IP, на сервер, отданный под монгу.Так как же, в зависимости от внешних условий
settings.MONGODB_ADDRESS
из settings.py должна принимать различные значения? Обычно в ход идёт voodoo-конструкция в конце, состоящая из __import__
, __dict__
, vars()
, try/except ImportError
, которая пытается дополнить и перекрыть пространство имён всеми потрохами другого модуля вроде settings_local.py. То, что дополнительно нужно подгружать именно
_local.py
задаётся или хардкодом или через переменную окружения. В любом случае, чтобы те же например unittest'ы включили свои настройки только на время запуска приходится плясать с бубном и нарушать Zen of Python: Explicit is better than implicit.Кроме того, такое решение сопряжено с другой проблемой, описанной далее.
Исполняемый код
Хранить настройки в виде исполняемого py-кода — жутко. На самом деле весь паттерн, видимо, изначально появился как якобы простое и элегантное решение: «А зачем нам какие-то cfg-парсеры, если можно сделать всё прям на питоне? И возможностей ведь больше!». В сценариях чуть сложнее тривиальных решение оборачивается боком. Рассмотрим, например, такой сниппет:
# settings.py
BASE_PATH = os.path.dirname(__file__)
PROJECT_HOSTNAME = 'localhost'
SOME_JOB_COMMAND = '%s/bin/do_job.py -H %s' % (BASE_PATH, PROJECT_HOSTNAME)
# settings_production.py
PROJECT_HOSTNAME = 'my-project.ru'
Понимаете в чём проблема? То, что мы перекрыли значение
PROJECT_HOSTNAME
абсолютно по барабану для итогового значения SOME_JOB_COMMAND
. Мы могли бы скрипя зубами скопипастить определение SOME_JOB_COMMAND
после перекрытия, но даже это не возможно: BASE_PATH
то, в другом модуле. Копипастить и его? Не слишком ли?Я уже не говорю о том, что исполняемый код в качестве конфигурации может просто приводить к трудноотлаживаемым
ImportError
при старте приложения в новой среде.Поэтому я уверен, что мухи должны быть отдельно, котлеты отдельно: базовые значения в текстовом файле, вычисляемые — в py-модуле.
High-coupling
Хороший проект — тот проект, который возможно разобрать на маленькие кубики, а каждый кубик выложить на github в качестве полноценного open-source проекта.
Когда всё так и есть, но с одним НО: «будьте добры иметь settings.py в корне проекта и чтобы в нём были настройки
FOO_BAR
, FOO_BAZ
и FOO_QUX
» выглядит это как-то нелепо, не правда ли? А когда что-то звучит нелепо, обычно это означает, что есть ситуации в которых эта нелепость аукается.В нашем случае, пример не заставляет долго себя выдумывать. Пусть наше приложение работает с VKontakte API, и у нас есть нечто вроде
VKontakteProfileCache
, которое в лоб пользуется settings.VK_API_KEY
и settings.VK_API_SECRET
. Ну пользуется и пользуется, а потом раз, и наш проект должен начать работать сразу с несколькими VKontakte-приложениями. А всё, VKontakteProfileCache
спроектирован так, что он работает только с одной парой credentials.Поэтому стройнее и целесообразнее вообще никогда не обращаться к модулю настроек напрямую. Пусть все потребители принимают нужные настройки через параметры конструктора, через dependency injection framework, как угодно, но не напрямую. А конкретные настройки пусть вытягивает самый-самый нижний уровень вроде кода в
if __name__ == '__main__'
. А откуда уж он их возьмёт — его личные проблемы. При таком подходе также крайне упрощается unit-тестирование: с какими настройками нужно прогнать, с теми и создаём.Возможное решение
Итак, паттерн «settings.py» грязью я полил. Мне полегчало, спасибо. Теперь о возможном решении. Я использовал подобный подход в нескольких проектах и нахожу его удобным и лишённым перечисленных проблем.
Настройки храним в текстовых ini-style файлах. Для парсинга используем ConfigObj: он имеет более богатые возможности по сравнению со стандартным ConfigParser, в частности с ним очень просто делать каскады.
В проекте заводим базовый файл настроек
default_settings.cfg
со всеми возможными настройками и их значениями с разумным умолчанием.Создаём модуль utils.config с функциями вроде
configure_from_files()
, configure_for_unittests()
, которые возвращают объект с настройками под разные ситуации. configure_from_files()
организовывает каскадный поиск по файлам: default_settings.cfg
, ~/.my-project.cfg
, /etc/my-project.cfg
и, вероятно, где-то ещё. Всё зависит от проекта.Вычисляемые настройки эвалюируются последним шагом сборки объекта-конфигурации.
Самим модулем пользуются лишь запускалки процессов или тестов. Все заинтересованные в настройках классы получают уже готовые значения через инъекции, то есть не общаются с настройками напрямую. На самом деле это не всегда удобно, когда настроек тьма всё же лучше передавать объект-конфигурацию целиком, но это не отменяет того, что её нужно передавать — никакого обращения в лоб.
Быть может, я слишком много понаписал о таком «пустяке» как настройки. Но если хоть кто-нибудь после прочтения задумается перед слепым копированием далеко не совершенного подхода к чему-либо и сделает лучше, проще, веселее, буду считать миссию этого поста выполненной.