vladignatyev / majorka

0 stars 0 forks source link

Про Dependency Injection/IoC и Энвайронмент #27

Open vladignatyev opened 5 years ago

vladignatyev commented 5 years ago

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) учитывает возможность отсутствия ожидаемого объекта.

В случае с 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.

Пример использования в коде скрипта:

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

  1. Не передан URL
  2. URL передан, однако по этому URL сервер отвечает 61: Connection refused
  3. 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, значит все параметры окружения настроены корректно.