Внедрение зависимостей просит нас отделить новые операторы от логики приложения . Это разделение заставляет ваш код иметь фабрики, которые отвечают за соединение вашего приложения . Однако лучше, чем при написании фабрик, мы хотим использовать автоматическое внедрение зависимостей, такое как GUICE, чтобы сделать подключение для нас. Но действительно ли DI может спасти нас от всех новых операторов?
Давайте посмотрим на две крайности. Скажем, у вас есть класс MusicPlayer, который должен овладеть AudioDevice. Здесь мы хотим использовать DI и запросить AudioDevice в конструкторе MusicPlayer. Это позволит нам добавить дружественный к тесту AudioDevice, который мы можем использовать, чтобы утверждать, что правильный звук выходит из нашего MusicPlayer. Если бы мы использовали новый оператор для создания экземпляра BuiltInSpeakerAudioDevice, у нас были бы трудности с тестированием. Итак, давайте назовем такие объекты, как AudioDevice или MusicPlayer «Injectables». Injectables — это объекты, которые вы будете запрашивать в конструкторах и ожидать, что инфраструктура DI предоставит.
Теперь давайте посмотрим на другую крайность. Предположим, у вас есть примитив «int», но вы хотите автоматически поместить его в «Integer», самое простое — вызвать новый Integer (5), и все готово. Но если DI является новым «новым», почему мы называем новый in-line? Повредит ли это нашему тестированию? Оказывается, что структуры DI не могут дать вам целое число, которое вы ищете, так как оно не знает, какое целое число вы имеете в виду. Это немного игрушечный пример, поэтому давайте рассмотрим что-то более сложное.
Допустим, пользователь ввел адрес электронной почты в поле входа в систему, и вам нужно позвонить по новой электронной почте («[email protected]»). Это нормально, или мы должны запросить адрес электронной почты в нашем конструкторе. Опять же, инфраструктура DI не может предоставить вам электронное письмо, поскольку сначала нужно получить строку, где находится электронное письмо. И есть из чего выбрать. Как вы можете видеть, существует множество объектов, которые DI Framework никогда не сможет предоставить. Давайте назовем эти «новые», так как вы будете вынуждены вызывать новые вручную.
Во-первых, давайте установим некоторые основные правила. Класс Injectable может запрашивать другие Injectables в своем конструкторе. (Иногда я называю Injectables объектами службы, но этот термин перегружен.) Injectables, как правило, имеют интерфейсы, так как есть вероятность, что нам, возможно, придется заменить их реализацией, удобной для тестирования. Тем не менее, Injectable никогда не может запросить не Injectable (Newable) в своем конструкторе. Это потому, что DI Framework не знает, как создать Newable. Вот несколько примеров классов, которые я ожидал бы получить от своей инфраструктуры DI: CreditCardProcessor, MusicPlayer, MailSender, OfflineQueue. Точно так же Newables может запрашивать другие Newables в своем конструкторе, но не Injectables (иногда я называю Newables объектом значения, но опять же, термин перегружен). Некоторые примеры Newables: электронная почта, MailMessage, пользователь, CreditCard,Песня. Если вы сохраните эти различия, ваш код будет легко тестировать и работать с ним. Если вы нарушите это правило, ваш код будет сложно протестировать.
Давайте посмотрим на пример MusicPlayer и песни
class Song { Song(String name, byte[] content); } class MusicPlayer { @Injectable MusicPlayer(AudioDevice device); play(Song song); }
Обратите внимание, что Song запрашивает только объекты, которые являются Newables. Это позволяет очень легко создать песню в тесте. Музыкальный проигрыватель полностью Injectable, как и его аргумент AudioDevice, поэтому его можно получить из DI Framework.
Теперь давайте посмотрим, что произойдет, если 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(); }
Здесь песня все еще нова, и ее легко создать в вашем тесте или в вашем коде. MusicPlayer это проблема. Если вы спросите DI Framework для MusicPlayer, то произойдет сбой, так как DI Framework не будет знать, на какую песню вы ссылаетесь. Большинство людей, плохо знакомых с DI-фреймворками, редко делают эту ошибку, так как это легко увидеть: ваш код не будет работать.
Теперь давайте посмотрим, что произойдет, если песня нарушит правило и попросит 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); } }
Сначала мир выглядит хорошо. Но подумайте о том, как будут создаваться песни. Предположительно, песни хранятся на диске, поэтому нам понадобится SongReader. SongReader должен будет запросить MusicPlayer, чтобы при вызове нового для песни он мог удовлетворить зависимости Song от MusicPlayer. Видите что-нибудь не так здесь? Почему в мире SongReader нужно знать о MusicPlayer. Это нарушение закона Деметры, SongReader не должен знать о MusicPlayer. Вы можете сказать, так как SongReader не вызывает метод MusicPlayer. Он знает только о MusicPlayer, потому что песня нарушила разделение Newable / Injectable. SongReader платит цену за ошибку в песне. Поскольку место, где совершается ошибка, и где ощущается боль, не одно и то же, эта ошибка очень тонкая и ее трудно диагностировать. Это также означает, что многие люди совершают эту ошибку.
Теперь с точки зрения тестирования это настоящая боль. Предположим, у вас есть SongWriter и вы хотите убедиться, что он правильно сериализует песню на диск. Почему вам нужно создать MockMusicPlayer, чтобы вы могли передать его в песню, чтобы вы могли передать его в SongWritter. Почему MusicPlayer на картинке? Давайте посмотрим на это с другой стороны. Песня — это то, что вы можете захотеть сериализовать, и самый простой способ сделать это — использовать сериализацию Java. Это позволит сериализовать не только песню, но также MusicPlayer и AudioDevice. Ни MusicPlayer, ни AudioDevice не должны быть сериализованы. Как вы можете заметить, небольшие изменения значительно облегчают тестируемость.
Как видите, с кодом проще всего работать, если мы будем различать эти два вида объектов. Если вы смешаете их, ваш код будет сложно протестировать. Newables — это объекты, которые находятся в конце графа объектов вашего приложения. Новинки могут зависеть от других новинок, так как в CreditCard может зависеть от адреса, который может зависеть от города, но эти вещи являются листами графа приложения. Поскольку они являются листами и не общаются с какими-либо внешними сервисами (внешние сервисы являются инъецируемыми), им не нужно издеваться над ними. Ничто не ведет себя так, как строка, чем строка. Зачем мне издеваться над пользователем, если я могу просто нового пользователя, зачем издеваться над любым из них: электронная почта, MailMessage, пользователь, CreditCard, Song? Просто позвоните новому и покончите с этим.
Теперь вот что-то очень тонкое. Это нормально для новичка, чтобы узнать о инъекционных. Что не хорошо, так это то, что Newable имеет ссылку на поле Injectable. Другими словами, для песни нормально знать о MusicPlayer. Например, нормально, чтобы Injectable MusicPlayer передавался через стек в новую песню. Это потому, что передача стека не зависит от структуры DI. Как в этом примере:
class Song { Song(String name, byte[] content); boolean isPlayable(MusicPlayer player); }
Проблема возникает, когда песня имеет ссылку на поле MusicPlayer. Ссылки на поля устанавливаются через конструктор, который вызовет нарушение закона Деметры для вызывающей стороны, и нам будет трудно проверить.