В главе 22 мы познакомились с понятием презентатора (presenter). Презентаторы являются разновидностью шаблона проектирования «Скромный объект» (Humble Object), помогающего выявлять и защищать архитектурные границы. Фактически чистая архитектура в предыдущей главе полна реализациями шаблона «Скромный объект».
Шаблон «Скромный объект»
Шаблон проектирования «Скромный объект»[47] первоначально предлагался для использования в модульном тестировании как способ отделения поведения, легко поддающегося тестированию, от поведения, с трудом поддающегося тестированию. Идея очень проста: разделить поведение на два модуля или класса. Один из модулей, который называется «скромным», содержит все, что с трудом поддается тестированию, в виде, упрощенном до предела. Второй — все, что было выброшено из «скромного» модуля.
Например, графические пользовательские интерфейсы сложны для модульного тестирования, потому что трудно писать тесты, которые могут видеть изображение на экране и проверять присутствие соответствующих элементов. Однако в действительности большая часть поведения пользовательского интерфейса легко тестируется. Используя шаблон «Скромный объект», можно разделить два вида поведения на два разных класса, которые называют Презентатором (Presenter) и Представлением (View).
Презентаторы и представления
Представление (View) — это «скромный» объект, сложный для тестирования. Код в этом объекте упрощается до предела. Он просто переносит данные в графический интерфейс, никак не обрабатывая их.
Презентатор (Presenter) — это легко тестируемый объект. Его задача — получить данные от приложения и преобразовать их так, чтобы Представление (View) могло просто переместить их на экран. Например, если приложению потребуется отобразить дату в некотором поле, оно должно передать Презентатору объект Date. Презентатор затем должен преобразовать дату в строку и поместить ее в простую структуру данных, которую называют Моделью представления (View Model), где Представление сможет найти ее.
Если приложению потребуется отобразить на экране денежную сумму, оно может передать Презентатору объект Currency. Презентатор должен преобразовать этот объект в строковое представление десятичного числа с соответствующим количеством десятичных знаков и знаком валюты и поместить полученную строку в Модель представления. Если отрицательные суммы должны отображаться красным цветом, тогда в Модели представления должен устанавливаться соответствующий флаг.
Каждая кнопка на экране имеет имя. Это имя является строкой в Модели представления, помещаемой туда Презентатором. Если кнопка должна отображаться как неактивная, Презентатор установит соответствующий логический флаг в Модели представления. Имя каждого пункта меню представлено строкой в Модели представления, помещаемой туда Презентатором. Имена всех радиокнопок, флажков и текстовых полей и соответствующих логических флагов устанавливаются Презентатором в Модели представления. Таблицы чисел, которые должны отображаться на экране, преобразуются Презентатором в таблицы форматированных строк в Модели представления.
Все, что отображается на экране и чем так или иначе управляет приложение, представлено в Модели представления строкой, или логическим значением, или элементом перечисления. На долю Представления остается только перенести данные из Модели представления на экран. То есть Представление играет скромную роль.
Тестирование и архитектура
Давно известно, что простота тестирования является характерным признаком хорошей архитектуры. Шаблон «Скромный объект» — хороший пример, потому что раздел между легко и тяжело тестируемыми частями часто совпадает с архитектурными границами. Раздел между Презентаторами и Представлениями — одна из таких границ, но существует много других.
Шлюзы к базам данных
Между средствами управления в вариантах использования и базами данных находятся шлюзы к базам данных[48]. Эти шлюзы являются полиморфными интерфейсами, содержащими методы для каждой операции создания, чтения, изменения и удаления, которые приложение может выполнить в базе данных. Например, если приложению может понадобиться узнать фамилии всех пользователей, работавших вчера, в интерфейсе UserGateway должен иметься метод с именем getLastNamesOfUsersWhoLoggedInAfter, принимающий объект Date и возвращающий список фамилий.
Как вы помните, мы полностью отвергаем возможность появления кода на SQL на уровне вариантов использования; для этого используются интерфейсы шлюзов, имеющие определенные методы. Эти шлюзы реализуются классами на уровне базы данных. Такие реализации являются «скромными объектами». Они просто используют код SQL или любой другой интерфейс доступа к базе данных для извлечения данных, необходимых каждому из методов. Механизмы управления в вариантах использования, напротив, не являются «скромными», потому что заключают бизнес-правила, характерные для приложения. Даже при том, что они не являются «скромными», средства управления легко поддаются тестированию, потому что шлюзы можно заменить соответствующими заглушками.
Преобразователи данных
Вернемся к теме баз данных. На каком уровне, по вашему мнению, должны находиться фреймворки ORM, такие как Hibernate?
Во-первых, давайте кое-что проясним: нет такой штуки, как инструмент объектно-реляционного преобразования (Object Relational Mapper; ORM), потому что объекты не являются структурами данных, по крайней мере с точки зрения пользователя. Пользователи объекта не видят данных, потому что все они хранятся в приватных полях. Пользователи видят только общедоступные методы объекта. То есть с точки зрения пользователя объект — это набор операций.
Структура данных, напротив, — набор общедоступных данных, не обладающих подразумеваемым поведением. Название «преобразователи данных» лучше подходит для инструментов ORM, потому что они загружают данные из реляционных таблиц в структуры.
Где должны находиться такие системы ORM? Конечно, на уровне базы данных. В действительности инструменты ORM представляют еще одну разновидность границы «Скромный объект» между интерфейсами шлюза и базой данных.
Службы
А что можно сказать о службах? Если приложение взаимодействует со службами или реализует свой набор служб, можно ли в этом случае применить шаблон «Скромный объект» для создания границы, отделяющей службу?
Конечно! Приложение должно загружать данные в простые структуры и передавать эти структуры через границу модулям, которые преобразуют данные и посылают их внешним службам. С другой стороны, в точке ввода слушатель службы должен принимать данные через интерфейс службы, преобразовывать их в простые структуры данных, пригодные для использования в приложении, и затем передавать их через границу службы.
Заключение
Практически на каждой архитектурной границе можно найти возможность применить шаблон «Скромный объект». Взаимодействия через границу почти всегда осуществляются с применением некой простой структуры данных, и граница часто делит что-то на сложное и простое для тестирования. Применение этого шаблона для организации архитектурных границ значительно улучшает возможность тестирования системы в целом.
Глава 24. Неполные границы
Полноценные архитектурные границы обходятся дорого. Они требуют определения двусторонних пограничных интерфейсов, структур для входных и выходных данных и управления зависимостями для выделения двух сторон в компоненты, компилируемые и развертываемые независимо. Это требует значительных усилий для создания и сопровождения.
Во многих ситуациях хороший архитектор мог бы посчитать затраты на создание такой границы слишком высокими, но хотел бы сохранить место для такой границы на будущее.
Подобное упреждающее проектирование часто расценивается многими последователями гибкой разработки как нарушение принципа YAGNI: «You Aren’t Going to Need It» («Вам это не понадобится»). Однако некоторые архитекторы смотрят на эту проблему и думают: «А мне может это понадобиться». В этом случае они могут реализовать неполную границу.
Пропустить последний шаг
Один из способов сконструировать неполную границу — проделать все, что необходимо для создания независимо компилируемых и развертываемых компонентов, и затем просто оставить их в одном компоненте. В этом компоненте будут присутствовать парные интерфейсы, структуры входных и выходных данных и все остальное, но все это будет компилироваться и развертываться как один компонент.
Очевидно, что для реализации такой неполной границы потребуется тот же объем кода и подготовительного проектирования, что и для полной границы. Но в этом случае не потребуется администрировать несколько компонентов. Не потребуется следить за номерами версий и нести дополнительное бремя управления версиями. Это отличие не нужно недооценивать.
Эта стратегия использовалась в начале развития FitNesse. Компонент веб-сервера FitNesse проектировался с возможностью отделения от компонентов вики и тестирования. Мы думали, что впоследствии у нас может появиться желание создать другие веб-приложения, использующие этот веб-компонент. В то же время мы не хотели вынуждать пользователей загружать два компонента. Как вы помните, одна из наших целей выражалась фразой «Загрузи и вперед». Мы специально хотели, чтобы пользователям нужно было загрузить и выполнить только один jar-файл, не заботясь о поиске других jar-файлов с совместимыми версиями и т.д.
История FitNesse также указывает на одну из опасностей такого подхода. Со временем, когда стало ясно, что отдельный веб-компонент никогда не понадобится, грань между веб-компонентом и компонентом вики стала размываться. Начали появляться зависимости, пересекающие линию в неправильном направлении. Разделить их сейчас было бы очень сложно.
Одномерные границы
Для оформления полноценной архитектурной границы требуется создать парные пограничные интерфейсы для управления изоляцией в обоих направлениях. Поддержание разделения в обоих направлениях обходится дорого не только на начальном этапе, но и на этапе сопровождения.
На рис. 24.1 показана более простая схема, помогающая зарезервировать место для последующего превращения в полноценную границу. Это пример традиционного шаблона «Стратегия». Клиенты пользуются интерфейсом ServiceBoundary, который реализуют классы ServiceImpl.
Рис. 24.1. Шаблон «Стратегия»
Должно быть ясно, что это создает основу для будущей архитектурной границы. Здесь имеет место инверсия зависимости, необходимая для отделения клиента от класса ServiceImpl. Также должно быть ясно, что разделение может очень быстро стираться, о чем свидетельствует пунктирная стрелка на диаграмме. В отсутствие парных интерфейсов ничто не мешает появлению таких обратных зависимостей, кроме старательности и дисциплинированности разработчиков и архитекторов.
Фасады
Еще более простой подход к организации границ дает шаблон «Фасад», изображенный на рис. 24.2. В этом случае отсутствует даже инверсия зависимостей. Граница определяется простым классом Facade c методами, представляющими службы и реализующими обращения к службам, к которым клиенты, как предполагается, не должны иметь прямого доступа.
Рис. 24.2. Шаблон «Фасад»
Обратите внимание, однако, что клиент имеет транзитивную (переходную) зависимость от всех этих классов служб. В языках со статической системой типов изменение исходного кода в одном из классов служб вызывает необходимость повторной компиляции клиента. Также представьте, насколько просто в этой схеме создать обратные связи.
Заключение
Мы увидели три простых способа реализации неполных архитектурных границ. Конечно, таких способов намного больше. Эти три стратегии служат лишь примерами.
Каждый из представленных подходов имеет свои достоинства и недостатки. Каждый подходит на роль заменителя полноценной архитектурной границы в определенных контекстах. И каждый может со временем деградировать, если граница никогда не будет материализована.
Одна из задач архитектора — решить, где провести архитектурную границу и как ее реализовать, частично или полностью.