Продолжается подписка на наши издания! Вы не забыли подписаться?

Структурные элементы предметно-ориентированного проектирования

Автор: Эрик Эванс
Опубликовано: 29.12.2010
Версия текста: 1.1

Многоуровневая архитектура
Пример
Разбиение сетевой системы банковского обслуживания на уровни
Связь между уровнями
Архитектурные среды
Уровень предметной области — вместилище модели
“Антишаблон” интеллектуального интерфейса пользователя
Другие виды изоляции
Ассоциации
Пример: ассоциации в модели брокерского счета
Сущности (указуемые объекты)
Моделирование СУЩНОСТЕЙ
Проектирование операций идентификации
Объектызначения
Пример
Оптимизация базы данных с помощью объектов-значений
Проектирование ассоциаций с помощью ОБЪЕКТОВ-ЗНАЧЕНИЙ
Службы
Службы и изоляция уровня предметной области
Степень модульности
Доступ к службам
Модули (пакеты)
Гибкая модульность
Правила программирования пакетов в Java
Ловушки инфраструктуры
Парадигмы моделирования
Причины доминирования объектной парадигмы
Не-объекты в объектном мире
ПРОЕКТИРОВАНИЕ ПО МОДЕЛИ в условиях смешения парадигм

Изоляция предметной области

Та часть программы, которая собственно и решает задачи из предметной области, — это, как правило, лишь небольшой фрагмент всей программной системы, хотя его значение далеко превосходит размеры. Чтобы применить наиболее эффективные подходы, необходимо посмотреть на элементы модели как на систему. Не следует поддаваться тенденции просто выдергивать их из значительно большей совокупности объектов, наподобие выделения созвездий на ночном небе. Необходимо отграничить объекты предметной области от остальных функций системы и тем самым избежать путаницы между понятиями этой области и программными технологиями, а то и опасности вовсе потерять видение предмета в общей массе системы. Существуют сложные и изощренные приемы такой изоляции предметной области. Эти вопросы хорошо изучены, но они настолько важны для успешного применения принципов моделирования на основе структуры предметной области, что осветить их хотя бы кратко необходимо, — конечно, с предметно-ориентированной точки зрения.

Многоуровневая архитектура


Для реализации в программе обслуживания грузоперевозок простого действия пользователя — выбора места назначения груза из списка городов — необходим программный код, который бы (1) выводил на экран некий интерфейс для диалога с пользователем, (2) запрашивал в базе данных список городов, (3) интерпретировал и проверял информацию, введенную пользователем, (4) ассоциировал выбранный город с грузом и (5) отражал сделанное изменение в базе данных. Весь этот код принадлежит одной программе, но к собственно грузоперевозкам относится лишь небольшая часть его.

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

В объектно-ориентированной программе код для реализации интерфейса пользователя, обращений к базе данных и других технических задач нередко вписывают напрямую в объекты прикладной модели (business objects). Кроме того, часть прикладной логики (business logic), т.е. алгоритмической части программы, часто реализуется прямо в элементах интерфейса пользователя и сценариях баз данных. Это происходит потому, что так легче всего работать на ближнюю перспективу.

Когда код, относящийся к операциям предметной области, размазан по огромным объемам другого кода, его становится очень трудно разыскивать и анализировать. Поверхностные изменения в интерфейсе пользователя могут случайно затронуть и операции алгоритмической части. А чтобы изменить какие-либо правила делового регламента (business rules), потребуется тщательная трассировка кода интерфейса, обращений к базе данных и других элементов программы. Реализация логически последовательных, основанных на модели объектов становится непрактичной. Автоматизированное тестирование оказывается неудобным. Учитывая объем технологий и операций в каждом из этих видов работ, программу следует “удерживать” в максимально упрощенном виде, иначе понять ее станет невозможно.

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

Существует много способов членения программной системы, но по накопленному в программной индустрии опыту и неписаным соглашениям преимущество отдается МНОГОУРОВНЕВОЙ АРХИТЕКТУРЕ (LAYERED ARCHITECTURE), состоящей из нескольких достаточно стандартных уровней. Метафора многоуровневости настолько широко распространена, что большинство программистов воспринимают ее естественно, интуитивно. В литературе о многоуровневой архитектуре написано достаточно много и хорошо, иногда даже в формате шаблона (например, [7]). Важнейший принцип многоуровневости состоит в том, что любой элемент какого-нибудь уровня зависит от работы только других элементов того же уровня или элементов более низких уровней. Передача информации наверх должна выполняться косвенными способами, о чем еще будет говориться.

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

Интерфейс пользователя (User Interface) или Уровень представления (Presentation Layer)

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

Операционный уровень или Уровень прикладных операций (Application Layer)

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

Этот уровень не нужно “раздувать” в размерах. В нем не содержатся ни знания, ни деловые регламенты (business rules), а только выполняется координирование задач и распределение работы между совокупностями объектов предметной области на следующем, более низком уровне. В нем не отражается состояние объектов прикладной модели, но зато он может содержать состояние, информирующее пользователя о степени выполнения задачи для информирования пользователя

Уровень предметной области (Domain Layer) или Уровень модели (Model Layer)

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

Этот уровень является главной, алгоритмической частью программы

Инфраструктурный уровень (Infrastructure Layer)

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

В некоторых проектах не делают четкого разграничения между уровнями пользовательского интерфейса и прикладных операций. В других есть несколько инфраструктурных уровней. Но ПРОЕКТИРОВАНИЕ ПО МОДЕЛИ (MODEL-DRIVEN DESIGN) становится возможным только в том случае, если четко отделен уровень предметной области.

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

Отделение уровня предметной области от уровней инфраструктуры и пользовательского интерфейса дает возможность гораздо тщательней спроектировать каждый из них. Изолированные уровни значительно легче дорабатывать и поддерживать, поскольку они имеют тенденцию развиваться разными темпами и обслуживать различные потребности. Изоляция способствует также гибкому и эффективному размещению модулей программы в распределенной системе — разные уровни можно разместить на различных серверах или клиентских станциях, чтобы минимизировать обмен данными и улучшить быстродействие [11].

Пример

Разбиение сетевой системы банковского обслуживания на уровни

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

Чтобы не перегружать этот пример, я опустил все основные технические подробности, в частности вопросы безопасности. Структура предметной области тоже изрядно упрощена. (В условиях реальной сложности потребность в многоуровневой архитектуре только возросла бы.) Более того, и подразумеваемая в примере инфраструктура специально задумана как простая и ясная, чтобы не усложнять пример, — в действительности для такой программы требуется совершенно другая структура. Обязанности же оставшихся частей программы будут распределены по уровням, как показано на рис. 4.1.

Обратите внимание, что за реализацию деловых регламентов (business rules) отвечает не уровень прикладных операций (application), а уровень предметной области (domain). В данном случае правило регламента выражается так: “Для всякого кредита найдется соответствующий дебет”.

На операционном уровне также не делается никаких предположений об источнике запроса на перевод. Можно считать, что программа содержит интерфейс пользователя с полями ввода для номеров счетов и сумм, а также с кнопками для команд. Но этот интерфейс можно и заменить сетевым XML-запросом, никак не влияя на операционный или любой другой уровень, расположенный ниже. Это разделение важно не только потому, что в программных проектах часто бывает необходимо заменить интерфейс пользователя на обработку сетевых запросов, но и потому, что оно помогает распределять обязанности и поддерживать архитектуру уровней в удобном для понимания и доработки виде.

Фактически рис. 4.1 в какой-то мере иллюстрирует проблемы, которые возникают из-за неизолированности предметной области. Так как на рисунке нужно было показать все операции от запроса до управления транзакцией, уровень предметной области пришлось упростить, чтобы вся работа системы в целом осталась обозримой. А вот если сосредоточиться на проектировании только изолированного предметного уровня, то и на странице, и в головах у нас хватило бы места для модели, которая лучше представляет логику модели — даже содержит объекты главной книги, дебета и кредита, валютных транзакций.


Рисунок 4.1. Объекты наделены обязанностями, соответствующими их уровню, и связаны с другими объектами своего уровня

Связь между уровнями

До сих пор речь у нас шла о разделении уровней и усовершенствовании архитектуры всех частей и аспектов программы, в частности уровня предметной области (domain layer), благодаря этому разделению. Но, разумеется, между уровнями должна также существовать связь. Как установить такую связь и не потерять преимуществ разделения — этой проблемой задавались создатели многочисленных архитектурных шаблонов.

Уровни должны быть связаны нежестко, и зависимость проектирования одного уровня от другого должна быть направлена только в одну сторону. Верхние уровни могут использовать элементы нижних или манипулировать ими напрямую: путем вызова их общедоступных (public) интерфейсов, хранения ссылок на них (хотя бы временного) или каким-нибудь другим обычным для программ способом. Но если объекту нижнего уровня требуется связь с верхним (причем связь, не вписывающаяся в рамки простого ответа на прямой запрос), то для этого нужен другой механизм, основанный на таких архитектурных шаблонах взаимодействующих слоев, как уведомление (callback) или НАБЛЮДАТЕЛЬ (OBSERVER) [14].

“Дедушка всех шаблонов”, связывающих интерфейс пользователя с уровнями прикладных операций и предметной области, — это модель-представление-контроллер (model-view-controller, MVC). Его впервые ввели в мире Smalltalk еще в 1970-е, и с тех пор на его основе было создано множество архитектур интерфейсов. В [13] рассматривается этот шаблон и несколько полезных вариаций на тему. В [17] эти проблемы нашли освещение в ШАБЛОНЕ РАЗДЕЛЕНИЯ МОДЕЛИ И ПРЕДСТАВЛЕНИЯ (MODEL-VIEW SEPARATION PATTERN); там же предлагается один из вариантов связи с операционным уровнем, КООРДИНАТОР ПРИКЛАДНЫХ ОПЕРАЦИЙ (APPLICATION COORDINATOR).

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

Инфраструктурный уровень обычно не инициирует никаких операций на уровне предметной области. Находясь “ниже” предметной области, он не обязан знать ничего конкретного об объектах, которые он обслуживает. Такие технические возможности чаще всего предоставляются в виде СЛУЖБ (SERVICES). Например, если приложению нужно послать письмо по электронной почте, то на инфраструктурном уровне найдется для этих целей некий почтовый интерфейс, а элементы операционного уровня смогут запросить передачу сообщения. Такое разграничение дает дополнительную гибкость. Почтовый интерфейс может подключаться к модулю отправки электронной почты, модулю отправки факсов или любому другому аналогичному средству. Но главное преимущество заключается в упрощении операционного уровня, сосредоточении его на выполнении узкого круга обязанностей: он знает, когда послать сообщение, но ему не нужно знать, как это сделать.

Итак, уровни прикладных операций (application) и предметной области (domain) обращаются к СЛУЖБАМ (SERVICES), предоставляемым инфраструктурным (infrastructure) уровнем. Если область действия СЛУЖБЫ выбрана удачно, а ее интерфейс хорошо спроектирован, то вызывающая сторона привязана к ней очень слабо и не подвержена влиянию той сложной функциональности, которая может стоять за интерфейсом СЛУЖБЫ.

Но не вся инфраструктура сводится к СЛУЖБАМ, вызываемым с верхних уровней. Некоторые технические компоненты проектируются так, чтобы напрямую поддерживать функционирование других уровней (например, предоставлять абстрактный базовый класс для всех объектов предметной области) и механизм, через который они взаимодействуют (например, реализации MVC и т.п.). Такая архитектурная среда (architectural framework) имеет значительно большее влияние на структуру других частей программы, чем службы.

Архитектурные среды

Если инфраструктура реализована в форме СЛУЖБ (SERVICES), вызываемых через интерфейсы, то многоуровневость и разделение уровней достигается вполне естественно, без принципиальных затруднений. Но некоторые технические проблемы требуют более “навязчивых” форм инфраструктуры. В подобных инфраструктурных средах (frameworks) реализуются в интегрированной форме нужные функции инфраструктуры, и одновременно эти среды навязывают другим уровням определенные способы реализации — например, в виде подклассов одного из классов среды или с заранее заданными сигнатурами методов. (Может показаться неестественным, что подкласс находится на уровне выше, чем родительский класс, но следует учесть, в каком из классов содержится больше знания о другом.) Лучшие архитектурные среды решают сложные технические задачи и при этом позволяют разработчику уровня предметной области сосредоточиться на выражении модели. Но среды могут и помешать работе: либо накладывая слишком много ограничений, сужающих выбор проектных решений в предметной области, либо делая реализацию столь тяжеловесной, что это тормозит разработчиков.

Обычно для работы требуется архитектурная среда в каком-то виде (кстати, группа разработчиков вполне может выбрать и такую среду, которая сослужит ей плохую службу). Используя среду, разработчики не должны забывать о своей цели: строить программную реализацию, выражающую модель предметной области, и решать с ее помощью важную прикладную задачу. Группа должна применять среду в той мере, в какой решается главная задача, пусть при этом задействуются и не все имеющиеся возможности. Например, в ранних приложениях на основе J2EE все объекты предметной области часто реализовывались в виде хранимых объектов Entity Beans. Эта методика снижала как быстродействие, так и темпы разработки. В настоящее время хорошим стилем считается использовать среду J2EE для более крупномасштабных объектов, реализуя большую часть прикладной модели в виде оригинальных объектов Java. Большинства недостатков архитектурных сред удается избежать, если применять их избирательно для преодоления трудных мест, а не искать одно общее готовое решение. Скрупулезный отбор только наиболее ценных в каждом случае функций среды снижает зависимость программной реализации от этой среды, давая большую гибкость в выборе будущих проектных решений. Что еще важнее, учитывая высокую сложность работы со многими современными средами, такой минимализм помогает сохранять объекты прикладной модели выразительными и доступными для чтения.

Архитектурным средам и другим средствам разработки, конечно, предстоит еще развиваться. В более новых средах все больше технических аспектов приложений будет реализовываться автоматически. Если эту автоматизацию применять правильно, разработчики смогут все лучше концентрироваться на моделировании поведения именно прикладного уровня задачи, чем повысят производительность и качество своего труда. Но двигаясь в этом направлении, следует сдерживать энтузиазм в отношении технических решений; сложные архитектурные среды вполне способны стеснять “свободу движений” разработчиков прикладных систем.

Уровень предметной области — вместилище модели

МНОГОУРОВНЕВАЯ АРХИТЕКТУРА (LAYERED ARCHITECTURE) используется сейчас в большинстве систем с различными вариациями на тему многоуровневости. Во многих стилях программирования также в той или иной мере пользуются разделением на уровни. А вот в предметно-ориентированном проектировании программ требуется обязательное наличие только одного конкретного уровня.

Модель предметной области (domain model) — это набор понятий (концепций). Уровень предметной области (domain layer) — это выражение данной модели и все элементы программной архитектуры, непосредственно имеющие к ней отношение. Уровень предметной области образуется путем проектирования и реализации всей совокупности понятий и связей в предметной области (business logic). В ПРОЕКТИРОВАНИИ ПО МОДЕЛИ (MODEL-DRIVEN DESIGN) программные конструкции уровня предметной области непосредственно отражают концепции модели.

Столь полного соответствия нет смысла добиваться, если структуру предметной области смешивают с другими функциями программы. Изоляция реализации предметной области — это обязательная предпосылка для применения такой методологии, как предметно-ориентированное проектирование.

“Антишаблон” интеллектуального интерфейса пользователя

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

Во многих программных проектах принимается и должен приниматься в дальнейшем намного менее структурированный подход к проектированию, который я называю ИНТЕЛЛЕКТУАЛЬНЫЙ ИНТЕРФЕЙС ПОЛЬЗОВАТЕЛЯ (SMART UI). Это подход, альтернативный предметно-ориентированному проектированию (DDD) и категорически с ним несовместимый. Если принимается именно он, большую часть написанного в этой книге можно выбросить. Меня интересуют ситуации, в которых ИНТЕЛЛЕКТУАЛЬНЫЙ ИНТЕРФЕЙС ПОЛЬЗОВАТЕЛЯ (SMART UI) неприменим, поэтому я иронично называю его “антишаблоном”. Но обсуждение этого подхода создает полезный контраст и помогает прояснить, как и почему выбирается более трудный путь, которому посвящена вся остальная часть книги.

* * *

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

Если низкоквалифицированная группа разработчиков, работая над простым проектом, решает применить ПРОЕКТИРОВАНИЕ ПО МОДЕЛИ (MODEL-DRIVEN DESIGN) с МНОГОУРОВНЕВОЙ АРХИТЕКТУРОЙ, то ей предстоит трудный процесс обучения. Разработчикам придется осваивать новые сложные технологии, на каждом шагу спотыкаясь при изучении объектного моделирования (а это дело сложное, даже с помощью нашей книги!). Лишние затраты на управление инфраструктурой и уровнями значительно затягивают реализацию простых задач. Простым проектам ставятся сжатые сроки реализации, и многого от них не требуется. Проект могут закрыть значительно раньше, чем разработчики закончат задачу, не говоря уже о том, чтобы продемонстрировать блестящие преимущества своей методологии.

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

Будь квалификация разработчиков повыше, на такие жертвы идти не пришлось бы. Закаленные программисты освоились бы быстрее и сэкономили бы время, необходимое для реализации многоуровневой архитектуры. Предметно-ориентированное проектирование дает наибольший эффект в масштабных проектах, но и требует от участников высокой квалификации. Не все проекты имеют такой размах. И не все группы разработчиков способны собрать нужные силы.

Итак, как действовать, когда этого требуют обстоятельства.

Поместите реализацию прикладной модели (business logic) прямо в интерфейс пользователя. Разбейте приложение на небольшие функции и реализуйте их в виде отдельных пользовательских интерфейсов, помещая правила деловых регламентов (business rules) непосредственно в них. Для хранения данных используйте общую реляционную базу. Пользуйтесь наиболее автоматизированными средствами построения интерфейса и визуального программирования.

Ересь! Истинное учение гласит (повсеместно, в том числе и в остальных частях этой книги), что предметная область и интерфейс должны быть отделены друг от друга! Фактически без такого разделения трудно применить какие бы то ни было методы, рассматриваемые в нашей книге далее. Поэтому ИНТЕЛЛЕКТУАЛЬНЫЙ ИНТЕРФЕЙС ПОЛЬЗОВАТЕЛЯ (SMART UI) можно считать “анти-шаблоном” в контексте предметно-ориентированного проектирования программ. Но в некоторых других контекстах он вполне допустим. По правде говоря, он даже имеет некоторые преимущества, а в некоторых ситуациях просто незаменим — в частности, поэтому он столь широко распространен. Здесь мы рассмотрели его для того, чтобы понять, почему нужно отделять уровень прикладных операций от предметной области, а также, что немаловажно, в каких случаях этого делать не нужно.

Преимущества.
Недостатки.

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

Большинство гибких языков программирования (таких как Java) для подобных проектов слишком изощренны, и их применение дорого обойдется. Следует пользоваться чем-то в стиле 4GL.

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

Аналогично, если разработчики привержены принципам ПРОЕКТИРОВАНИЯ ПО МОДЕЛИ (MODEL-DRIVEN DESIGN), то им следует работать по ним с самого начала. Разумеется, даже опытные коллективы с большими амбициями должны начинать с самых простых функций и двигаться вперед в режиме последовательных приближений. Но все эти приближения, пусть предварительные, будут построены на модели с изолированным уровнем предметной области, иначе проект так и застрянет на стадии ИНТЕЛЛЕКТУАЛЬНОГО ИНТЕРФЕЙСА (SMART UI).

* * *

Шаблон ИНТЕЛЛЕКТУАЛЬНОГО ИНТЕРФЕЙСА (SMART UI) рассматривается здесь только чтобы прояснить, почему и когда для изоляции предметной области необходимо применять такой шаблон, как МНОГОУРОВНЕВАЯ АРХИТЕКТУРА (LAYERED ARCHITECTURE).

Существуют и другие архитектурные решения в промежутке между ИНТЕЛЛЕКТУАЛЬНЫМ ИНТЕРФЕЙСОМ (SMART UI) и МНОГОУРОВНЕВОЙ АРХИТЕКТУРОЙ (LAYERED ARCHITECTURE). Например, в [13] описан ТРАНЗАКЦИОННЫЙ СЦЕНАРИЙ (TRANSACTION SCRIPT), который отделяет интерфейс от уровня прикладных операций, но не предназначен для реализации объектной модели. Так что, подводя итог, надо сказать следующее. Если та или иная архитектура изолирует код предметной области таким образом, что внутренне связную реализацию предметной области можно без труда выделить из остальной части системы, то такая архитектура, вероятно, сможет поддерживать предметно-ориентированное проектирование.

Другие стили программирования тоже имеют свое место под солнцем, и нужно правильно выбирать подход, балансируя на грани сложности и гибкости. Отказ от четкого выделения предметной области при определенных условиях может обернуться катастрофой. Если задача предстоит сложная и принято решение следовать принципам ПРОЕКТИРОВАНИЯ ПО МОДЕЛИ (MODEL-DRIVEN DESIGN), то стисните зубы, пригласите нужных специалистов и избегайте ИНТЕЛЛЕКТУАЛЬНОГО ИНТЕРФЕЙСА (SMART UI).

Другие виды изоляции

К сожалению, повредить хрупкому организму модели предметной области могут не только инфраструктура и интерфейс пользователя. Приходится иметь дело с другими компонентами предметной области, которые не вполне интегрированы в модель. Приходится работать с другими группами разработчиков, которые используют другую модель той же предметной области. Эти и другие факторы могут испортить модель и лишить ее полезности. В главе 14 рассматривается именно этот вопрос; там вводятся такие шаблоны, как ОГРАНИЧЕННЫЙ КОНТЕКСТ (BOUNDED CONTEXT) и ПРЕДОХРАНИТЕЛЬНЫЙ УРОВЕНЬ (ANTICORRUPTION LAYER). Очень сложная модель предметной области сама по себе может стать громоздкой и неповоротливой. В главе 15 рассматривается проблема, как проводить различение и разграничение в пределах уровня предметной модели, чтобы не путать наиболее существенные понятия с второстепенными подробностями.

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

Модель, выраженная в программе

Чтобы суметь найти компромиссы в программной реализации и при этом не потерять преимуществ ПРОЕКТИРОВАНИЯ ПО МОДЕЛИ (MODEL-DRIVEN DESIGN), нужно начинать с пересмотра азов. Связь между моделью и реализацией необходимо прорабатывать на уровне деталей. В этой главе мы сосредоточимся на отдельных элементах модели и придадим им форму, для того чтобы иметь возможность двигаться дальше в следующих главах. Начнем с вопросов проектирования и рационализации ассоциаций. Ассоциации между объектами легко себе представить и нетрудно нарисовать на схеме, но их реализация может завести Бог знает куда. Именно ассоциации показывают, насколько важно в ПРОЕКТИРОВАНИИ ПО МОДЕЛИ принимать детализированные проектные решения. Переходя к самим объектам, но при этом продолжая “препарировать” соотношения между детализированными решениями в модели и вопросами программной реализации, мы сосредоточимся на различиях между тремя шаблонами элементов модели, которые ее выражают: СУЩНОСТЯМИ (ENTITIES), ЗНАЧЕНИЯМИ (VALUE OBJECTS) и СЛУЖБАМИ (SERVICES). Определение объектов, заключающих в себе понятия модели, поначалу кажется интуитивно очевидным, но оттенки смысловых значений могут заставить всерьез задуматься. Существуют определенные нюансы, которые проясняют смысл элементов модели и в совокупности образуют методы проектирования специфических типов объектов. Представляет ли объект собою нечто, имеющее протяженность и обособленность — то, что можно проследить в разных его состояниях или даже разных реализациях? Или же это атрибут, который описывает состояние какого-то другого объекта? В этом и состоит основное различие между СУЩНОСТЬЮ (ENTITY) и ОБЪЕКТОМ-ЗНАЧЕНИЕМ (VALUE OBJECT). Объявление объектов в четком соответствии с одним или другим шаблоном устраняет двусмысленность и многозначность, прокладывая путь к надежным проектным решениям. Есть и такие аспекты предметной области, которые выражаются более удобно в виде действий или операций, а не объектов. Несмотря на некоторый отход от традиций объектно-ориентированного моделирования, такие аспекты лучше выражать в виде СЛУЖБ (SERVICES), а не навязывать ответственность за эти операции сущностям или объектам-значениям. СЛУЖБА (SERVICE) — это нечто, срабатывающее в ответ на запрос клиента. На технических уровнях программного обеспечения существует множество служб. Появляются они и на уровне предметной области при моделировании некоей деятельности, которая соответствует операциям программы, но не ассоциируется ни с каким ее состоянием. Неизбежно возникают ситуации, в которых приходится идти на компромисс и отходить от “чистой” объектной модели — например, для хранения данных в реляционной базе данных. В этой главе будут даны некоторые рекомендации, как не свернуть с правильного пути в обстоятельствах, когда приходится иметь дело с суровой реальностью.

Наконец, в разговоре о МОДУЛЯХ (MODULES) мы поймем, почему каждое проектное решение должно основываться на умозаключении, выведенном из модели. Идеи высокой внутренней связности (high cohesion) и низкой внешней зависимости (low coupling), которые часто относят исключительно к техническим аспектам программирования, можно применять и к самим понятиям модели. В методологии проектирования по модели МОДУЛИ составляют часть модели, поэтому должны отражать понятия предметной области.

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

Ассоциации

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

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

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

Например, ассоциацию “один ко многим” можно реализовать в виде коллекции в переменной-экземпляре. Но вовсе не обязательно делать это так прямолинейно. Коллекции может и не быть; вместо этого метод доступа к атрибуту может запрашивать в базе данных поиск соответствующих записей и создавать экземпляры объектов на их основе. Оба варианта отражают одну и ту же модель. Но в архитектуре программы следует реализовать конкретный механизм, поведение которого в точности соответствует ассоциации в модели.

В реальной жизни существует множество ассоциаций “многие ко многим”, и значительная часть из них — естественно двунаправленного характера. То же самое справедливо и в отношении ранних версий моделей, создаваемых в ходе мозговых штурмов и исследований предметной области. Но слишком общие ассоциации усложняют реализацию и доработку программы. Более того, они несут слишком расплывчатую информацию о природе тех или иных отношений.

Существует как минимум три способа сделать ассоциации более удобными и управляемыми.

  1. Свести отношения к однонаправленным, прослеживаемым в одном направлении.
  2. Добавить квалификаторы, тем самым снижая кратность.
  3. Устранить несущественные ассоциации.

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

В США, как и в других странах, было много президентов. Это двунаправленное отношение “один ко многим”. И все-таки мы редко начинаем с имени “Джордж Вашингтон” и задаем вопрос “Президентом какой страны он был?”. С практической точки зрения эту связь можно считать однонаправленной ассоциацией, прослеживаемой от страны к президенту. Это усовершенствование отражает как лучшее понимание модели, так и практичность архитектуры. В нем заключено утверждение, что одно из направлений ассоциации более важно и значимо, чем другое. При этом класс Человек (Person) оказывается независимым от значительно менее фундаментального понятия Президент (President).


Рисунок 5.1. Естественная асимметрия в предметной области отражается в виде однонаправленной ассоциации

Очень часто углубленное понимание модели порождает квалификатор к той или иной ассоциации. Если глубже задуматься над примером с президентами, то можно сделать вывод, что в стране может быть одновременно не более одного президента (если не считать времена гражданской войны). Этот квалификатор уменьшает кратность до отношения “один к одному” и внедряет это правило в модель явным образом. Теперь вопрос: кто был президентом США в 1790 году? Ответ: Джордж Вашингтон.


Рисунок 5.2. Ограниченные ассоциации передают больше знаний и порождают более практичную архитектуру

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

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

Самым “сильным” упрощением, конечно, будет полное устранение ассоциации, если она несущественна для работы или фундаментального понимания объектов модели.

Пример: ассоциации в модели брокерского счета

Одной из реализаций брокерского счета (Brokerage Account) в этой модели на языке Java будет следующая.

public class BrokerageAccount 
{ 
  String accountNumber; 
  Customer customer; 
  Set investments; 

  // Конструкторы и т.п. опущены

  public Customer getCustomer() 
  { 
    return customer; 
  } 
  public Set getInvestments() 
  { 
    return investments; 
  } 
} 


Рисунок 5.3.

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

Таблица: BROKERAGE_ACCOUNT

ACCOUNT_NUMBER

CUSTOMER_SS_NUMBER

Таблица: CUSTOMER

SS_NUMBER

NAME

Таблица: INVESTMENT

ACCOUNT_NUMBER

STOCK_SYMBOL

AMOUNT

public class BrokerageAccount 
{ 
  String accountNumber; 
  String customerSocialSecurityNumber; 

  // Опущены конструкторы и т.п.
  
  public Customer getCustomer() 
  { 
    String sqlQuery = 
      "SELECT * FROM CUSTOMER WHERE" +
      "SS_NUMBER='"+customerSocialSecurityNumber+"'"; 
    return QueryService.findSingleCustomerFor(sqlQuery); 
  } 
  public Set getInvestments() 
  { 
    String sqlQuery = 
      "SELECT * FROM INVESTMENT WHERE" + 
      "BROKERAGE_ACCOUNT='"+accountNumber+"'"; 
    return QueryService.findInvestmentsFor(sqlQuery); 
  } 
} 

Примечание. Утилита QueryService, используемая для извлечения строк из базы данных и создания объектов, удобна в пояснительных примерах, но не всегда — в реальном проекте.

Давайте усовершенствуем модель, добавив квалификатор к ассоциации между Брокерским счетом (Brokerage Account) и Инвестицией (Investment) с целью уменьшить ее кратность. Квалификатор гласит: разрешается только одна инвестиция на одну акцию.

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

Реализация на Java могла бы выглядеть так.

public class BrokerageAccount 
{ 
  String accountNumber; 
  Customer customer; 
  Map investments; 

  // Опущены конструкторы и т.п. 
  public Customer getCustomer() 
  { 
    return customer; 
  } 
  public Investment getInvestment(String stockSymbol) 
  { 
    return (Investment)investments.get(stockSymbol); 
  } 
} 


Рисунок 5.4.

Реализация с применением SQL будет такой.

public class BrokerageAccount 
{ 
  String accountNumber; 
  String customerSocialSecurityNumber; 

  // Опущены конструкторы и т.п. 
  public Customer getCustomer() 
  { 
    String sqlQuery = 
      "SELECT * FROM CUSTOMER WHERE SS_NUMBER='"
      + customerSocialSecurityNumber + "'"; 
    return QueryService.findSingleCustomerFor(sqlQuery); 
  } 
  public Investment getInvestment(String stockSymbol) 
  { 
    String sqlQuery = "SELECT * FROM INVESTMENT " 
      + "WHERE BROKERAGE_ACCOUNT='" + accountNumber + "'" 
      + "AND STOCK_SYMBOL='" + stockSymbol +"'"; 
    return QueryService.findInvestmentFor(sqlQuery); 
  } 
} 

Тщательная дистилляция и накладывание ограничений на ассоциации модели — это большой шаг к ПРОЕКТИРОВАНИЮ ПО МОДЕЛИ (MODEL-DRIVEN DESIGN). Теперь обратимся к самим объектам. Существуют нюансы, которые делают модель более понятной и одновременно ведут к более практичной реализации.

Сущности (указуемые объекты)

Многие объекты не сводятся к набору атрибутов, а характеризуются непрерывностью и индивидуальностью существования.

ПРИМЕЧАНИЕ

В заголовке употребляются термины entity (сущность, самостоятельная логическая единица) и reference object (называемый, обозначаемый, указуемый объект). — Примеч. перев.

Автор употребляет здесь слово identity, которое означает и набор идентификационных данных типа имени, фамилии, адреса, и индивидуальное, уникальное существование некоего объекта или субъекта. Это дает ему право пояснять свои мысли на приведенном далее примере. В русском языке приходится использовать целый комплекс терминов: индивидуальность, индивидуальное существование, идентичность, идентификация и т.п. — Примеч. перев.

* * *

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

Но почти сразу же я догадался, что меня, должно быть, просто приняли за другого человека. Я позвонил истице и объяснил ей это, но она не поверила. Бывший жилец несколько месяцев скрывался от нее. Как я мог доказать, что я не тот человек, из-за которого она понесла такие убытки? Ведь я был единственным Эриком Эвансом в телефонном справочнике.

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

Но компьютеры такой изобретательностью не отличаются. Подобная путаница в программной системе чревата повреждением данных и ошибками выполнения.

Здесь имеются некоторые проблемы технического характера, о которых будет сказано позже, а пока рассмотрим фундаментальную теоретическую проблему: многие вещи определяются не частными атрибутами, а индивидуальным, неповторимым существованием. В нашем обычном понимании человек (если продолжить наш нетехнический пример) имеет индивидуальное существование, которое продолжается от рождения до смерти и даже после нее. Физические атрибуты человека изменяются, а со временем и вовсе исчезают. Может измениться его имя. Завязываются и обрываются финансовые взаимоотношения. У человеческой личности нет ни единого атрибута, который бы не мог измениться, а вот его индивидуальность сохраняется. Тот ли я человек, которым был в пятилетнем возрасте? Подобный метафизический вопрос немаловажен при поиске эффективных моделей предметных областей. Если слегка перефразировать: имеет ли для пользователя программы значение, был или не был я тем же человеком в свои пять лет?

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

Каждая из этих форм объекта-клиента реализуется по-разному, с применением разных языков и технологий программирования. Но когда нужно поднять трубку и предъявить денежную претензию, необходимо знать: тот ли это клиент, на счету которого зафиксированы нарушения? Тот ли это клиент, о котором Джек (конкретный агент) собирает информацию уже несколько недель? Или же это совершенно новый клиент?

Индивидуального субъекта приходится опознавать, сравнивая различные реализации объектов, их постоянно хранимые формы, а также внешних агентов наподобие телефонных абонентов. Атрибуты при этом могут различаться. Агент мог ввести новый адрес в программе управления контактами, которая только начинает работать с должниками. У двух клиентов может быть одно и то же имя. В распределенных программных системах данные могут вводиться несколькими пользователями из разных источников, из-за чего транзакции обновления “ходят” по всей системе и согласуются в разных базах данных асинхронно.

В объектном моделировании главное внимание обычно фокусируется на атрибутах объекта, но для фундаментального понятия СУЩНОСТИ главное — не атрибуты, а абстрактное непрерывное существование в течение всего жизненного цикла, даже с переходом в различные формы.

Для некоторых объектов определение через атрибуты не является главным. Они представляют собой индивидуально существующие логические единицы (сущности), протяженные во времени и часто проходящие через несколько различных представлений. Иногда такой объект приходится ставить в соответствие другому объекту, хотя их атрибуты отличаются. Или же объект требуется отличать от другого объекта, хотя их атрибуты могут быть одинаковыми. Путаница в объектах может привести к искажению данных.

Логически целостный объект, определяемый совокупностью индивидуальных черт, называется СУЩНОСТЬЮ (ENTITY). Для таких объектов существуют особые принципы моделирования и проектирования. На протяжении их цикла существования у них может радикально меняться и форма, и содержание, но непрерывность этого существования обязана поддерживаться. Они должны идентифицироваться таким образом, чтобы их можно было однозначно отследить. Определения классов, обязанностей, атрибутов и ассоциаций для таких объектов следует строить вокруг их смыслового значения, а не присвоенных им атрибутов. Но даже если некоторые объекты-СУЩНОСТИ не подвергаются таким радикальным изменениям или не имеют такого сложного цикла существования, их все равно полезно отнести к этой семантической категории, потому что такие модели будут более ясными, а их реализации — более надежными.

ПРИМЕЧАНИЕ

Модель объекта ENTITY — это совсем не то, что объект entity bean в Java. Последний задумывался как основа для реализации объектов-единиц, но так и не стал таковой. Большинство сущностей entity реализуются в виде обыкновенных объектов. Но независимо от способа реализации, такие сущности являются принципиально важными в модели предметной области.

Большинство логических “сущностей” в программной системе не представляют собой физические или юридические лица. СУЩНОСТЬ — это все то, что сохраняет свое индивидуальное существование и отличие на протяжении срока “жизни”, независимо от атрибутов, важных для пользователя приложения. Это может быть человек, город, автомобиль, лотерейный билет или банковская транзакция.

ПРИМЕЧАНИЕ

Это пояснение автора стало необходимым только потому, что одно из значений слова entity в английском языке — предприятие, фирма, юридическое лицо. — Примеч. перев.

С другой стороны, отчетливо индивидуальным существованием бывают наделены далеко не все объекты модели. Этот важный момент “затуманивается” тем фактом, что в объектно-ориентированных языках операции проверки идентичности (например, “==” в Java) автоматически встраиваются в каждый объект. Эти операции определяют, указывают ли две ссылки на один и тот же объект, сравнивая их положение в памяти или используя какой-нибудь другой механизм. В этом смысле любой экземпляр объекта является индивидуальной логической единицей, сущностью. Если предметная область — это, скажем, создание исполняемой среды Java или технических средств для поддержки локального кэширования удаленных объектов, то каждый экземпляр объектов в них действительно будет СУЩНОСТЬЮ. Но в других прикладных областях такой механизм индивидуальной идентификации значит весьма мало. Индивидуальное существование — это тонкий смысловой атрибут СУЩНОСТЕЙ, который невозможно превратить в стандартное автоматизированное средство языка.

Рассмотрим транзакции в банковских операциях. Два депозита на одну и ту же сумму в один и тот же день являются различными транзакциями, у них есть индивидуальность, и поэтому они — СУЩНОСТИ. С другой стороны, атрибуты “сумма” в этих двух транзакциях, скорее всего, представляют собой экземпляры некоего объекта “деньги”. У этих значений никакого индивидуального существования нет, потому что различать их никому не надо. Фактически два объекта могут не иметь индивидуальных отличий друг от друга, даже если у них нет одинаковых атрибутов; более того, они могут даже не принадлежать одному классу. Когда клиент банка сверяет транзакции из банковского баланса с расчетными операциями, зафиксированными в его собственных учетных документах, перед ним стоит задача найти одинаковые транзакции, пусть даже они фиксировались разными людьми в разные дни (конечно, при условии, что баланс подсчитан после всех взаимных расчетов). Уникальным идентификатором для этой цели призван служить номер чека — неважно, выполняется обработка данных вручную или программно. Снятие наличных и помещение средств на депозит при отсутствии такого идентификационного номера представляют большие трудности, но принцип здесь тот же: каждая транзакция — это СУЩНОСТЬ, проявляющаяся как минимум в двух формах.

Индивидуальность объекта-СУЩНОСТИ, как правило, является весомым фактором и за пределами компьютерных систем, — например, в приведенных примерах банковских транзакций и квартиросъемщиков. Но иногда индивидуальное существование возможно только в контексте программной системы, — например, в виде вычислительного процесса.

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

Индивидуальность, индивидуальное существование — это не имманентное свойство вещей в природе, а качество, всего лишь приписываемое им из-за его полезности. На самом деле одна и та же реально существующая в природе вещь может как представляться, так и не представляться объектом-СУЩНОСТЬЮ в модели предметной области.

Например, в программе резервирования мест на стадионе как зрители, так и места могут считаться СУЩНОСТЯМИ. В случае билетов с местами, где на каждом билете напечатан номер строго определенного места, это место действительно является сущностью. Идентификатор этой сущности — номер места, и в рамках данного стадиона он уникален. Место может иметь много других атрибутов, таких как точное местоположение, качество вида на игровое поле, цену, но только номер места (или пара “ряд-место”) может использоваться для точного определения и различения мест.

С другой стороны, если билеты продаются без места, т.е. купившие их зрители садятся на любые еще не занятые места, различать отдельные места нет никакой необходимости. Значение имеет только общее количество мест. Хотя номера мест по-прежнему физически нанесены на сиденья, программа не обязана за ними следить. Вообще-то, в этом случае ассоциировать номера мест с билетами в модели было бы прямой ошибкой, потому что при продаже билетов “без мест” такое требование отсутствует. Здесь места не являются СУЩНОСТЯМИ, и идентификатор для них не нужен.

* * *

Моделирование СУЩНОСТЕЙ

При моделировании объекта совершенно естественно думать о его атрибутах, и очень важно думать о его поведении, рабочих функциях. Но основная функция СУЩНОСТИ — поддерживать непрерывность своего существования таким образом, чтобы ее поведение было понятным и предсказуемым. Лучший способ добиться этого — умеренность и экономия. Вместо того чтобы сосредоточиться на атрибутах или даже на рабочих функциях объекта, следует ограничить определение объекта-СУЩНОСТИ наиболее неотъемлемыми его характеристиками, т.е. теми, которые однозначно выделяют его среди других и обычно используются для его поиска и сравнения. Задавайте только те рабочие функции, которые существенны для создания понятия об объекте, и только те атрибуты, которых требуют эти функции. Все атрибуты и функции, которые выходят за эти рамки, старайтесь выносить в другие объекты, ассоциированные с главной СУЩНОСТЬЮ. Некоторые из них тоже будут СУЩНОСТЯМИ, а некоторые — ОБЪЕКТАМИ-ЗНАЧЕНИЯМИ (VALUE OBJECTS); это следующий шаблон, который мы рассматриваем в этой главе. Кроме индивидуальности и непрерывности существования, для СУЩНОСТЕЙ часто характерно то, что они выполняют свои функции, координируя операции объектов, которые им принадлежат.


Рисунок 5.5. Атрибуты, неотъемлемые от индивидуально существующего объекта, приписываются СУЩНОСТИ

Атрибут customerID — это единственный реальный идентификатор объекта Клиент (Customer) на рис. 5.5, но для поиска или сопоставления личности Клиента может также использоваться номер телефона или адрес. Имя не определяет индивидуальность человека, но входит в число средств, позволяющих проверить ее подлинность. В данном примере атрибуты номера телефона и адреса помещены в объект Клиент, но в реальном проекте для такого решения были бы приняты во внимание типичные способы различения или сопоставления клиентов. Например, если у Клиента есть много номеров телефонов для разных целей, то номер нельзя ассоциировать с индивидуальной личностью клиента, и его следует вынести в Деловые контакты (Sales Contact).

Проектирование операций идентификации

Для каждого объекта-СУЩНОСТИ должен быть задан способ сопоставления его с другим объектом — такой, который бы позволял различать их даже в случае совпадения описательных атрибутов. Идентифицирующий атрибут должен гарантированно оставаться уникальным в пределах системы, независимо от способа ее определения — даже в распределенной системе, даже в архиве.

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

Даже если у нас есть среда программирования, упрощающая решение технических задач, остается принципиальный вопрос: как узнать, представляют ли два объекта одну и ту же концептуальную СУЩНОСТЬ? Определение идентичности возникает из модели. Чтобы построить такое определение, нужно понимать предметную область.

Иногда некоторые атрибуты данных или комбинации атрибутов гарантированно являются уникальными в пределах системы, — например, в силу наложенных на них требований. При таком подходе у СУЩНОСТИ есть неповторимый ключ, который ее однозначно идентифицирует. Например, ежедневную газету можно идентифицировать по названию, городу и дате выпуска. (Если не считать внеочередных выпусков и возможных изменений названия.)

Если из атрибутов объекта нельзя составить действительно уникальный ключ, то есть другое типовое решение — каждому экземпляру присвоить символ-идентификатор (число или текстовую строку), не повторяющийся в пределах класса. После создания идентификатора и помещения его в атрибут СУЩНОСТИ его следует сделать неизменяемым. Он никогда не должен изменяться, даже если среда программирования не может напрямую принудить программу к выполнению этого требования. Например, такой идентификатор сохраняется, когда объект упаковывают в базу данных, а затем извлекают оттуда. Иногда технические средства программирования могут помочь в этом, но в других случаях приходится полагаться на дисциплинированность разработчика.

Идентификаторы часто генерируются системой автоматически. Алгоритм генерирования должен гарантировать уникальность в пределах системы, что для параллельных вычислений и распределенных систем может оказаться непростой задачей. Генерирование таких идентификаторов может потребовать приемов и технологий, далеко выходящих за пределы тематики нашей книги. Здесь хотелось бы только указать, когда именно возникает необходимость рассмотрения таких вопросов, чтобы разработчики были в курсе существования проблемы и могли сузить поиск решения до самой критической области. Главное здесь — осознать, что вопросы идентификации связаны со специфическими аспектами самой модели. Создание средств идентификации часто требует тщательного изучения предметной области.

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

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

В некоторых случаях уникальность идентификатора не должна ограничиваться пределами компьютерной системы. Например, если между двумя больницами, имеющими отдельные компьютерные системы управления, идет обмен медицинскими картами, то в идеале каждая система должна была бы обозначать пациента одним и тем же идентификационным кодом. Но это сложно сделать, если они сами генерируют свои идентификаторы. В таких системах часто используются идентификаторы, выпущенные какой-нибудь другой организацией — как правило, государственной. В США в качестве идентификаторов для пациентов больниц часто применяются номера социального страхования (Social Security). Впрочем, такие методы не дают полной гарантии. Например, не у всех есть номера социального страхования (в частности, их нет у детей и у иностранных граждан), и многие граждане протестуют против использования этих номеров по соображениям сохранения личной тайны.

В менее формальных ситуациях (например, в видеопрокате) идентификаторами могут служить номера телефонов. Но один телефон может использоваться несколькими людьми. К тому же номер может измениться или его могут даже передать другому абоненту.

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

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

Объектызначения

У многих объектов в принципе нет индивидуального существования. Они описывают характеристики того или иного предмета.

* * *

Когда ребенок рисует, его интересует цвет карандаша или фломастера, а также, возможно, острота его грифеля. Но если есть два карандаша одинакового цвета и формы, ему, вероятно, будет все равно, каким рисовать. Если карандаш потерялся, и его пришлось заменить таким же из новой коробки, малыш может продолжить работу, нисколько не отвлекаясь на сам факт замены.

Спросите ребенка о рисунках на холодильнике, и он быстро отличит, какие рисовал он сам, а какие — его сестра. Как он, так и сестра обладают индивидуальным существованием. Это же свойство характерно и для их рисунков. Но представьте себе, сколько бы потребовалось труда, если бы пришлось отслеживать, какие линии и каким карандашом сделаны. Рисование сразу перестало бы быть детской игрой.

Самые важные объекты в модели обычно представляют собой СУЩНОСТИ. Каждую такую сущность очень важно правильно идентифицировать и отслеживать. Естественным образом возникает желание сделать все объекты области индивидуальными. Действительно, в некоторых средах программирования каждому объекту присваивается уникальный идентификатор.

Системе приходится справляться с отслеживанием всех этих индивидуальных объектов, и это часто мешает применить различные способы оптимизации быстродействия. Требуется немалая аналитическая работа, чтобы грамотно определить способы идентификации и выработать надежные методы отслеживания объектов в распределенных системах или при хранении в базах данных. Искусственно же сконструированная идентификация только вводит в заблуждение, портит модель, перемешивает все объекты в одну хаотическую “кашу”.

Является ли адрес объектом-значением?

Смотря для кого. В программе для фирмы, торгующей по почте, адрес необходим для подтверждения кредитной карточки и отправки посылки. Но если там же заказывает товар сосед по квартире, фирме совершенно безразлично, что он находится по тому же адресу. Здесь адрес — это ОБЪЕКТ-ЗНАЧЕНИЕ.

В программе обслуживания почтовых отправлений, предназначенной для организации маршрутов доставки почты, страна может быть представлена в виде иерархии областей, городов, индексных зон и жилищных массивов, которая заканчивается отдельными адресами. Такие адресные объекты наследуют свой почтовый индекс от родительского объекта в иерархии, и если почтовое управление решит изменить индексные зоны, все адреса в их пределах “подстроятся” под это изменение. Здесь адрес является СУЩНОСТЬЮ.

В программе для коммунальной электрической компании адрес соответствует месту, к которому подведены кабельные линии и где оказываются услуги. Если два соседа по квартире позвонят и по отдельности закажут какие-то работы, фирме нужно знать, что они находятся в одном месте. Здесь адрес — также СУЩНОСТЬ. Но в модели коммунальные услуги могут ассоциироваться с “жилищем”, т.е. объектом-сущностью, у которого есть атрибут “адрес”. Тогда “адрес” будет ОБЪЕКТОМ-ЗНАЧЕНИЕМ.

Идентификация СУЩНОСТЕЙ — дело нужное, но выделение других видов объектов как индивидуально существующих может повредить быстродействию системы, добавить лишнюю аналитическую работу, ухудшить модель из-за того, что все объекты в ней приобретут одинаковый характер.

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

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

ОБЪЕКТОМ-ЗНАЧЕНИЕМ (VALUE OBJECT) называется объект, который представляет описательный аспект предметной области и не имеет индивидуального существования, собственной идентичности. Такие объекты создаются в программе для представления тех элементов проекта, о которых достаточно знать только, что они собой представляют, но не кем именно они являются.

В базовых библиотеках многих современных систем разработки программ представлены такие ОБЪЕКТЫ-ЗНАЧЕНИЯ, как цвета, а также строки и числа. (Кому нужно различать отдельные четверки или буквы Q?) Это совсем простые примеры, но реальные объекты-показатели не обязательно бывают такими простыми. Например, программа анализа и генерирования цветов может иметь в своей основе сложную модель, в которой объекты-цвета можно смешивать для получения новых цветов. Для получения нового объекта-значения таким образом могут применяться сложные алгоритмы.

ОБЪЕКТ-ЗНАЧЕНИЕ может представлять собой совокупность других объектов. В программах архитектурного проектирования жилых домов для любого стиля окон можно создать отдельный объект. Этот “стиль окна” можно инкорпорировать в объект “окно” наряду с атрибутами ширины и высоты, а также правилами изменения и комбинирования. Такие окна — сложные ОБЪЕКТЫ-ЗНАЧЕНИЯ, составленные из других ОБЪЕКТОВ-ЗНАЧЕНИЙ. Они, в свою очередь, могут инкорпорироваться в еще большие составные элементы плана здания — например, объекты“стены”.

ОБЪЕКТЫ-ЗНАЧЕНИЯ могут даже ссылаться на СУЩНОСТИ. Например, если я запрошу картографическую онлайн-службу проложить мне живописный маршрут из Сан-Франциско в Лос-Анджелес, то она может сгенерировать объект Маршрут (Route), соединяющий эти два города по шоссе Pacific Coast Highway. Этот объект будет ЗНАЧЕНИЕМ, пусть даже все три объекта, на которые он ссылается (два города и шоссе), являются СУЩНОСТЯМИ.

ОБЪЕКТЫ-ЗНАЧЕНИЯ часто передаются в качестве параметров в сообщениях между объектами. Нередко они носят временный характер — создаются для конкретной операции и тут же уничтожаются. ОБЪЕКТЫ-ЗНАЧЕНИЯ могут использоваться в виде атрибутов СУЩНОСТЕЙ (и других ЗНАЧЕНИЙ). Так, человек, в целом, может представляться индивидуальной СУЩНОСТЬЮ, но его имя будет ОБЪЕКТОМ-ЗНАЧЕНИЕМ.

Если элемент модели полностью определяется своими атрибутами, то его следует считать ОБЪЕКТОМ-ЗНАЧЕНИЕМ. Сделайте так, чтобы он отражал смысл заложенных в него атрибутов, и придайте ему соответствующую функциональность. Считайте такой объект неизменяющимся. Не давайте ему индивидуальности, вообще избегайте любых сложностей, неизбежных при программном управлении СУЩНОСТЯМИ.

Атрибуты, образующие в совокупности ОБЪЕКТ-ЗНАЧЕНИЕ, должны быть единым концептуальным целым. Например, улица, город и почтовый индекс не должны быть отдельными атрибутами объекта Человек (Person). Они входят как составные части в адрес, что делает проще устройство объекта Человек и больше соответствует концепции ОБЪЕКТА-ЗНАЧЕНИЯ.

ПРИМЕЧАНИЕ

Шаблон WHOLE VALUE (целостный объект-значение), автор Уорд Каннингем (Ward Cunningham).

* * *
Проектирование ОБЪЕКТОВ-ЗНАЧЕНИЙ

Нам все равно, какой именно экземпляр ОБЪЕКТА-ЗНАЧЕНИЯ (VALUE OBJECT) у нас имеется. Такая вольность дает проектировщику достаточно места для маневров по упрощению архитектуры и оптимизации быстродействия. При этом приходится принимать решения по части копирования, совместного использования и неизменяемости.

Если двое людей имеют одно и то же имя, они от этого не становятся одним и тем же человеком; не становятся они и взаимозаменяемыми. Но вот объект, представляющий имя, — вполне заменяем, лишь бы имя было написано правильно. Объект Имя (Name) можно смело копировать из первого объекта Человек (Person) во второй.

В принципе два объекта Человек могут и не нуждаться в собственных экземплярах объекта-имени. Один и тот же объект Имя может совместно использоваться двумя объектами Человек (в каждом будет содержаться указатель на один и тот же экземпляр Имени), и при этом никаких изменений в их поведении или способе идентификации не потребуется. То есть, они будут работать правильно, пока у одного человека не изменится имя. Тогда так же изменится имя и другого человека! Чтобы этого избежать и сделать совместное использование объекта безопасным, его нужно сделать неизменяемым — запрещенным для любых изменений иначе как путем полной замены.


Рисунок 5. 6. Объект-значение предоставляет информацию об объекте-сущности. Он должен быть единым концептуальным целым.

Та же проблема возникает, когда объект передает один из своих атрибутов другому объекту в качестве аргумента или возвращаемого значения. Со странствующим объектом может случиться все, что угодно, за то время, пока он не находится под контролем владельца. ЗНАЧЕНИЕ (VALUE) может измениться таким образом, что будет поврежден и объект-владелец путем искажения его инвариантов. Чтобы избежать этого, передаваемый объект делают неизменяемым или же передают его копию.

Бывает полезно создать несколько потенциальных возможностей для оптимизации быстродействия, так как ОБЪЕКТЫ-ЗНАЧЕНИЯ имеют тенденцию множиться. Примером может служить программа для проектирования жилого дома. Если каждый электрический вывод является отдельным ОБЪЕКТОМ-ЗНАЧЕНИЕМ, то всего в одной версии одного проекта дома их может быть более сотни. Но если все выводы посчитать взаимозаменяемыми, то можно совместно использовать всего один объект, указывая на него сотню раз (это пример шаблона FLYWEIGHT, мелкий объект [14]). В больших системах объектам подобного рода идет счет на тысячи, а оптимизация сразу показывает разницу между нормальной работоспособной системой и еле ползающим бесполезным монстром, задыхающимся от миллионов лишних объектов. И это всего лишь один пример такой оптимизации, которая недоступна объектам-СУЩНОСТЯМ.

Сравнительная экономичность копирования и совместного использования зависит от среды реализации. Копирование может “затопить” систему огромным количеством объектов, зато совместное использование способно замедлить работу распределенной сетевой системы. Если между двумя рабочими станциями передается копия объекта, то посылается одно-единственное сообщение, и копия затем существует независимо от оригинала. Но если совместно используется один и тот же экземпляр объекта, то передается только ссылка на него, отчего при каждом обращении необходимо пересылать сообщение объекту.

Совместное использование объектов стоит ограничить теми случаями, когда оно приносит наибольшую пользу и доставляет меньше всего неприятностей:

Особые случаи: когда разрешать изменяемость

Неизменяемость объектов сильно упрощает программную реализацию, гарантируя безопасность совместного использования и передачи ссылок. Она также следует в русле идеи объектов как значений. Если изменяется значение некоторого атрибута, то вместо модификации существующего ОБЪЕКТА-ЗНАЧЕНИЯ просто используется новый. Но бывают случаи, когда из соображений быстродействия лучше разрешить вносить изменения в ОБЪЕКТЫ-ЗНАЧЕНИЯ. В пользу этого обычно говорят следующие факторы:

Нелишне будет еще раз напомнить: если реализация ОБЪЕКТА-ЗНАЧЕНИЯ предполагается изменяемой, такой объект не должен совместно использоваться с другим объектом. Но независимо от того, будут они совместно использоваться или нет, при любой возможности не изменяйте ОБЪЕКТЫ-ЗНАЧЕНИЯ.

Атрибут или объект в некоторых языках и средах программирования можно объявить неизменяемым, а в других — нет. Если такая возможность есть, это помогает выразить принятое проектное решение в явной форме, но если ее нет — ничего страшного. Многие особенности, присутствующие в модели, невозможно определить явно в программной реализации с помощью существующих средств и языков программирования. Например, нельзя так объявить объект-СУЩНОСТЬ, чтобы ему при этом автоматически назначалась операция проверки идентичности. Но если для концептуальной особенности модели не существует прямой поддержки в языке, это еще не значит, что сама особенность бесполезна. Если существуют только неявные способы для ее реализации, это всего лишь потребует от разработчика большей “стилистической дисциплины”. Здесь должны пойти в ход соглашения об именах, тщательность подготовки документации и постоянное обсуждение проблемы.

Если ОБЪЕКТ-ЗНАЧЕНИЕ объявлен неизменяемым, то управлять его изменениями очень просто. Единственный способ изменения — полная замена. Неизменяемые объекты можно предоставлять в совместный доступ, как в примере с электрическим выводом. Если “сборка мусора” (garbage collection) работает надежно, то удаление — всего лишь вопрос сброса всех ссылок на объект. Если в архитектуре программы ОБЪЕКТ-ЗНАЧЕНИЕ сделан неизменяемым, то разработчики вольны принимать любые решения по таким вопросам, как копирование и совместный доступ, из чисто технических соображений — они твердо знают, что работа программы не зависит критически от конкретного экземпляра такого объекта.

Определение ОБЪЕКТОВ-ЗНАЧЕНИЙ и объявление их неизменяемыми — это частный случай общего правила, которое состоит в следующем. Чтобы иметь возможность технически оптимизировать быстродействие, разработчики должны избегать лишних связей-ограничений в модели. А если явно определить все существенные ограничения, это дает возможность оптимизировать архитектуру, не боясь нарушить работу важнейших алгоритмических частей программы. Такая оптимизация архитектуры часто зависит от конкретной технологии, используемой в проекте.

Пример

Оптимизация базы данных с помощью объектов-значений

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

Если на объект ссылается много других объектов, то не все из них обязательно располагаются близко от него (на той же странице), и поэтому для получения данных требуется дополнительная физическая операция. Но если экземпляр ОБЪЕКТА-ЗНАЧЕНИЯ, который служит атрибутом нескольких СУЩНОСТЕЙ, физически скопировать, а не просто поставить ссылку на него, то такой объект можно будет разместить на той же странице, что и любую из использующих его СУЩНОСТЕЙ. Этот прием размножения копий одних и тех же данных называется денормализацией и часто используется тогда, когда время обращения к данным важнее, чем экономия дискового пространства или удобство обслуживания базы.

В реляционной базе данных можно поместить ОБЪЕКТ-ЗНАЧЕНИЕ в таблицу объекта-СУЩНОСТИ, который им владеет, а не создавать ассоциативную связь с отдельной таблицей. Если в распределенной системе хранить ссылку на ОБЪЕКТ-ЗНАЧЕНИЕ на другом сервере, от этого наверняка замедлится передача сообщений. Поэтому лучше передать на другой сервер полную копию объекта. Таких копий можно сделать сколько угодно, поскольку мы имеем дело с ОБЪЕКТАМИ-ЗНАЧЕНИЯМИ.

Проектирование ассоциаций с помощью ОБЪЕКТОВ-ЗНАЧЕНИЙ

Многое, что было раньше сказано об ассоциациях, относится как к СУЩНОСТЯМ (ENTITIES), так и к ОБЪЕКТАМ-ЗНАЧЕНИЯМ (VALUE OBJECTS). Чем меньше в модели ассоциаций и чем они проще, тем лучше.

В то время как двунаправленные ассоциации между СУЩНОСТЯМИ бывает трудно поддерживать, двунаправленные ассоциации между ОБЪЕКТАМИ-ЗНАЧЕНИЯМИ вообще не имеют смысла. При отсутствии индивидуальности у объектов бессмысленно говорить, что тот или иной объект ссылается на тот же ОБЪЕКТ-ЗНАЧЕНИЕ, который ссылается на него. Можно разве что сказать так: первый объект ссылается на другой, эквивалентный тому, который сам ссылается на первый. Но и этот инвариант надо как-то принудительно задать в другом месте. Хотя можно было бы теоретически сделать указатели в обе стороны, трудно придумать примеры, в которых этот прием был бы уместен. Поэтому старайтесь избегать двунаправленных ассоциаций между ОБЪЕКТАМИ-ЗНАЧЕНИЯМИ. Если же такие ассоциации кажутся необходимыми в модели, пересмотрите само решение сделать эти объекты значениями. Может быть, у них есть своеобразная индивидуальность, которую вы до сих пор не заметили.

СУЩНОСТИ (ENTITIES) и ОБЪЕКТЫ-ЗНАЧЕНИЯ (VALUE OBJECTS) — это главные элементы самых распространенных объектных моделей, но проектировщики программ на своем опыте пришли к выводу о необходимости еще и третьего вида элементов — СЛУЖБ (SERVICES).

Службы

Не все на свете сводится к вещам и предметам.

В некоторых случаях самые четкие и практичные программные архитектуры содержат операции, которые по своей сути не принадлежат ни одному конкретному объекту. Чем выкручиваться из этого положения искусственными методами, лучше последовать естественному ходу событий и включить в модель так называемые СЛУЖБЫ (SERVICES).

* * *

В прикладной предметной области бывают такие операции, которым нельзя найти естественное место в объекте типа СУЩНОСТИ (ENTITY) или ЗНАЧЕНИЯ (VALUE OBJECT). Они по своей сути являются не предметами, а видами деятельности. Но поскольку в основе нашей парадигмы моделирования лежит объектный подход, мы попробуем превратить их в объекты.

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

Часто службы выступают под личиной объектов модели, не наполненных никаким особым смыслом, кроме выполнения определенной операции. Эти “исполнители” в конце концов получают имена наподобие “диспетчер (manager) чего-нибудь”. У них нет ни собственного характерного состояния, ни другой роли в предметной области, кроме реализации конкретной операции. Но, по крайней мере, при таком решении характерные действия, выполняемые в модели, куда-то помещены и не загромождают настоящие, смысловые объекты модели.

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

СЛУЖБА (SERVICE) — это операция, предлагаемая в модели в виде обособленного интерфейса, который, в отличие от СУЩНОСТЕЙ и ОБЪЕКТОВ-ЗНАЧЕНИЙ, не инкапсулирует никакого состояния. СЛУЖБА — это обычный шаблон в технических средах программирования, но службы применимы и на уровне предметной области.

Само название служба подчеркивает положение, подчиненное нуждам других объектов. В отличие от СУЩНОСТЕЙ и ОБЪЕКТОВ-ЗНАЧЕНИЙ, она определяется только тем, что умеет делать для клиента. Соответственно, и имя СЛУЖБЕ обычно дают по ее функции, а не сути — это глагол, а не существительное. Определение у нее, тем не менее, может быть достаточно абстрактным, но с другим оттенком по сравнению с объектом. На СЛУЖБУ должны возлагаться конкретные обязанности, и они вместе с интерфейсом должны определяться в составе модели. Имена операций следует взять из ЕДИНОГО ЯЗЫКА (UBIQUITOUS LANGUAGE) или ввести в него. Параметрами и результатами работы СЛУЖБЫ должны быть объекты модели.

Не стоит, однако, и злоупотреблять СЛУЖБАМИ, лишая СУЩНОСТИ и ОБЪЕКТЫ-ЗНАЧЕНИЯ всякой функциональной нагрузки. В то время как операция — это важное понятие предметной области, СЛУЖБА является естественным элементом в рамках ПРОЕКТИРОВАНИЯ ПО МОДЕЛИ. Если операция объявлена в модели СЛУЖБОЙ, а не “липовым” объектом, ничего реального не представляющим, то она никого не введет в заблуждение.

Хорошая СЛУЖБА обладает тремя свойствами.

  1. Выполняемая ею операция соответствует понятию модели, не являющемуся естественной частью объекта-сущности или объекта-значения.
  2. Интерфейс службы определен через другие элементы модели предметной области.
  3. Операция не имеет собственного состояния.

Последний пункт — отсутствие состояния — означает, что любой клиент может воспользоваться любым экземпляром нужной ему СЛУЖБЫ, не обращая внимания на индивидуальную предысторию ее работы. При работе СЛУЖБЫ может использоваться (и даже подвергаться изменениям) информация, доступная глобально, поэтому возможны побочные эффекты. Но СЛУЖБА не хранит данных о своем собственном состоянии, которое влияло бы на ее работу, в отличие от большинства объектов модели.

Если существенно важный процесс или преобразование в модели не относится к естественным обязанностям объекта-сущности или значения, добавьте в модель эту операцию с отдельным интерфейсом и назовите ее СЛУЖБОЙ. Определите интерфейс на языке модели и сделайте имя операции элементом ЕДИНОГО ЯЗЫКА. У СЛУЖБЫ не должно быть собственного состояния.

Службы и изоляция уровня предметной области

В основе этого шаблона лежат такие СЛУЖБЫ, которые сами по себе много значат для предметной области. Но, конечно, их применение не ограничивается одним этим уровнем. Не так уж легко отличить СЛУЖБЫ, принадлежащие к уровню предметной области, от СЛУЖБ других уровней, и соответственно разделить обязанности, чтобы четко обозначить это различие.

Большинство описанных в литературе СЛУЖБ имеет чисто технический характер и принадлежит инфраструктурному уровню. СЛУЖБЫ уровней модели и прикладных операций взаимодействуют с инфраструктурными СЛУЖБАМИ. Например, в банке может работать программа, которая отсылает клиенту письмо по электронной почте всякий раз, когда баланс на его счету падает ниже определенного порога. Интерфейс, инкапсулирующий почтовую систему, а с нею, возможно, и другие способы извещения клиента, образует СЛУЖБУ на инфраструктурном уровне.

А вот отличить СЛУЖБЫ операционного уровня от уровня предметной области (модели) бывает труднее. Операционный уровень отвечает за то, чтобы отдать приказ об извещении клиента. А уровень модели определяет, достигнут ли порог — правда, эта задача не требует отдельной СЛУЖБЫ, а скорее, входит в полномочия объекта “банковский счет”. Та же самая банковская программа, возможно, занимается и переводом денежных средств. Если сконструировать СЛУЖБУ, отвечающую за подведение дебета и кредита в процессе перевода, то она будет принадлежать уровню предметной области. Перевод денег — это смысловой элемент банковского дела, а его реализация — это фундаментальная операция предметной области. В технических же службах никакие понятия прикладной модели вообще не должны упоминаться.

Многие СЛУЖБЫ уровней модели и прикладных операций строятся на основе совокупностей ОБЪЕКТОВ-СУЩНОСТЕЙ и ОБЪЕКТОВ-ЗНАЧЕНИЙ. Они ведут себя как сценарии, которые мобилизуют потенциал предметной области на выполнение чего-то полезного. Сами СУЩНОСТИ и ЗНАЧЕНИЯ часто бывают слишком мелкомасштабными, чтобы дать пользователю удобный доступ к возможностям уровня предметной области. Можно заметить, что грань между уровнями предметной области и прикладных операций очень тонка. Например, если банковская программа умеет преобразовывать и экспортировать все наши финансовые транзакции в файл электронной таблицы (чтобы мы могли его читать и анализировать), то СЛУЖБА такого экспорта относится к уровню прикладных операций. В предметной области банковского дела никаких “форматов файлов” нет, да и логика прикладной модели здесь тоже не используется.

С другой стороны, функция перевода денег с одного счета на другой — это СЛУЖБА предметной области, потому что в ней реализуются ее алгоритмы, деловые регламенты (например, ведение дебета и кредита по счетам), а также потому, что “перевод денежных средств” — это термин банковского дела. В этом случае сама СЛУЖБА не делает ничего особенного, а просит два объекта “банковский счет” выполнить большую часть работы. Но поместить операцию “перевод” в объект “счет” было бы неверно, поскольку в операции участвуют два счета и ряд глобальных регламентов.

Распределение служб по уровням

Операционный

Операционная служба перевода средств:

  • принимает входные данные (например, XML_запрос);
  • посылает сообщение в службу модели для выполнения;
  • ожидает подтверждения;
  • принимает решение об отправке извещения через службу инфраструктурного уровня

Предметной области

Служба перевода средств в предметной модели:

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

Инфраструктурный

Служба рассылки извещений:

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

Теоретически можно было бы создать объект Перевод (Funds Transfer), представляющий два банковских счета плюс регламентные правила и история перевода. Но в межбанковских сетях все равно никуда не деться от вызова СЛУЖБ. Более того, в большинстве сред разработки создавать прямой интерфейс между объектом предметной области и внешними ресурсами — это нарушение стиля программирования. Внешние СЛУЖБЫ можно “задрапировать” ФАСАДНЫМ МЕТОДОМ (FACADE), на вход которого поступают объекты модели, а возвращается в качестве результата, например объект Перевод. Но какие бы у нас ни были посредники (пусть даже не из нашей системы), эти СЛУЖБЫ все равно выполняют обязанности из предметной области по переводу денежных средств.

Степень модульности

Хотя этот раздел в основном посвящен тому, чтобы подчеркнуть, как выразительно и удобно некоторые понятия модели реализуются в виде СЛУЖБ, сам по себе этот шаблон ценен тем, что помогает управлять крупностью разбиения (степенью модульности) в интерфейсах уровня предметной области, а также изоляцией клиентов от СУЩНОСТЕЙ и ОБЪЕКТОВ-ЗНАЧЕНИЙ.

Средней крупности СЛУЖБЫ, не имеющие собственного состояния, легче переносить в большие системы, поскольку в них за простым интерфейсом инкапсулируются большие функциональные возможности. Если же объекты имеют слишком мелкий масштаб, в распределенной системе это может привести к неэффективному обмену сообщениями.

Как говорилось ранее, из-за слишком мелких объектов предметной области может произойти утечка знания на операционный уровень, где координируется работа объектов модели. Сложность взаимодействия, слишком загроможденного мелкими деталями, в конце концов, передается на операционный уровень. Знание из модели потихоньку “растекается” по коду прикладных операций или пользовательского интерфейса, уходя с положенного ему уровня. Разумное же применение служб уровня предметной области позволяет удерживать четкую границу между уровнями.

В этом архитектурном шаблоне простоте интерфейса отдается предпочтение перед разнообразием функций и управлением со стороны клиента. Разбиение (модульность) функциональных возможностей поддерживается на среднем уровне, как раз удобном для инкапсулирования компонентов в больших или распределенных системах. К тому же, иногда СЛУЖБА — это самый естественный способ выразить понятие из модели предметной области.

Доступ к службам

В распределенных системных архитектурах, таких как J2EE и CORBA, имеется специальный механизм объявления СЛУЖБ вместе с соглашениями по их использованию, а также функциями распределения и доступа. Но такие среды не всегда используются в программных проектах, а даже если и используются, то в таком простом деле, как логическое разделение обязанностей, это будет стрельба из пушки по воробьям.

Не так важны конкретные средства для обеспечения доступа к службе, как базовое проектное решение разделить те или иные обязанности. Для реализации интерфейса какойнибудь СЛУЖБЫ вполне может подойти и объект“агент”. Например, для обеспечения доступа можно легко написать простой объект типа SINGLETON [14]. С помощью стилевых соглашений легко выделить эти объекты так, чтобы стало ясно — это просто механизмы для создания интерфейса СЛУЖБ, а не важные объекты модели. Сложные архитектуры следует применять только тогда, когда есть реальная потребность в сетевом распределении системы или другом использовании мощных средств архитектурной среды.

Модули (пакеты)

МОДУЛЬ (MODULE) — это устоявшийся элемент архитектуры программ. Кроме технических соображений, при разбиении на модули в основном руководствуются стремлением уменьшить смысловую сложность той или иной части программы. МОДУЛИ дают возможность посмотреть на модель с разных сторон: во-первых, можно изучить подробности устройства модуля, не вникая в сложное целое; во-вторых, удобно рассматривать взаимоотношения между модулями, не вдаваясь в детали их внутреннего устройства.

На уровне предметной области МОДУЛИ должны соответствовать смысловым частям модели, выражая суть и структуру модели в крупном масштабе.

* * *

МОДУЛИ используют все, но мало кто воспринимает их как полноценные структурные составляющие модели. Код подразделяют по разным критериям на всевозможные виды структурных элементов: от отдельных аспектов технической архитектуры до рабочих заданий конкретных разработчиков. Но даже те разработчики, которые много занимаются рефакторингом, склонны не нарушать границ МОДУЛЕЙ, установленных на ранних этапах проекта.

То, что при делении на модули должна соблюдаться низкая внешняя зависимость (low coupling) при высокой внутренней связности (high cohesion) — это общие слова. Определения зависимости и связности грешат уклоном в чисто технические, количественные критерии, по которым их якобы можно измерить, подсчитав количество ассоциаций и взаимодействий. Но это не просто механические характеристики подразделения кода на модули, а идейные концепции. Человек не может одновременно удерживать в уме слишком много предметов (отсюда низкая внешняя зависимость). А плохо связанные между собой фрагменты информации так же трудно понять, как неструктурированную “кашу” из идей (отсюда высокая внутренняя связность).

Низкая внешняя зависимость и высокая внутренняя связность — это общие принципы программной архитектуры, применяемые как к отдельным объектам, так и к МОДУЛЯМ. Но особенную важность они приобретают в крупном масштабе моделирования архитектуры. Эти термины существуют уже долгое время; изложение на эту тему в стиле описания архитектурных шаблонов можно найти в [17].

Когда два элемента модели разводятся по разным модулям, взаимосвязь между ними становится не такой прямой, как раньше. Из-за этого бывает труднее понять, какое место они занимают в архитектуре программы. Если минимизировать зависимость между МОДУЛЯМИ, задача облегчается, и становится возможным анализировать содержимое одного МОДУЛЯ, почти не обращаясь к тем другим, с которыми он взаимодействует.

В то же время элементы хорошей модели склонны к синергизму, и если подразделение на МОДУЛИ выбрано удачно, то взаимоотношения между элементами модели выходят на новый концептуальный уровень. Высокая связность между объектами родственнного назначения позволяет сосредоточить работу по моделированию и архитектурному проектированию в пределах одного МОДУЛЯ, т.е. на таком уровне сложности, который человеческому уму вполне по плечу.

МОДУЛИ и меньшие структурные элементы должны эволюционировать вместе, но на практике так обычно не получается. МОДУЛИ выбираются на ранней стадии работы, чтобы придать организованность первоначальным версиям объектов. После этого объекты имеют тенденцию развиваться так, чтобы не нарушать границ уже установленного определения МОДУЛЯ. Рефакторинг модулей требует больших затрат труда и более опасен, чем рефакторинг классов, да и делать его так же часто, скорее всего, не удастся. Но как объекты модели вначале выглядят наивно и слишком конкретно, а со временем преображаются и начинают выражать более глубокое понимание предмета, так и МОДУЛИ могут выйти на более высокий уровень точности и абстракции. Если дать МОДУЛЯМ возможность отражать изменяющееся понимание предмета, то и у объектов внутри них появится возможность более гибкой эволюции.

Как и все остальное в искусстве DDD, МОДУЛИ являются механизмом коммуникации. Выбор разбиения на МОДУЛИ должен основываться на смысле объектов, подвергающихся разделению. Если вы помещаете несколько классов в один МОДУЛЬ, вы тем самым говорите вашему преемнику, которому предстоит изучать эту архитектуру: думай о них вместе, как о целом. Если ваша модель рассказывает историю, то МОДУЛИ — это ее главы. Имя МОДУЛЯ должно передавать его содержание и входить в ЕДИНЫЙ ЯЗЫК модели. Можно сказать специалисту в предметной области: “а сейчас давайте поговорим о модуле Клиент”, и для разговора сразу же создается контекст.

Выберите такие МОДУЛИ, которые бы рассказывали историю системы и содержали связные наборы понятий. От этого часто сама собой возникает низкая зависимость МОДУЛЕЙ друг от друга. Но если это не так, найдите способ изменить модель таким образом, чтобы отделить понятия друг от друга, или же поищите пропущенное в модели понятие, которое могло бы стать основой для МОДУЛЯ и тем самым свести элементы модели вместе естественным, осмысленным способом. Добивайтесь низкой зависимости модулей друг от друга в том смысле, чтобы понятия в разных модулях можно было анализировать и воспринимать независимо друг от друга. Дорабатывайте модель до тех пор, пока в ней не возникнут естественные границы в соответствии с высокоуровневыми концепциями предметной области, а соответствующий код не разделится соответствующим образом.

Дайте модулям такие имена, которые войдут в ЕДИНЫЙ ЯЗЫК. Как сами МОДУЛИ, так и их имена должны отражать знание и понимание предметной области.

Анализ концептуальных взаимосвязей не заменяет технической работы. Это разные аспекты одной и той же проблемы, и заниматься придется ими всеми. Но мышление на основе модели порождает более глубокое и качественное решение, чем сиюминутные мероприятия. Если возникает проблема выбора, лучше придерживаться концептуальной ясности, пусть даже от этого между МОДУЛЯМИ будет больше внешних ссылок или же при внесении изменений проявятся какие-нибудь мелкие побочные эффекты. Разработчики справятся с этими мелочами, если они понимают, что именно им рассказывает модель.

* * *

Гибкая модульность

МОДУЛЯМ необходимо эволюционировать вместе с остальными компонентами модели. Это означает, что над МОДУЛЯМИ, как и над моделью с кодом, тоже нужно выполнять рефакторинг. Но такой рефакторинг часто не делают вовсе. Чтобы изменить МОДУЛИ, как правило, нужно вносить в код широкомасштабные изменения. Такие изменения могут оказаться пагубными для коммуникации в группе разработчиков, и даже нарушить работу средств разработки, например, систем управления исходным кодом. В результате структура и имена МОДУЛЕЙ часто отражают гораздо более ранние “очертания” модели, чем это делают классы.

Неизбежные ошибки, сделанные на ранних этапах разработки, приводят к высокой взаимозависимости МОДУЛЕЙ, что затрудняет рефакторинг. А недостаточный рефакторинг — это способ оттягивать решение проблемы. Решить ее можно, только стиснув зубы и реорганизовав модули. При этом надо основываться на опыте, который подсказывает, где в коде находятся проблемные места.

Некоторые средства разработки и системы программирования только усугубляют проблему. Но какая бы технология разработки не применялась для реализации программы, необходимо найти способ минимизировать работу по рефакторингу МОДУЛЕЙ и устранить препятствия, мешающие коммуникации в группе.

Пример

Правила программирования пакетов в Java

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

ClassA1 
import packageB.ClassB1; 
import packageB.ClassB2; 
import packageB.ClassB3; 
import packageC.ClassC1; 
import packageC.ClassC2; 
import packageC.ClassC3; 
. . . 

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

ClassA1 
import packageB.*; 
import packageC.*; 
. . . 

Правда, в этом приеме смешиваются два разных масштаба модульности (классы импортируют пакеты), но и информации сообщается больше, чем в предыдущем объемистом списке классов — здесь декларируется намерение установить связь (создать зависимость) с некоторым МОДУЛЕМ.

Если же конкретный класс действительно зависит от отдельного класса из другого пакета, а его МОДУЛЬ не показывает ярко выраженной концептуальной зависимости от того, другого МОДУЛЯ, то, возможно, следует или переместить класс, или даже пересмотреть всю структуру этих МОДУЛЕЙ.

Ловушки инфраструктуры

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

Пример очень полезного стандарта, задаваемого средой — принудительное введение МНОГОУРОВНЕВОЙ АРХИТЕКТУРЫ путем помещения кода инфраструктуры и пользовательского интерфейса в разные группы пакетов. Вследствие этого уровень предметной области (модели) тоже физически выделяется в свой, отдельный набор пакетов.

С другой стороны, многоярусные архитектуры могут привести к слишком фрагментированной реализации объектов модели. В некоторых средах многоуровневость создается таким образом, что обязанности одного объекта модели распределяются между многими объектами программы, которые еще и помещаются в разные пакеты. Например, обычная практика в J2EE — поместить данные и средства доступа к ним в объект Java Bean (entity bean), а связанные с ними деловые регламенты и прикладные операции — в сеансовый объект Java Bean (session bean). Не считая дополнительных сложностей реализации каждого компонента, это разделение еще и лишает объектную модель связности и согласованности. Один из фундаментальных принципов объектно-ориентированного программирования состоит в том, что в объекте должны инкапсулироваться и данные, и операции над этими данными.

В этой многоуровневой реализации, казалось бы, еще нет ничего страшного, поскольку оба компонента можно рассматривать вместе как выражение одного элемента модели. Но как будто этого мало, сеансовый объект Java Bean часто еще и выносится в другой пакет. На этом этапе мысленно собирать разные объекты в одну концептуальную СУЩНОСТЬ (ENTITY) уже отнимает слишком много труда. Теряется связь между моделью и архитектурой программы. Хорошим стилем тут будет применение более крупномасштабных компонентов EJB, чем объекты-СУЩНОСТИ, чтобы уменьшить тем самым побочные эффекты разделения уровней. Но и мелкомасштабные объекты тоже часто разбиваются на уровни.

С такими проблемами я столкнулся, например, в одном довольно неплохо управляемом проекте, в котором каждый концептуальный объект разбивался на четыре уровня. И каждое из таких разбиений имело веское обоснование. Первый уровень управлял хранением и отображением данных, а также доступом к реляционной базе. Затем шел уровень, управлявший характерными, универсальными операциями объектов. Далее находился уровень, накладывавший на все это специфические для приложения операции. И наконец, четвертый уровень задумывался как общедоступный интерфейс, отделенный от всей лежащей ниже операционной части. Эта схема была, пожалуй, сложновата, однако уровни были хорошо определены, и обязанности разделены довольно аккуратно. Мысленно объединить все эти программные объекты в один концептуальный объект модели было не так уж сложно, а разделение обязанностей даже иногда помогало. В частности, многое упростило отделение уровня данных.

Но в дополнение ко всему этому архитектурная среда программирования требовала, чтобы каждый уровень находился в отдельном наборе пакетов с именами, соответствующими правилам идентификации для этого уровня. Теперь разделение уже не помещалось в голове. В результате разработчики предметной области старались завести поменьше МОДУЛЕЙ (учитывая, что их количество надо было умножать на четыре), и почти никогда не изменяли ни одного из них, поскольку рефакторинг любого МОДУЛЯ требовал слишком много усилий. Что еще хуже, отследить все данные и операции, относящиеся к одному концептуальному объекту, стало так трудно (учитывая еще и непрямой характер деления на уровни), что разработчикам некогда было думать о каких-то там моделях. Программа все-таки была написана и сдана заказчику. Но ее “чахлая” модель предметной области свелась к тому, чтобы удовлетворить требования приложения к работе с базой данных и распределить все операции между несколькими СЛУЖБАМИ. Почти никак не проявились преимущества ПРОЕКТИРОВАНИЯ ПО МОДЕЛИ (MODEL-DRIVEN DESIGN), потому что модель не настолько прямо выражалась в коде, чтобы разработчик мог с нею работать.

Программирование в рамках архитектурных сред преследует две вполне законные цели. Во-первых, это логическое разделение обязанностей: один объект отвечает за доступ к базе данных, другой — за операции прикладной модели и т.д. Такое разделение позволяет легче понимать работу каждого уровня (в техническом смысле) и свободнее переключаться между ними. Беда в том, что при этом не осознается, какую цену приходится платить разработчикам. В этой книге вопросы программирования в архитектурных средах не рассматриваются, поэтому альтернативных решений мы искать не будем (хотя они есть). Но даже если бы других вариантов не было, лучше было бы отбросить все эти преимущества ради более связного уровня предметной области.

Движущим мотивом для применения таких схем разбиения на пакеты еще называют сетевую распределенность уровней. Это сильный довод в тех случаях, когда код действительно устанавливается на разных серверах. Но обычно это не так. А гибкости в этом смысле стоит добиваться только тогда, когда это действительно необходимо. В проекте, где есть надежда воспользоваться преимуществами ПРОЕКТИРОВАНИЯ ПО МОДЕЛИ, такая жертва слишком велика, если только она не решает наболевшую, не терпящую отлагательства проблему.

Сложные схемы разбиения на пакеты, базирующиеся на технических соображениях, имеют две особенности:

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

Если нет реальной необходимости распределять код между разными серверами, храните весь код, реализующий один концептуальный объект, в одном МОДУЛЕ, а то и классе.

К тому же самому заключению можно было бы придти, следуя старому стандарту “высокая внутренняя связность + низкая внешняя зависимость”. Связи между подобъектом, реализующим операции модели, и другим подобъектом, отвечающим за доступ к базе данных, настолько обширны, что высокая внешняя зависимость тут налицо.

Существуют и другие ловушки, грозящие принципу ПРОЕКТИРОВАНИЯ ПО МОДЕЛИ из-за правил и соглашений, принятых в архитектурных средах программирования, компаниях или отдельных проектах. Из-за них тоже может пострадать естественная связность объектов предметной области, и вывод будет таким же. Наличие строгих правил или чрезмерное количество обязательных пакетов часто сводит на нет возможность реализации других схем модульного разбиения, специально построенных для нужд модели предметной области.

Используйте разбиение на пакеты только для того, чтобы отделить уровень предметной области от остального кода. Оставляйте побольше свободы разработчикам модели предметной области, чтобы они могли объединить ее объекты в пакеты таким образом, какой лучше всего подходит для выражения модели и проектных архитектурных решений.

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

Вопрос модульного строения программы встает все острее по мере того, как архитектура программы усложняется и разрастается. В этом разделе представлены только элементарные соображения по этому поводу. В части IV “Стратегическое проектирование” много говорится о том, как правильно разбивать программу на модули, на какие части и по каким принципам делить большие модели и архитектуры, а также как привлечь внимание к их ключевым элементам для лучшего понимания процесса.

Каждое понятие модели должно найти свое отражение в элементе реализации. СУЩНОСТИ, ОБЪЕКТЫ-ЗНАЧЕНИЯ, ассоциации между ними, а также несколько СЛУЖБ уровня предметной области и МОДУЛИ, служащие для организации всего этого, — в них устанавливается прямое соответствие между программной реализацией и моделью. Объекты, указатели и механизмы извлечения данных в реализации должны соответствовать модели напрямую и самым очевидным образом. Если это не получается, приведите в порядок код, вернитесь на несколько шагов назад, измените модель — или сделайте все это вместе.

Боритесь с искушением нагрузить объекты предметной области чем-нибудь таким, что непосредственно не относится к понятиям, которые они представляют. Помните, что у этих элементов программной архитектуры уже есть обязанности — они выражают модель. Существуют и другие относящиеся к предметной области обязанности, и их надо выполнять; существуют и другие данные, и их нужно обрабатывать для слаженной работы всей системы. Но объекты модели не обязаны тащить все это на себе. В главе 6 мы рассмотрим некоторые вспомогательные объекты, выполняющие технические обязанности уровня предметной модели — например, задание поиска по базе данных и инкапсуляция создания сложных объектов.

Четыре архитектурных шаблона, представленных в этой главе, — это структурные единицы, “кирпичики” объектной модели. Но в ПРОЕКТИРОВАНИИ ПО МОДЕЛИ не обязательно насильно впихивать все на свете в формат объектов. Существуют и другие парадигмы моделирования, поддерживаемые средами программирования — например, сервер приложений (business rule engine). В проектах приходится идти на прагматические компромиссы между такими парадигмами. Но все эти среды, архитектуры и приемы — только средства для ПРОЕКТИРОВАНИЯ ПО МОДЕЛИ, а не замена ему.

Парадигмы моделирования

ПРОЕКТИРОВАНИЕ ПО МОДЕЛИ требует такой технологии реализации, которая согласовывалась бы с конкретной парадигмой моделирования, применяемой в проекте. Эксперименты проводились со многими парадигмами, но всего несколько из них остались в широком употреблении. В настоящее время основная парадигма — объектно-ориентированная. В основе разработки наиболее сложных проектов лежит именно работа с объектами. Объектно-ориентированный подход стал доминировать по ряду причин. Частично сыграли свою роль естественные свойства объектов, частично так сложились обстоятельства, а некоторые преимущества вытекают уже просто из широкой распространенности этой парадигмы.

Причины доминирования объектной парадигмы

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

Если парадигма моделирования слишком трудна для восприятия, ее не сможет освоить достаточное количество разработчиков, и использоваться она будет неправильно. Если члены группы, непосредственно не связанные с кодом, не будут владеть хотя бы основами парадигмы, они не поймут модель, и в проекте потеряется ЕДИНЫЙ ЯЗЫК. Принципы объектно-ориентированного программирования большинству людей кажутся естественными. Хотя некоторые программисты теряются в тонкостях моделирования, даже “не-технари” способны понять графическую схему объектной модели.

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

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

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

А вот объектно-ориентированный подход уже известен сообществу из многих тысяч программистов, руководителей проектов и всевозможных специалистов, участвующих в разработке программного обеспечения.

Проиллюстрировать рискованность работы с неразвитой парадигмой можно на примере одного проекта, разрабатывавшегося около полутора десятилетий тому назад. В начале 90-х годов для этого проекта были приняты на вооружение несколько самых передовых технологий, в том числе широкое использование объектно-ориентированной базы данных. Это было очень увлекательно. Гостям проекта с гордостью рассказывали, что создается и разворачивается самая большая база данных, когда-либо поддерживавшаяся в рамках этой технологии. Когда я присоединился к проекту, несколько разных групп уже мастерили объектно-ориентированные архитектуры и безо всякого труда помещали свои объекты в базу. Но постепенно на нас снизошло понимание того, что мы уже заняли значительную часть допустимого объема базы — и это еще только тестовыми данными! А реальная база должна была быть в десятки раз объемнее, и объем транзакций, проходящих через нее, — в десятки раз больше. Так возможно ли применить данную технологию в этой разработке? Использовали ли мы ее неправильно? Это было выше нашего понимания.

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

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

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

За прошедшие с тех пор годы объектно-ориентированные технологии достигли сравнительно высокого уровня развития. Большинство требуемых инфраструктурных решений доступны в готовом виде, обкатанном в индустрии. Критически важные для проекта инструментальные средства можно купить у крупных поставщиков, часто у нескольких, или взять из надежных проектов с открытым кодом. Многие из инфраструктурных решений сами по себе применяются настолько широко, что уже существует и достаточный круг знающих людей, и книги с описаниями, и т.п. Ограничения широко применяемых технологий также хорошо известны, поэтому квалифицированный коллектив имеет мало шансов “съехать” в сторону.

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

Вот почему в настоящее время в большинстве проектов, где планируется применять ПРОЕКТИРОВАНИЕ ПО МОДЕЛИ, разумно будет выбрать объектно-ориентированную технологию в качестве ядра системы. При этом чисто объектной системой ограничиваться нет нужды — объектная парадигма главенствует в отрасли, так что существуют средства интеграции с практически любой существующей технологией.

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

Не-объекты в объектном мире

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

Какова бы ни была основная парадигма моделирования в проекте, в предметной области непременно будут такие аспекты, которые легче выразить в рамках другой парадигмы. Если в предметной области совсем немного элементов, которые не вписываются в принятую (и подходящую) парадигму, то разработчики вполне могут смириться с наличием нескольких “неуклюжих” объектов в модели, которая в остальном выглядит стройно и согласованно. (В случае другой крайности — если большая часть модели более естественно описывается в рамках другой парадигмы — может быть, имеет смысл перейти на эту другую парадигму и сменить платформу реализации?) Но если достаточно крупные фрагменты модели относятся к другим парадигмам, разумно будет в каждом частном случае принять на вооружение подходящие парадигмы, а программную реализацию делать с применением смешанного набора средств разработки. Если взаимосвязи и зависимости не слишком тесные, можно ограничиться инкапсуляцией подсистемы, построенной на другой парадигме, — например, просто вызвать из объекта процедуру сложных математических расчетов. Но бывает и так, что разные аспекты модели связаны более тесно, — например, если взаимодействие между объектами зависит от неких математических соотношений.

Именно поэтому в объектные системы часто приходится интегрировать такие необъектные компоненты, как сервер приложений (business rules engine) и сервер делопроизводства (workflow engine). Смешивание парадигм позволяет разработчикам моделировать те или иные понятия таким способом, который лучше всего для них подходит. К тому же, в большинстве систем все равно приходится использовать необъектные элементы технической инфраструктуры, — как правило, реляционные базы данных. Но построить единообразную модель, подчиняющуюся нескольким парадигмам, нелегко, и обеспечить сосуществование технических средств программирования — тоже задача непростая. Если разработчики не видят четкого выражения модели в программе, ПРОЕКТИРОВАНИЕ ПО МОДЕЛИ “приказывает долго жить” — пусть даже смешение парадигм и инфраструктурных средств требует именно его.

ПРОЕКТИРОВАНИЕ ПО МОДЕЛИ в условиях смешения парадигм

Здесь в качестве примера технологии, иногда “примешиваемой” к объектно-ориентированным приложениям, будет служить сервер приложений (rules engine), реализующий деловые регламенты из определенной области. Информоемкая модель предметной области вполне может содержать явно прописанные регламентные правила, вот только объектная парадигма лишена нужных семантических средств для формулировки правил и связей между ними. Конечно, регламенты можно моделировать объектами, и это часто делается не без успеха, но их инкапсуляция в объектах мешает применять глобальные правила, распространяющиеся на всю систему. Технология серверов приложений не лишена привлекательности, поскольку обещает более естественный и декларативный способ определения правил, при котором парадигма регламентов может смешиваться с объектной парадигмой. Парадигма логики (регламентированных правил) хорошо разработана и высокоэффективна, и есть основания полагать, что она может послужить хорошим дополнением к сильным и слабым местам объектов.

Но не всегда от серверов приложений удается получить то, на что надеешься. Многие программные продукты не хотят работать как следует, и все тут. Некоторым из них не хватает внутренней согласованности — когда понятия модели, разделенные между двумя средами реализации, сохраняют родственность и взаимосвязанность. В результате часто получается так, что приложение как бы разбивается на две части: статическую систему хранения данных с использованием объектов и регламентно-алгоритмическую подсистему, которая утратила почти всякую связь с объектной моделью.

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

Если нет согласованной единообразной среды, то вся тяжесть построения модели из четких и фундаментальных концепций ложится на разработчиков. Именно от них зависит, удастся ли выдержать всю архитектуру как более-менее единое целое.

Наиболее эффективный инструмент сбора частей в одно целое — это надежный ЕДИНЫЙ ЯЗЫК, стоящий за неоднородной моделью. Последовательное применение имен и терминов в двух разных средах и введение этих имен в ЕДИНЫЙ ЯЗЫК помогает заполнить смысловой пробел.

Эта тема сама по себе заслуживает отдельной книги. Цель же этого раздела состоит в том, чтобы показать: нет причин отказываться от ПРОЕКТИРОВАНИЯ ПО МОДЕЛИ, и стоит приложить все усилия, чтобы удержаться на этом пути.

Хотя ПРОЕКТИРОВАНИЕ ПО МОДЕЛИ не обязано быть объектно-ориентированным, в нем все-таки нельзя обойтись без выразительной реализации конструкций модели — будь то объекты, регламенты или процедуры делопроизводства. Если выбранные для работы программные средства не обладают нужной выразительностью, пересмотрите этот выбор. Невыразительная программная реализация сводит на нет преимущества дополнительной парадигмы.

Ниже даны четыре основных правила для ввода необъектных элементов в преимущественно объектно-ориентированную систему.

Прежде чем взваливать на себя груз нескольких смешанных парадигм, следует исчерпать все возможности в пределах доминирующей парадигмы. Пусть некоторые понятия модели и не представляют собой очевидных объектов — все равно их часто можно промоделировать в пределах парадигмы. В главе 9 будет рассматриваться моделирование необычных видов понятий моделей с использованием объектной технологии.

Особый случай смешения парадигм представляет собой реляционная парадигма. Будучи самой распространенной необъектной технологией, реляционная база данных в то же время более тесно связана с объектной моделью, чем другие компоненты, потому что служит постоянным хранилищем данных, образующих в совокупности сами объекты. Хранение объектных данных в реляционных базах, как и многие другие проблемы цикла существования объектов, будет рассматриваться в главе 6.


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

Ваши предложения и комментарии мы ожидаем по адресу: mag@rsdn.ru
Copyright © 1994-2002 Оптим.ру