Статьи

Три шаблона проектирования, которые используют инверсию управления

Для многих разработчиков инверсия управления (IoC) является нечеткой концепцией, которая практически не применяется в реальном мире. В лучшем случае это просто эквивалент внедрения зависимостей (DI). Однако уравнение IoC = DI верно, когда обе стороны ссылаются на инвертирование управления управлением зависимостями. Хотя внедрение зависимостей на самом деле является широко известной формой IoC, правда состоит в том, что IoC представляет собой гораздо более широкую парадигму проектирования программного обеспечения, которая может быть реализована с помощью нескольких шаблонов . В этой статье мы рассмотрим, как внедрение зависимости, шаблон наблюдателя и шаблонный шаблонный метод реализуют инверсию управления.

Как и во многих других шаблонах проектирования из богатого репертуара, реализация IoC является компромиссом для разработчика:

  • Разработка компонентов с высокой степенью разделения и инкапсуляция логики приложения в одном месте являются прямыми и естественными последствиями реализации IoC.
  • С другой стороны, реализация требует построения как минимум одного уровня косвенности, а в некоторых случаях это может быть просто излишним.

Рассмотрение нескольких конкретных реализаций поможет вам найти компромисс между этими свойствами.

Демистификация парадигмы IoC

Инверсия контроля — это паттерн с несколькими наклонами. Типичный пример IoC дает Мартин Фаулер в следующей простой программе, которая собирает пользовательские данные с консоли:

public static void main(String[] args) { while (true) { BufferedReader userInputReader = new BufferedReader( new InputStreamReader(System.in)); System.out.println("Please enter some text: "); try { System.out.println(userInputReader.readLine()); } catch (IOException e) { e.printStackTrace(); } } } 

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

Теперь рассмотрим обновленную версию программы, которая использует графический интерфейс пользователя (GUI) для сбора входных данных через текстовое поле, кнопку и прослушиватель действий, привязанных к нему. В этом контексте каждый раз, когда пользователь нажимает кнопку, введенный текст собирается слушателем и распечатывается на панели.

В этой версии программы она фактически находится под контролем модели прослушивателя событий (в данном случае это платформа) для вызова кода, написанного разработчиком, для чтения и печати пользовательского ввода. Проще говоря, фреймворк будет вызывать код разработчика, а не наоборот. Фреймворк на самом деле является расширяемой структурой, которая предоставляет разработчику набор особых точек для внедрения сегментов пользовательского кода.

В этом смысле управление было эффективно инвертировано .

С более общей точки зрения каждая вызываемая точка расширения, определяемая платформой, либо в форме реализации (ов) интерфейса , либо наследования реализации (также называемого подклассом), является четко определенной формой IoC.

Рассмотрим случай простого сервлета :

 public class MyServlet extends HttpServlet { protected void doPost( HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // developer implementation here } protected void doGet( HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // developer implementation here } } 

Здесь класс HttpServlet (принадлежащий платформе) — это элемент, который полностью контролирует программу, а не подкласс MyServlet . Код в методах doGet() и doPost() автоматически вызывается сервлетом в ответ на HTTP-запросы GET и POST после его создания контейнером сервлета.

По сравнению с типичной перспективой наследования, где подклассы имеют элемент управления вместо базового класса, элемент управления был инвертирован .

Фактически, методы сервлета являются реализацией шаблона метода шаблона , который мы подробно обсудим позже.

В случае каркасов, которые придерживаются принципа открытого / закрытого за счет предоставления расширяемого API, роль разработчика, использующего каркас, сводится к определению их собственного набора пользовательских классов, либо путем реализации одного или нескольких интерфейсов, предоставляемых каркасом, либо наследуя от существующих базовых классов. В свою очередь, экземпляры классов непосредственно создаются и вызываются платформой.

Процитирую Фаулера:

Фреймворк вызывает разработчика, а не разработчик называет фреймворк.

Следовательно, IoC часто называют Голливудским принципом:

Не звоните нам, мы вам позвоним.

Инверсия управления, используемая внедрением зависимостей, наблюдателями и шаблоном метода шаблона

Реализация инверсии управления

На этом этапе становится очевидным, что существует несколько методологий для реализации инверсии управления. Давайте рассмотрим сводку о том, как реализовать наиболее распространенные из них.

IoC через инъекцию зависимостей

Как было сказано ранее, DI — это всего лишь одна из форм IoC, и, вполне возможно, одна из наиболее распространенных в объектно-ориентированном дизайне. Но давайте подумаем над этим: каким образом DI фактически инвертирует управление?

Чтобы ответить на это, давайте создадим наивный пример:

 public interface UserQueue { void add(User user); void remove(User user); User get(); } public abstract class AbstractUserQueue implements UserQueue { protected LinkedList<User> queue = new LinkedList<>(); @Override public void add(User user) { queue.addFirst(user); } @Override public void remove(User user) { queue.remove(user); } @Override public abstract User get(); } public class UserFifoQueue extends AbstractUserQueue { public User get() { return queue.getLast(); } } public class UserLifoQueue extends AbstractUserQueue { public User get() { return queue.getFirst(); } } 

Интерфейс UserQueue определяет открытый API простой очереди, в которой хранятся пользовательские объекты (реализация класса User здесь опущена для краткости), тогда как AbstractUserQueue обеспечивает некоторую общую реализацию далее по иерархии. Наконец, UserFifoQueue и UserLifoQueue реализуют базовые очереди FIFO и LIFO .

Это эффективный способ реализации подтипа полиморфизма . Но что это дает нам в конкретных условиях? Вообще-то, довольно.

Создавая клиентский класс, который объявляет зависимость от абстрактного типа UserQueue (он же сервис в терминологии DI), можно внедрить различные реализации во время выполнения без рефакторинга кода, использующего клиентский класс:

 public class UserProcessor { private UserQueue userQueue; public UserProcessor(UserQueue userQueue) { this.userQueue = userQueue; } public void process() { // process queued users here } } 

UserProcessor в двух словах показывает, почему DI на самом деле является формой IoC.

Мы могли бы поместить элемент управления для получения зависимости от очереди в UserProcessor , непосредственно UserProcessor их экземпляры в конструкторе, через несколько жестко закодированных new операторов. Но это типичный запах кода, который вводит прочную связь между клиентскими классами и их зависимостями и отправляет тестируемость в гибель. Я слышу несколько звонков тревоги в моих ушах! Не так ли? Да, это был бы плохой дизайн.

Вместо этого класс объявляет зависимость от абстрактного типа UserQueue в конструкторе, таким образом, он больше не находится под его контролем, чтобы искать своего сотрудника через new оператор в конструкторе. И наоборот, зависимость вводится извне, либо с помощью структура DI ( CDI и Google Guice — это отличные примеры сторонних инжекторов ) или просто фабрики и застройщики старой школы.

В двух словах, с DI контроль над тем, как зависимости получают клиентские классы, больше не находится в этих классах; вместо этого он находится в инжекторах :

 public static void main(String[] args) { UserFifoQueue fifoQueue = new UserFifoQueue(); fifoQueue.add(new User("user1")); fifoQueue.add(new User("user2")); fifoQueue.add(new User("user3")); UserProcessor userProcessor = new UserProcessor(fifoQueue); userProcessor.process(); } 

Это будет работать, как и ожидалось, и внедрение реализации UserLifoQueue довольно просто. Ясно видеть, что DI — это просто способ достижения инверсии управления (в этом случае DI — это уровень косвенности для реализации IoC).

IoC через шаблон наблюдателя

Другой простой способ реализации IoC — через шаблон наблюдателя . В более широком смысле, способ, которым наблюдатели инвертируют элемент управления, аналогичен тому, как слушатель действия в контексте GUI. В то время как в случае слушателей действий они вызываются в ответ на определенное пользовательское событие (щелчок мыши, несколько событий клавиатуры / окна и т. Д.), Наблюдатели обычно используются для отслеживания изменений в состоянии объекта модели. в контексте модельного представления .

В классической реализации один или несколько наблюдателей привязываются к наблюдаемому объекту (он же субъект в терминологии шаблона), например, путем вызова метода addObserver . Как только привязки между субъектом и наблюдателем (ями) были определены, наблюдатели вызывают в ответ на изменение состояния субъекта.

Чтобы лучше понять эту концепцию, рассмотрим следующий пример:

 @FunctionalInterface public interface SubjectObserver { void update(); } 

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

А вот и наблюдаемый класс:

 public class User { private String name; private List<SubjectObserver> observers = new ArrayList<>(); public User(String name) { this.name = name; } public void setName(String name) { this.name = name; notifyObservers(); } public String getName() { return name; } public void addObserver(SubjectObserver observer) { observers.add(observer); } public void deleteObserver(SubjectObserver observer) { observers.remove(observer); } private void notifyObservers(){ observers.stream().forEach(observer -> observer.update()); } } 

Класс User является простым классом домена, который уведомляет присоединенного наблюдателя (ей) о каждом изменении его состояния с помощью методов установки.

С интерфейсом SubjectObserver и темой на месте, вот как можно наблюдать экземпляр:

 public static void main(String[] args) { User user = new User("John"); user.addObserver(() -> System.out.println( "Observable subject " + user + " has changed its state.")); user.setName("Jack"); } 

Каждый раз, когда состояние user объекта изменяется через установщик, наблюдатель получает уведомление и, следовательно, выводит сообщение на консоль. До сих пор это было довольно тривиальным применением схемы наблюдателя. Что не так тривиально, так это посмотреть, как управление инвертируется в этом случае.

С паттерном наблюдателя субъект становится «рамкой», которая контролирует, кого и когда вызывают. Это наблюдатели, чей контроль отнимается, потому что они не имеют никакого влияния на то, когда их вызывают (пока они остаются зарегистрированными субъектом). Это означает, что мы можем определить одну линию, где управление инвертировано — это когда наблюдатель привязан к субъекту:

 user.addObserver(() -> System.out.println( "Observable subject " + user + " has changed its state.")); 

Это показывает в двух словах, почему шаблон наблюдателя (или слушатель действий в среде с графическим интерфейсом) является довольно простым способом достижения IoC. Именно в этой форме децентрализованного проектирования компонентов программного обеспечения происходит инверсия управления .

IoC через шаблон шаблонов

Мотивация шаблона шаблонного метода состоит в том, чтобы определить общий алгоритм в базовом классе с помощью нескольких абстрактных методов (или этапов алгоритма), и позволить подклассам предоставлять конкретные реализации их, сохраняя структуру алгоритма без изменений.

Мы могли бы применить эту концепцию и определить общий алгоритм для обработки сущностей домена:

 public abstract class EntityProcessor { public final void processEntity() { getEntityData(); createEntity(); validateEntity(); persistEntity(); } protected abstract void getEntityData(); protected abstract void createEntity(); protected abstract void validateEntity(); protected abstract void persistEntity(); } 

Метод processEntity() — это шаблонный метод, который определяет структуру алгоритма, который обрабатывает объекты, а абстрактные методы — это шаги алгоритма, которые должны быть реализованы подклассами. Несколько версий алгоритма могут быть созданы путем создания подкласса EntityProcessor столько раз, сколько требуется, и обеспечения различных реализаций абстрактных методов.

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

В типичной реализации наследования подклассы вызывают методы, определенные в базовом классе. В этом случае происходит обратное: методы, реализуемые подклассами (шаги алгоритма), вызываются в базовом классе через метод шаблона. Таким образом, именно в базовом классе фактически находится элемент управления, а не в подклассах .

Это еще один классический пример IoC, достигнутый благодаря иерархической структуре. В этом случае метод шаблона — это просто причудливое имя для определения вызываемой точки расширения, которая используется разработчиком для предоставления своего собственного набора реализаций.

Резюме

Несмотря на то, что в экосистеме Java преобладает инверсия управления, особенно во многих доступных инфраструктурах и повсеместном внедрении внедрения зависимостей, для многих разработчиков шаблон все еще остается расплывчатым и в значительной степени ограничивается введением зависимостей. В этой статье я разъяснил эту концепцию, продемонстрировав несколько подходов, которые можно использовать для реализации IoC в реальном мире.

  • Внедрение зависимостей. Элемент управления способом получения зависимостей клиентскими классами больше не находится в этих классах. Вместо этого он находится в базовых структурах инжекторов / DI.

  • Образец наблюдателя: контроль передается от наблюдателей субъекту, когда дело доходит до реагирования на изменения.

  • Шаблонный шаблон: элемент управления находится в базовом классе, который определяет метод шаблона, а не в подклассах, реализующих шаги алгоритма.

Как обычно, как и когда использовать IoC — это то, что следует оценивать для каждого конкретного случая, не впадая в бессмысленное слепое поклонение.