Быть “new” или не быть…

Автор оригинала: Miško Hevery
  • Перевод
И снова здравствуйте. В преддверии старта базового и продвинутого курсов по Android-разработке мы подготовили для вас еще один интересный перевод.




Внедрение зависимостей требует от нас разделять операторы new и логику приложения. Это разделение подталкивает вас к использованию фабрик в вашем коде, которые отвечают за связывание вашего приложения. Однако, нежели писать фабрики, мы лучше будем использовать автоматическое внедрение зависимостей, такое как GUICE, которое бы взяло связывание на себя. Но действительно ли внедрение зависимостей может спасти нас от всех операторов new?
Давайте рассмотрим две крайности. Скажем, у вас есть класс MusicPlayer, который должен заполучить AudioDevice. Здесь мы хотим использовать внедрение зависимости и запросить AudioDevice в конструкторе MusicPlayer. Это позволит нам добавить дружественный к тестированию AudioDevice, который мы можем использовать, чтобы утверждать, что из нашего MusicPlayer выходит правильный звук. Если бы мы использовали оператор new для создания экземпляра BuiltInSpeakerAudioDevice, то у нас были бы некоторые трудности с тестированием. Итак, давайте называть такие объекты, как AudioDevice или MusicPlayer «Injectable». Injectable — это объекты, которые вы будете запрашивать в конструкторах и ожидать, что фреймворк для внедрения зависимостей вам их предоставит.

Теперь к другой крайности. Предположим, у вас есть примитив «int», но вы хотите автоупаковать его в «Integer», самое простое — вызвать new Integer (5), и дело с концом. Но если внедрение зависимостей является новым «new», почему мы вызываем new in-line? Повредит ли это нашему тестированию? Оказывается, что фреймворки для внедрения зависимостей не могут дать вам Integer, который вам нужен, поскольку они не понимают, о каком конкретно Integer идет речь. Это несколько игрушечный пример, поэтому давайте рассмотрим что-то более сложное.

Допустим, пользователь ввел адрес электронной почты в поле для логина, и вам нужно вызвать new Email(«a@b.com»). Можно оставить так, или же мы должны запросить Email в нашем конструкторе? Опять же, фреймворк для внедрения зависимостей не может предоставить вам Email, поскольку сначала нужно получить String, в котором находится электронное письмо. А String-ов на выбор очень много. Как вы можете заметить, существует множество объектов, которые фреймворк внедрения зависимостей никогда не сможет предоставить. Давайте назовать их «Newable», так как вы будете вынуждены вызывать для них new вручную.

Во-первых, давайте установим некоторые основные правила. Injectable класс может запрашивать другие Injectable в своем конструкторе. (Иногда я называю Injectable как Service Object, но этот термин перегружен.) Injectable, как правило, имеют интерфейсы, так как есть вероятность, что нам придется заменить их реализацией, удобной для тестирования. Тем не менее, Injectable никогда не может запросить не-Injectable (Newable) в своем конструкторе. Это потому, что фреймворк для внедрения зависимостей не знает, как создать Newable. Вот несколько примеров классов, которые я ожидал бы получить от своего фреймворка для внедрения зависимостей: CreditCardProcessor, MusicPlayer, MailSender, OfflineQueue. Точно так же Newable могут запрашивать другие Newable в своем конструкторе, но не Injectable (иногда я называю Newable как Value Object, но опять же, этот термин перегружен). Некоторые примеры Newable: Email, MailMessage, User, CreditCard, Song. Если вы будете следовать этим разграничениям, ваш код будет легок в тестировании и работе с ним. Если же вы нарушите эти правила, ваш код будет сложно тестировать.

Давайте разберемся на примере MusicPlayer и Song

class Song {
  Song(String name, byte[] content);
}
class MusicPlayer {
  @Injectable
  MusicPlayer(AudioDevice device);
  play(Song song);
}

Обратите внимание, что Song запрашивает только объекты, которые являются Newable. Это позволяет очень легко создать экземпляр Song в тесте. MusicPlayer полностью Injectable, как и его аргумент AudioDevice, поэтому его можно получить из фреймворка для внедрения зависимостей.

Теперь давайте посмотрим, что произойдет, если MusicPlayer нарушит правило и запросит Newable в своем конструкторе.

class Song {
  String name;
  byte[] content;
  Song(String name, byte[] content);
}
class MusicPlayer {
  AudioDevice device;
  Song song;
  @Injectable
  MusicPlayer(AudioDevice device, Song song);
  play();
}

Здесь Song все еще Newable, и его легко создать в вашем тесте или в вашем коде. MusicPlayer — это уже проблема. Если вы запросите MusicPlayer у вашего фреймворка для внедрения зависимостей, то произойдет сбой, так как фреймворк не будет знать, о каком Song идет речь. Большинство людей, плохо знакомых с фреймворками для внедрения зависимостей, редко делают эту ошибку, так как ее легко заметить: ваш код не будет работать.

Теперь давайте посмотрим, что произойдет, если Song нарушит правило и запросит Injectable в своем конструкторе.

class MusicPlayer {
  AudioDevice device;
  @Injectable
  MusicPlayer(AudioDevice device);
}
class Song {
  String name;
  byte[] content;
  MusicPlayer palyer;
  Song(String name, byte[] content, MusicPlayer player);
  play();
}
class SongReader {
  MusicPlayer player
  @Injectable
  SongReader(MusicPlayer player) {
    this.player = player;
  }
  Song read(File file) {
    return new Song(file.getName(),
                    readBytes(file),
                    player);
  }
}

На первый взгляд все нормально. Но подумайте о том, как будут создаваться Song. Предположительно, песни хранятся на диске, поэтому нам понадобится SongReader. SongReader должен будет запросить MusicPlayer, чтобы при вызове new для Song он мог удовлетворить зависимости Song от MusicPlayer. Заметили здесь что-нибудь не то? С какого перепугу SongReader нужно знать о MusicPlayer? Это нарушение закона Деметры. SongReader не должен знать о MusicPlayer. Хотя бы потому, что SongReader не вызывает методов с MusicPlayer. Он знает о MusicPlayer только потому, что Song нарушил разделение Newable/Injectable. SongReader платит за ошибку в Song. Так как место, где совершается ошибка и где проявляются последствия, не одно и то же, эта ошибка является очень тонкой и ее трудно диагностировать. Это также означает, что многие люди скорее всего совершают эту ошибку.

С точки зрения тестирования это настоящая боль. Предположим, у вас есть SongWriter и вы хотите убедиться, что он правильно сериализует Song на диск. Вам нужно создать MockMusicPlayer, чтобы вы могли передать его в Song, чтобы вы могли передать его в SongWritter. Почему мы вообще сталкиваемся здесь с MusicPlayer? Давайте посмотрим на это с другой стороны. Song — это то, что вы можете захотеть сериализовать, и самый простой способ сделать это — использовать сериализацию Java. Таким образом ме сериализуем не только Song, но также MusicPlayer и AudioDevice. Ни MusicPlayer, ни AudioDevice не должны быть сериализованы. Как вы можете заметить, небольшие изменения значительно облегчают тестируемость.

Как видите, работать с кодом проще, если мы будем разделять эти два вида объектов. Если вы смешаете их, ваш код будет сложно протестировать. Newable — это объекты, которые находятся в конце графа объектов вашего приложения. Newable могут зависеть от других Newable, например, как CreditCard может зависеть от Address, который может зависеть от City — эти вещи являются листами графа приложения. Поскольку они являются листами и не общаются с какими-либо внешними службами (внешние службы являются Injectable), для них не нужно делать заглушки. Ничто не похоже на String больше, чем сам String. Зачем мне делать заглушку для User, если я могу просто вызвать new User, зачем делать заглушки для чего-либо из этого: Email, MailMessage, User, CreditCard, Song? Просто вызовите new и покончите с этим.

Теперь обратим внимание на нечто очень тонкое. Это нормально для Newable знать о Injectable. Что не нормально, так это то, чтобы Newable имел в качестве поля ссылку на Injectable. Другими словами, Song может знать о MusicPlayer. Например, это нормально, чтобы Injectable MusicPlayer передавался через стек в Newable Song. Потому, что передача через стек не зависит от фреймворка для внедрения зависимостей. Как в этом примере:

class Song {
  Song(String name, byte[] content);
  boolean isPlayable(MusicPlayer player);
}

Проблема возникает, когда Song имеет поле-ссылку на MusicPlayer. Поля-ссылки устанавливаются через конструктор, что вызовет нарушение закона Деметры для вызывающей стороны, и трудности для наших тестов.

Узнать подробнее о курсах


OTUS. Онлайн-образование
Цифровые навыки от ведущих экспертов

Комментарии 2

    +3

    Перевод статьи 2008 года? Как бы 12 лет прошло)

      0
      Некоторые до сих пор живут в мире 5.4.

    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

    Самое читаемое