четверг, 25 мая 2017 г.

Контейнеры для вставки зависимостей загрязняют код

В то время как инъекция зависимостей (aka, «DI») - естественный метод создания объектов в ООП (известный задолго до того, как термин был введен Мартином Фаулером), Spring IoC, Google Guice, Java EE6 CDI, Dagger и другие структуры DI превращают его в Анти-шаблон.

Я не собираюсь обсуждать очевидные аргументы против «инъекций сеттера» (например, Spring IoC) и «инъекции полей» (как в PicoContainer). Эти механизмы просто нарушают основные принципы объектно-ориентированного программирования и побуждают нас создавать неполные, изменяемые объекты, которые заполняются данными в ходе выполнения приложения. Помните: идеальные объекты должны быть неизменными и не должны содержать сеттеров.

Вместо этого давайте поговорим о «введении конструктора» (как в Google Guice) и его использовании с контейнерами инъекции зависимостей. Я попытаюсь показать, почему я рассматриваю эти контейнеры как избыточность.

Что такое инъекция зависимостей?

Это то, что представляет собой инъекция зависимостей (на самом деле не отличается от простой старой композиции объекта):

public class Budget {
  private final DB db;
  public Budget(DB data) {
    this.db = data;
  }
  public long total() {
    return this.db.cell(
      "SELECT SUM(cost) FROM ledger"
    );
  }
}

Данные объекта называются «зависимость».

Бюджет не знает, с какой базой данных он работает. Все, что ему нужно от базы данных - это возможность получить ячейку, используя произвольный SQL-запрос, с помощью метода cell (). Мы можем создать экземпляр бюджета с использованием PostgreSQL интерфейса DB, например:

public class App {
  public static void main(String... args) {
    Budget budget = new Budget(
      new Postgres("jdbc:postgresql:5740/main")
    );
    System.out.println("Total is: " + budget.total());
  }
}

Другими словами, мы «вводим» зависимость в новый бюджет объекта.

Альтернативой этому «инъекционному подходу» было бы позволить Бюджету решить, с какой базой данных он хочет работать:

public class Budget {
  private final DB db =
    new Postgres("jdbc:postgresql:5740/main");
  // class methods
}

Это очень грязно и приводит к 1) дублированию кода, 2) невозможности повторного использования, 3) невозможности тестирования и т. Д. Не нужно обсуждать почему. Это очевидно.

Таким образом, вставка зависимостей через конструктор является удивительной техникой. Ну, вообще-то, даже не техника. Это больше похоже на Java и все другие объектно-ориентированные языки. Ожидается, что почти любой объект захочет инкапсулировать некоторые знания (иначе, «состояние»). Для этого и нужны конструкторы.
Что такое контейнер DI?

Пока все хорошо, но здесь идет темная сторона - контейнер для инъекций зависимостей. Вот как это работает (давайте использовать Google Guice в качестве примера):

import javax.inject.Inject;
public class Budget {
  private final DB db;
  @Inject
  public Budget(DB data) {
    this.db = data;
  }
  // same methods as above
}

Обратите внимание: конструктор аннотирован @Inject.

Затем мы должны настроить контейнер где-нибудь, когда приложение запускается:

Injector injector = Guice.createInjector(
  new AbstractModule() {
    @Override
    public void configure() {
      this.bind(DB.class).toInstance(
        new Postgres("jdbc:postgresql:5740/main")
      );
    }
  }
);

Некоторые платформы даже позволяют нам конфигурировать инжектор в XML-файле.

С этого момента нам не разрешается создавать Бюджет через оператор new, как мы это делали раньше. Вместо этого мы должны использовать инжектор, который мы только что создали:

public class App {
  public static void main(String... args) {
    Injection injector = // as we just did in the previous snippet
    Budget budget = injector.getInstance(Budget.class);
    System.out.println("Total is: " + budget.total());
  }
}

Впрыск автоматически обнаруживает, что для создания экземпляра бюджета он должен предоставить аргумент для своего конструктора. Он будет использовать экземпляр класса Postgres, который мы инстанцировали в инжектор.

Это правильный и рекомендуемый способ использования Guice. Тем не менее есть несколько еще более темных рисунков, которые возможны, но не рекомендуются. Например, вы можете сделать свой инжектор одноточечным и использовать его прямо внутри класса Budget. Тем не менее, эти механизмы считаются неправильными даже разработчиками контейнеров DI, поэтому давайте их игнорировать и сосредоточимся на рекомендованном сценарии.

Для чего?

Позвольте мне повторить и суммировать сценарии неправильного использования контейнеров инъекции зависимостей:
  •     Инъекция полей
  •     Инъекциясеттера
  •     Передача инжектора в качестве зависимости
  •     Создание инжектором глобального синглтона
Если отложить все их в сторону, все, что у нас осталось, это инъекция конструктора, описанная выше. И как это нам помогает? Зачем нам это нужно? Почему мы не можем использовать простое старое new в основном классе приложения?

Созданный контейнер просто добавляет больше строк в базу кода или даже больше файлов, если мы используем XML. И он ничего не добавляет, кроме дополнительной сложности. Мы всегда должны помнить об этом, если у нас есть вопрос: «Какая база данных используется в качестве аргумента бюджета?»

Правильный путь

Теперь позвольте мне показать вам пример из реальной жизни использования new для построения приложения. Вот как мы создаем «мыслящий движок» в rultor.com (полный класс находится в Agents.java):
final Agent agent = new Agent.Iterative(
  new Array<Agent>(
    new Understands(
      this.github,
      new QnSince(
        49092213,
        new QnReferredTo(
          this.github.users().self().login(),
          new QnParametrized(
            new Question.FirstOf(
              new Array<Question>(
                new QnIfContains("config", new QnConfig(profile)),
                new QnIfContains("status", new QnStatus(talk)),
                new QnIfContains("version", new QnVersion()),
                new QnIfContains("hello", new QnHello()),
                new QnIfCollaborator(
                  new QnAlone(
                    talk, locks,
                    new Question.FirstOf(
                      new Array<Question>(
                        new QnIfContains(
                          "merge",
                          new QnAskedBy(
                            profile,
                            Agents.commanders("merge"),
                            new QnMerge()
                          )
                        ),
                        new QnIfContains(
                          "deploy",
                          new QnAskedBy(
                            profile,
                            Agents.commanders("deploy"),
                            new QnDeploy()
                          )
                        ),
                        new QnIfContains(
                          "release",
                          new QnAskedBy(
                            profile,
                            Agents.commanders("release"),
                            new QnRelease()
                          )
                        )
                      )
                    )
                  )
                )
              )
            )
          )
        )
      )
    ),
    new StartsRequest(profile),
    new RegistersShell(
      "b1.rultor.com", 22,
      "rultor",
      IOUtils.toString(
        this.getClass().getResourceAsStream("rultor.key"),
        CharEncoding.UTF_8
      )
    ),
    new StartsDaemon(profile),
    new KillsDaemon(TimeUnit.HOURS.toMinutes(2L)),
    new EndsDaemon(),
    new EndsRequest(),
    new Tweets(
      this.github,
      new OAuthTwitter(
        Manifests.read("Rultor-TwitterKey"),
        Manifests.read("Rultor-TwitterSecret"),
        Manifests.read("Rultor-TwitterToken"),
        Manifests.read("Rultor-TwitterTokenSecret")
      )
    ),
    new CommentsTag(this.github),
    new Reports(this.github),
    new RemovesShell(),
    new ArchivesDaemon(
      new ReRegion(
        new Region.Simple(
          Manifests.read("Rultor-S3Key"),
          Manifests.read("Rultor-S3Secret")
        )
      ).bucket(Manifests.read("Rultor-S3Bucket"))
    ),
    new Publishes(profile)
  )
);

Впечатляет? Это истинная композиция объекта. Я считаю, что именно так должно быть создано соответствующее объектно-ориентированное приложение.

И контейнеры DI? По-моему, они просто добавляют ненужный шум.

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

Отправить комментарий