IoC/DI решает проблему «конфигурирования» объекта класса, за счёт того, что инициализацию всех настроек (т.е. «зависимостей» — классов, от которых зависит данный класс, без которых он не может работать, и которые, в тоже время, частично определяют его поведение), выносит за него, перекладывая эту обязанность на фреймворк.
До рассмотрения зависимостей класса, он может выглядеть следующим образом.
Листинг программы без DI/IoC
Невозможно протестировать изолированно A; невозможно подменить зависимости для тестирования или в случае смены конфигурации приложения:
class A(object):
def __init__(self):
self.c = C()
self.d = D()
Листинг программы с DI/IoC
Теперь класс получает зависимости из вне. (Кто их проставляет? IoC!)
class A(object):
def __init__(self, c=None, d=None):
self.c = c
self.d = d
В строготипизированных языках, без фреймворка обеспечивающего IoC, аналогичный код требует помимо введения интерфейсов IC и ID, реализацию класса IoCContainer который в какой-то момент жизненного цикла приложения инстанцирует конкретные реализации C и D и при инстанцировании класса A передаст их в качестве параметров.
Без введения интерфейсов IC и ID, невозможно будет скомпилировать класс A (типы невозможно «привести» в момент компиляции).
Однако, получается, что класс IoCContainer сам становится зависимым от конкретных реализаций IC и ID, да еще и сам должен «знать» как и когда инстанцировать конкретные реализации IC, ID (или подставить статические классы/синглтоны).
Необходимость в инициализации контейнера создаёт усложнение жизненного цикла приложения. Появляется дополнительный шаг в виде инициализации контейнера, появляется и оверхед при инстанцировании класса: в случае, когда создаётся новый инстанс класса, нам необходимо и инстанцировать зависимости или передать ссылки на синглтоны.
По сути, IoC выступает в роли конфигуратора. Создать класс IoCContainer средствами одних только конструкций языка оказывается невозможным в следствие зависимостей IoCContainer'а. Поэтому-то «конфигурация» связей у объектов в приложении выносится в отдельный декларативный модуль (.xml с описанием зависимостей), либо в метаданные/аннотации/докстринги в коде — всё то, что «не заметит» компилятор и не создаст проблем при приведении типов. Необходимо, чтобы компилятор получил такой код, в котором типы разрешимы.
Один из примеров решения задачи, аналогично которой решает DI/IoC — это создание кроссплатформенных приложений, когда под каждую отдельную платформу нам необходимы различные реализации системных модулей. Пример: игровое приложение использует OpenGL в Linux/BSD, но использует DirectX под Windows.
В С/С++ реализация паттерна мало похожа на реализацию, например, в Java. Но по аналогии используются метаданные, как надстройка над языком. В С/С++ используются макросы условной компиляции #ifdef/#ifndef. В ходе обработки кода препроцессором, до компиляции, благодаря этим макросам, формируется файл исходного кода, который содержит только код для целевой платформы, который уже и компилируется в последствии. (todo: возможно найти/показать стандарт; в исходниках Линукс или того же nginx есть бездна примеров подобных ситуаций).
В динамически типизируемых языках, у нас нет возможности (а равно и надобности) указывать конкретный тип в сигнатуре класса, и проблемы которые решает IoC отсутствуют — мы можем передать во время рантайма любой объект в качестве c или d, в примерах сверху. Само собой, если мы нарушим «требования» класса A к этим зависимостям, мы нарушим логику работы приложения.
Аналогичное, справедливо и для IoC в некоторых строготипизируемых языках (например Java) — единственным контрактом там служит наличие реализаций методов, сигнатуры которых указаны в интерфейсах, необязательно эти реализации удовлетворят потребностям класса A.
В прочем, не исключение и Rust. В нём, данный контракт усилен только тем, что в сигнатуре методов возможно явно указать, что метод может создавать panic (т.е. порождать RuntimeException в терминах Java), и Option, который лишь гарантирует что код пользователя (в данном случае класса A) учитывает возможность отсутствия ожидаемого объекта.
Скрипты в tools ожидают что пользователь (оператор, мы) передал корректные параметры в environment variables для соединений с Redis, Clickhouse, взаимодействия с Majorka. В случае передачи недостаточных или некорректных параметров, само выполнение этих скриптов не имеет никакого смысла. Однако, различным скриптам необходимы разные комбинации этих параметров.
Если оставлять обязанность обработки параметров environment variables на коде скриптов, мы неизбежно получаем копипасту, которая будет переносится из скрипта в скрипт. Практически в каждом скрипте необходим доступ к Clickhouse, позже появится необходимость в интеграции с REST API рекламных площадок, могут потребоваться и другие сервисы (например, majorka-cli, которой необходим доступ к Redis).
Предложение заключается в том, чтобы повторно используемый код вынести в библиотечный класс Environment, который выступает фасадом к os.environ, и является контейнером для требуемых сервисов. Кроме этого, Environment унифицирует названия env параметров. Сейчас в некоторых местах используется CH_URL, CLICKHOUSE_URL, MAJORKA_CLICKHOUSE для обозначения одного и того же — URL Clickhouse.
Пример использования в коде скрипта:
from proc.env import Environment
env = Environment()
db = env.get_reporting_db()
print list(db.read(sql='SELECT * FROM hits'))
Если в environment variables не переданы параметры для Clickhouse (CH_URL), данный скрипт «упадет» с человекопонятной ошибкой:
Set the 'CH_URL' environmental variable to the URL of Clickhouse instance/slave. Example: http://127.0.0.1:8123/
В противном случае, скрипт выполнит свою работу. В коде скрипта не нужно обрабатывать ошибочные состояния и показывать сообщение пользователю, которых очень много:
Не передан URL
URL передан, однако по этому URL сервер отвечает 61: Connection refused
URL передан, есть соединение по URL, однако ping-запрос (в коде data.framework это SELECT 1;) возвращает неожиданный ответ
Аналогичные ситуации могут возникать и для Redis, majorka-cli и иных сторонних сервисов, настройка которых определяется системной конфигурацией.
Если бы Environment был, базовый тест проверяющий конфигурацию установки приложения на сервере выглядел бы так:
from proc.env import Environment
env = Environment()
bus = env.get_bus()
majorka_exist = env.run_majorka_cli(None) # read help of majorka-cli from stdout
Если скрипт завершился с returncode=0, значит все параметры окружения настроены корректно.
IoC/DI решает проблему «конфигурирования» объекта класса, за счёт того, что инициализацию всех настроек (т.е. «зависимостей» — классов, от которых зависит данный класс, без которых он не может работать, и которые, в тоже время, частично определяют его поведение), выносит за него, перекладывая эту обязанность на фреймворк.
До рассмотрения зависимостей класса, он может выглядеть следующим образом.
Листинг программы без DI/IoC
Невозможно протестировать изолированно A; невозможно подменить зависимости для тестирования или в случае смены конфигурации приложения:
Листинг программы с DI/IoC
Теперь класс получает зависимости из вне. (Кто их проставляет? IoC!)
В строготипизированных языках, без фреймворка обеспечивающего IoC, аналогичный код требует помимо введения интерфейсов
IC
иID
, реализацию классаIoCContainer
который в какой-то момент жизненного цикла приложения инстанцирует конкретные реализации C и D и при инстанцировании класса A передаст их в качестве параметров. Без введения интерфейсов IC и ID, невозможно будет скомпилировать класс A (типы невозможно «привести» в момент компиляции).Однако, получается, что класс IoCContainer сам становится зависимым от конкретных реализаций IC и ID, да еще и сам должен «знать» как и когда инстанцировать конкретные реализации IC, ID (или подставить статические классы/синглтоны).
Необходимость в инициализации контейнера создаёт усложнение жизненного цикла приложения. Появляется дополнительный шаг в виде инициализации контейнера, появляется и оверхед при инстанцировании класса: в случае, когда создаётся новый инстанс класса, нам необходимо и инстанцировать зависимости или передать ссылки на синглтоны.
По сути, IoC выступает в роли конфигуратора. Создать класс
IoCContainer
средствами одних только конструкций языка оказывается невозможным в следствие зависимостей IoCContainer'а. Поэтому-то «конфигурация» связей у объектов в приложении выносится в отдельный декларативный модуль (.xml с описанием зависимостей), либо в метаданные/аннотации/докстринги в коде — всё то, что «не заметит» компилятор и не создаст проблем при приведении типов. Необходимо, чтобы компилятор получил такой код, в котором типы разрешимы.Один из примеров решения задачи, аналогично которой решает DI/IoC — это создание кроссплатформенных приложений, когда под каждую отдельную платформу нам необходимы различные реализации системных модулей. Пример: игровое приложение использует OpenGL в Linux/BSD, но использует DirectX под Windows.
В С/С++ реализация паттерна мало похожа на реализацию, например, в Java. Но по аналогии используются метаданные, как надстройка над языком. В С/С++ используются макросы условной компиляции #ifdef/#ifndef. В ходе обработки кода препроцессором, до компиляции, благодаря этим макросам, формируется файл исходного кода, который содержит только код для целевой платформы, который уже и компилируется в последствии. (todo: возможно найти/показать стандарт; в исходниках Линукс или того же nginx есть бездна примеров подобных ситуаций).
В динамически типизируемых языках, у нас нет возможности (а равно и надобности) указывать конкретный тип в сигнатуре класса, и проблемы которые решает IoC отсутствуют — мы можем передать во время рантайма любой объект в качестве c или d, в примерах сверху. Само собой, если мы нарушим «требования»
класса A
к этим зависимостям, мы нарушим логику работы приложения.Аналогичное, справедливо и для IoC в некоторых строготипизируемых языках (например Java) — единственным контрактом там служит наличие реализаций методов, сигнатуры которых указаны в интерфейсах, необязательно эти реализации удовлетворят потребностям
класса A
.В прочем, не исключение и Rust. В нём, данный контракт усилен только тем, что в сигнатуре методов возможно явно указать, что метод может создавать
panic
(т.е. порождатьRuntimeException
в терминах Java), иOption
, который лишь гарантирует что код пользователя (в данном случаекласса A
) учитывает возможность отсутствия ожидаемого объекта.В случае с
Option
, к счастью, вJava
поддерживается расширение языка, добавляющее аннотации @Nullable и @NotNull. Хотя они не являются частью стандартной поставки языка и не обрабатываются компилятором, среды разработки Eclipse и IDEA, поддерживают их. (Ссылки: https://www.jetbrains.com/help/idea/nullable-and-notnull-annotations.html https://help.eclipse.org/neon/topic/org.eclipse.jdt.doc.user/tasks/task-using_null_annotations.htm)Предложение
Скрипты в
tools
ожидают что пользователь (оператор, мы) передал корректные параметры в environment variables для соединений с Redis, Clickhouse, взаимодействия с Majorka. В случае передачи недостаточных или некорректных параметров, само выполнение этих скриптов не имеет никакого смысла. Однако, различным скриптам необходимы разные комбинации этих параметров.Если оставлять обязанность обработки параметров environment variables на коде скриптов, мы неизбежно получаем копипасту, которая будет переносится из скрипта в скрипт. Практически в каждом скрипте необходим доступ к Clickhouse, позже появится необходимость в интеграции с REST API рекламных площадок, могут потребоваться и другие сервисы (например,
majorka-cli
, которой необходим доступ к Redis).Предложение заключается в том, чтобы повторно используемый код вынести в библиотечный класс
Environment
, который выступает фасадом кos.environ
, и является контейнером для требуемых сервисов. Кроме этого,Environment
унифицирует названия env параметров. Сейчас в некоторых местах используетсяCH_URL
,CLICKHOUSE_URL
,MAJORKA_CLICKHOUSE
для обозначения одного и того же — URL Clickhouse.Пример использования в коде скрипта:
Если в environment variables не переданы параметры для Clickhouse (
CH_URL
), данный скрипт «упадет» с человекопонятной ошибкой:Set the 'CH_URL' environmental variable to the URL of Clickhouse instance/slave. Example: http://127.0.0.1:8123/
В противном случае, скрипт выполнит свою работу. В коде скрипта не нужно обрабатывать ошибочные состояния и показывать сообщение пользователю, которых очень много:SELECT 1;
) возвращает неожиданный ответАналогичные ситуации могут возникать и для Redis,
majorka-cli
и иных сторонних сервисов, настройка которых определяется системной конфигурацией.Если бы
Environment
был, базовый тест проверяющий конфигурацию установки приложения на сервере выглядел бы так:Если скрипт завершился с
returncode=0
, значит все параметры окружения настроены корректно.