Как стать автором
Обновить

Антипаттерн settings.py

Python


Хабрапитонерам привет!

Время от времени я сталкиваюсь с паттернами разработки, которые существуют не потому что они хорошо решают какую-то проблему, а потому что так сделано в популярном фреймворке 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 и, вероятно, где-то ещё. Всё зависит от проекта.

Вычисляемые настройки эвалюируются последним шагом сборки объекта-конфигурации.

Самим модулем пользуются лишь запускалки процессов или тестов. Все заинтересованные в настройках классы получают уже готовые значения через инъекции, то есть не общаются с настройками напрямую. На самом деле это не всегда удобно, когда настроек тьма всё же лучше передавать объект-конфигурацию целиком, но это не отменяет того, что её нужно передавать — никакого обращения в лоб.

Быть может, я слишком много понаписал о таком «пустяке» как настройки. Но если хоть кто-нибудь после прочтения задумается перед слепым копированием далеко не совершенного подхода к чему-либо и сделает лучше, проще, веселее, буду считать миссию этого поста выполненной.
Теги:архитектураdjangoпаттерны проектированияdependency injectionконфигурация
Хабы: Python
Всего голосов 102: ↑86 и ↓16 +70
Просмотры12.8K

Похожие публикации

Backend Python/Django
от 2 500 до 4 500 $Borderless360Можно удаленно
Python Backend Developer (Team Lead)
от 250 000 ₽OutstreamСанкт-ПетербургМожно удаленно
Lead Python Developer (django)
от 170 000 ₽UpmarketМожно удаленно
Developer (Python/Django)
от 70 000 ₽SkyDNSМожно удаленно
Разработчик Python/Django
от 80 000 до 120 000 ₽PrintumМожно удаленно

Лучшие публикации за сутки