Переход к шаблонам

Глава из книги “Применение DDD и шаблонов проектирования: проблемно-ориентированное проектирование приложений с примерами на C# и .NET”

Автор: Джимми Нильссон
Источник: Применение DDD и шаблонов проектирования
Материал предоставил: Издательство ''Вильямс''
Опубликовано: 18.09.2007
Версия текста: 1.0
Вкратце о шаблонах
Предостережения в отношении шаблонов
Шаблоны проектирования
Пример применения шаблона State
Архитектурные шаблоны
Пример применения шаблона Layers
Шаблон Domain Model в качестве другого примера
Шаблоны проектирования для конкретных типов приложений
Пример применения шаблона Query Object
Шаблоны предметной области
Пример применения шаблона Factory
Резюме

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

Мне уже не раз приходилось слышать, что шаблоны — теоретическая чепуха и элитарная выдумка, не приносящая никакой пользы. Эта глава опровергает такое мнение, поскольку оно очень далеко от истины. Шаблоны могут быть очень практичными, полезными для повседневной работы и чрезвычайно интересными для разработчиков. Возможно, вы обратили внимание на то, что я уже упоминал в предыдущей главе некоторые шаблоны. К ним, в частности, относится шаблон модели предметной области [Fowler PoEAA]. В этой главе рассмотрены три разные категории шаблонов: шаблоны проектирования (как обобщенные, так и прикладные), архитектурные шаблоны и шаблоны предметной области.

СОВЕТ

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

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

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

Вкратце о шаблонах

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

ПРИМЕЧАНИЕ

Грегори Янг заметил, что многие шаблоны имеют отношение к повторному использованию посредством развязывания. Характерным тому примером служит шаблон внесения зависимостей (Dependency Injection), более подробно рассматриваемый в главе 10.

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

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

Зачем изучать шаблоны

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

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

Еще одним основанием для изучения шаблонов служит тот факт, что умение использовать шаблоны может пригодиться надолго. Для сравнения я изучил SQL в 1988 году и могу до сих пор неплохо зарабатывать себе на жизнь, работая только с этим языком. Продукты и платформы, с которыми я работаю, менялись с тех пор несколько раз, но базовые принципы остались неизменными. То же самое относится и к шаблонам. Книга Design Patterns [GoF Design Patterns] вышла в 1995 году и до сих пор очень актуальна. Следует также заметить, что шаблоны не зависят от конкретного языка, продукта или платформы. (На разных платформах могут поддерживаться определенные разновидности реализации, и это лишний раз свидетельствует в пользу книги Design Patterns, которая была написана с учетом объектной ориентации.)

Читая эту книгу, можно обнаружить, что шаблоны весьма согласуются с принципами хорошего объектно-ориентированного проектирования. А что собой представляет хорошее объектно-ориентированное проектирование? Некоторые его принципы рассматриваются в книге Роберта К. Мартина (Robert C. Martin) Быстрая разработка программ: принципы, шаблоны и практические методы (Agile Software Development: Principles, Patterns, and Practices, пер. с англ., ИД “Вильямс”, 2003 г.) [Matrin PPP]. К ним, в частности, относятся принцип единственной ответственности (ПЕО — Single Responsibility Principle (SRP)), принцип открытости-закрытости (ПОЗ — OpenClosed Principle (OCP)) и принцип подстановки Лискова (ППЛ — Liskov Substitution Principle (LSP)).

Принципы объектно-ориентированного проектирования

Ниже дается краткое пояснение принципов объектно-ориентированного проектирования, сформулированных Мартином.

Принцип единственной ответственности (ПЕО)

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

Принцип открытости-закрытости (ПОЗ)

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

Принцип подстановки Лискова (ППЛ)

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

Проблема рефлексии имеет отношение к синтаксису. Мартин приводит в большей степени семантический пример класса Square (Квадрат), который относится к классу Rectangle (Прямоугольник). Но когда используется метод задания ширины квадрата SetWidth() для класса Square, это не имеет смысла — по крайней мере, для этого нужно еще вызвать (внутренним образом) метод задания высоты квадрата SetHeight(). Такое поведение отличается оттого, что требуется для класса Rectangle.

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

Шаблоны пригодны не только для проектирования наперед, но и очень полезны (возможно, даже в большей степени) во время рефакторинга кода, когда невольно возникает мысль: “Мой код становится все более неорганизованным! Пора воспользоваться шаблоном!” Это проблема, где трудно отделить причину от следствия, но я решил все же начать с шаблонов, а затем перейти к рефакторингу кода (в следующей главе).

Предостережения в отношении шаблонов

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

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

СОВЕТ

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

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

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

Шаблоны нередко воспринимаются как слишком “простые”, но они становятся очень сложными в контексте и в сочетании с другими шаблонами. Я уже не раз встречал в группах новостей сообщения, подобные следующему: “Я разобрался с шаблоном X, но когда я попытался применить его в своем приложении вместе с шаблонами Y и Z, это оказалось чрезвычайно трудно. Пожалуйста, помогите!” Но ведь это не повод, чтобы не изучать шаблоны. В этой книге, к сожалению, нет места для обсуждения данного вопроса, хотя ему и уделяется некоторое внимание. Прежде всего, необходимо рассмотреть шаблоны отдельно от вопросов проектирования.

В своей книге Рефакторинг с использованием шаблонов (Refactoring to Patterns, пер. с англ., ИД “Вильямс”, 2006 г.) [Kerievsky R2P] Джошуа Кериевски неоднократно подчеркивает, что шаблоны чаще всего должны применяться не при проектировании наперед, а входе рефакторинга кода в направлении шаблонов.

Вот мнение Грега Ирвина (Gregg Irwin) об усвоении шаблонов: “Как и многие другие понятия, шаблоны, на мой взгляд, следует изучать поэтапно:

  1. Сначала вы пользуетесь ими, даже не осознавая этого.
  2. Затем вы узнаете, читаете о них и пытаетесь в них разобраться.
  3. Изучив шаблоны больше, вы начинаете применять их откровенно, если не наивно.
  4. Воодушевившись, вы начинаете их проповедовать (возможно и такое).
  5. Наконец до вас что-то “доходит”.
  6. Вы продолжаете изучать шаблоны и применять их “не так наивно” и менее откровенно.
  7. Проходит время, и вы замечаете в них недостатки.
  8. Далее вы подвергаете сомнению саму концепцию шаблонов (зачастую из-за неправильного ее применения).
  9. После этого вы вообще забываете о шаблонах или же приобретаете больше знаний и опыта их применения (повторяя, если требуется, пп. 5–9).
  10. И, наконец, вы вновь пользуетесь ими, даже не осознавая этого”.

А теперь перейдем к рассмотрению первой категории шаблонов и постараемся сделать так, чтобы они стали более понятными для начинающих.

Шаблоны проектирования

Одна лишь мысль об этой категории шаблонов вызывает ассоциации с книгой Design Patterns [GoF Design Patterns], которая не раз упоминается в данной книге. Это, конечно, не единственная книга о шаблонах проектирования, тем не менее, она считается авторитетным трудом на данную тему.

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

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

ПРИМЕЧАНИЕ

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

Книга Design Patterns [GoF Design Patterns] читается с большим трудом. Но всякий раз, когда я читаю ее, то узнаю и выясняю для себя что-то новое. В частности, по поводу шаблонов у меня часто возникали мысли о том, что это неверное, не самое лучшее и даже нелепое решение. Но по зрелом размышлении, я всякий раз приходил к выводу, что оно все-таки верное.

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

Пример применения шаблона State

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

Задача

Заказ на закупку может находиться в одном из следующих состояний: New Order (Новый заказ), Registered (Зарегистрирован), Granted (Подтвержден), Shipped (Отгружен), Invoiced (Выписан счет-фактура) и Cancelled (Отменен). Имеются строгие правила, регламентирующие переход заказа из одного состояния в другое. Например, не разрешается непосредственный переход из состояния Registered в состояние Shipped.

Кроме того, имеются отличия в поведении в зависимости от конкретного состояния. Так, в состоянии Cancelled нельзя вызвать метод AddOrderLine(), чтобы ввести в заказ дополнительные позиции (это же, кстати, относится и к состояниям Shipped и Invoiced).

Следует также иметь в виду, что определенное поведение приводит к смене состояния. Так, если вызывается метод AddOrderLine(), происходит смена состояния Granted обратно на New Order.

Первое предлагаемое решение

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


Рис. 2.1. Граф состояний кнопки

Если применить этот же метод к заказу (Order), то получится граф состояний, приведенный на рис. 2.2.


Рис. 2.2. Граф состояний заказа

Одно из наиболее очевидных решений, вероятно, состоит в следующем применении оператора enum:

public enum OrderState
{
   NewOrder,
   Registered,
   Granted,
   Shipped,
   Invoiced,
   Cancelled
}

Затем для хранения текущего состояния заказа можно воспользоваться закрытым полем в классе Order следующим образом:

private OrderState _currentState = OrderState.NewOrder;

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

private void AddOrderLine(OrderLine orderLine)
{
   if (_currentState == OrderState.Registered || _currentState == OrderState.Granted)
      _currentState == OrderState.NewOrder;
   else if (_currentState == OrderState.NewOrder)
           // Не выполнять переход.
        else
           throw new ApplicationException(...
   // Выполнить полезные действия...
}

Как следует из приведенного выше фрагмента кода, в данный метод было введено немало избыточного кода только ради соблюдения графа состояний. Аналогичный код предстоит рассредоточить по всему классу Order, чтобы распространить сведения о графе состояний среди разных методов. Это характерный пример едва заметного, но пагубного дублирования кода.

Даже в таких примерах, как этот, следует принять меры для сведения к минимуму дублирования и фрагментации кода. Попробуем это сделать.

Второе предлагаемое решение

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

private void _ChangeState(OrderState newState)
{
   if (newState == _currentState)
      return; // Не считать ошибкой переход состояния в самое себя
   switch (_currentState)
   {
      case OrderState.NewOrder:
         switch (newState)
         {
            case OrderState.Registered:
            case OrderState.Cancelled:
               _currentState = newState;
            break;
            default:
               throw new ApplicationException(...
            break;
         }
      case OrderState.Registered:
         switch (newState)
         {
            case OrderState.NewOrder:
            case OrderState.Granted:
            case OrderState.Cancelled:
               _currentState = newState;
            break;
            default:
               throw new ApplicationException(...
            break;
         }
...
// И так далее...
   }
}

Теперь метод AddOrderLine() выглядит следующим образом:

public void AddOrderLine(OrderLine orderLine)
{
   _ChangeState(OrderState.NewOrder);
   // Выполнить полезные действия...
}

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

ПРИМЕЧАНИЕ

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

Что же касается недоброкачественного кода, то более подробно о нем речь пойдет в следующей главе.

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

Третье предлагаемое решение

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

Текущее состояниеНовое допустимое состояние
NewOrderRegistered
NewOrderCancelled
RegisteredNewOrder
RegisteredGranted
RegisteredCancelled
Таблица 2.1. Смена состояний заказа

Теперь в методе _ChangeState() достаточно проверить, является ли допустимым новое состояние, передаваемое в качестве параметра, если, например, текущим оказывается состояние NewOrder. В этом случае в качестве нового допускается только состояние Registered или Cancelled.

Данную таблицу можно дополнить еще одним столбцом (табл. 2.2).

Текущее состояниеМетодНовое допустимое состояние
NewOrderRegister()Registered
NewOrderCancel()Cancelled
RegisteredAddOrderLine()NewOrder
RegisteredGrant()Granted
RegisteredCancel()Cancelled
Таблица 2.2. Смена состояний заказа, пересмотренная

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

А есть ли другие решения? Последуем принципу повторного использования знаний и попробуем применить шаблон проектирования под названием State .

Четвертое предлагаемое решение

Общая структура шаблона State приведена на рис. 2.3.


Рис. 2.3. Общая структура шаблона State

Идея заключается в том, чтобы инкапсулировать разные состояния в виде отдельных классов (см. ConcreteStateA и ConcreteStateB). Эти классы конкретных состояний наследуют от класса State. У класса Context имеется экземпляр в виде поля, и когда он получает вызов метода запроса Request(), то вызывает метод обработки Handle() экземпляра состояния. Метод Handle() по-разному реализуется для классов различных состояний.

Итак, это общая структура данного шаблона. А теперь посмотрим, как она будет выглядеть, если применить ее к решаемой задаче. На рис. 2.4 приведена UML-схема для рассматриваемого примера.


Рис. 2.4. Конкретный пример применения шаблона State

СОВЕТ

В данном примере было бы целесообразно ввести еще один абстрактный класс в качестве базового класса для состояний NewOrder, Registered и Granted. В этом классе могут быть реализованы методы AddOrderLine() и Cancel().

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

Вновь образовавшийся класс Order получает новый экземпляр состояния NewOrder при создании копии экземпляра и передает себя конструктору следующим образом:

internal OrderState _currentState = new NewOrder(this);

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

На этот раз метод Register() выглядит в классе Order достаточно просто, например, следующим образом:

public void Register()
{
   _currentState.Register();
}

И в классе NewOrder метод Register() выглядит не намного сложнее. По крайней мере, в нем можно теперь уделить основное внимание определению его собственного состояния, и благодаря этому код становится четким и ясным. Во всяком случае, он мог бы иметь следующий вид:

public void Register()
{
   _parent._Register();
   _parent._currentState = new Registered(_parent);
}

Перед сменой состояния происходит обратный вызов родителя (_parent._Register()), предписывающий ему подготовиться к смене состояния. Следует заметить, что обратный вызов передается внутреннему методу _Register() а не общедоступному методу Register(). Разумеется, это лишь один из возможных вариантов.

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

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

ПРИМЕЧАНИЕ

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

Это также означает, что вместо базового класса OrderState можно воспользоваться интерфейсом. Догадываюсь, что если бы книга Design Patterns [GoF Design Patterns] была написана ныне, во многих шаблонах вместо абстрактных базовых классов (или, по крайней мере, вместе с ними) использовались бы интерфейсы. Шаблон State не может служить характерным тому примером, хотя и в нем такое вполне возможно.

Дополнительные пояснения

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

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

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

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

Архитектурные шаблоны

Термин архитектурные шаблоны обычно ассоциируется с теми шаблонами, которые рассматриваются в книге Pattern-Oriented Software Architecture (Pattern-Oriented Software Architecture) [POSA 1] Бушмана и других (Buschmann et al.). В этой книге некоторые шаблоны отнесены к категории архитектурных. К ним, в частности, относятся шаблоны Pipes and Filters (Каналы и фильтры) и Reflection (Рефлексия).

Шаблон Pipes and Filters направляет потоки данных по каналам и обрабатывает их с помощью фильтров. Этот шаблон нашел признание среди разработчиков, пользующихся SOA, — архитектурой, ориентированной на службы, в качестве полезного средства для построения систем, ориентированных на обмен сообщениями.

Шаблон Reflection в строен в обе платформы Java и .NET. При этом вы получаете возможность писать программы, которые читают и записывают информацию в объекты в самом общем виде, используя метаданные объектов, т.е. ничего не зная заранее о типе объекта.

Если шаблоны проектирования предназначены для улучшения подсистем или компонентов, то архитектурные шаблоны служат для структурирования подсистем. А теперь перейдем к характерному примеру — шаблону Layers (Слои) [POSA 1].

Пример применения шаблона Layers

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

Шаблон Layers широко распространен и вполне понятен, поэтому приведем лишь краткий пример его применения, чтобы дать ясное представление о данной категории шаблонов.

Задача

Допустим, что построен ряд классов для приложения SalesOrder (Заказ на закупку), в том числе классы Customer, Order, Product и т.д. Эти классы инкапсулируют значение, которое они имеют для предметной области, а также способ их сохраняемости/несохраняемости и представления. Текущая реализация должна обеспечивать сохраняемость в файловой системе и представление объектов в виде фрагментов кода HTML.

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

Первое предлагаемое решение: применить слои

Самое распространенное решение данной задачи состоит, вероятно, в том, чтобы перенести ответственность некоторых классов. Ведь новые требования проливают свет на то, что эти классы нарушают ПЕО.

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

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

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

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

К вопросу разделения на слои мы еще вернемся в главе 4, рассмотрев его под несколько иным углом зрения.

Шаблон Domain Model в качестве другого примера

Шаблон модели предметной области ( Domain Model) [Fowler PoEAA] мы уже упоминали в главе 1 и будем делать это на протяжении всей этой книги. Мне лично данный шаблон представляется удачным примером архитектурного шаблона.

Мы не будем рассматривать здесь пример применения шаблона Domain Model, поскольку данной теме посвящена львиная этой книги. Вместо этого продолжим обсуждение шаблонов, уделив внимание тому, насколько они зависят от предметной области. Итак, перейдем к шаблонам проектирования для конкретных типов приложений.

Шаблоны проектирования для конкретных типов приложений

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

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

Основным источником по шаблонам данной категории служит книга Архитектура корпоративных программных приложений [Fowler PoEAA] Мартина Фаулера.

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

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

В качестве конкретного примера рассмотрим применение шаблона Query Object (Объект запроса) [Fowler PoEAA].

Пример применения шаблона Query Object

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

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

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

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

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

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

Задача

Пользователям требуется форма, в которой им было бы удобно искать покупателей. У них должна быть возможность запрашивать всех покупателей, например, по следующим признакам:

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

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

Первое предлагаемое решение: фильтрация в модели предметной области

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

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

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

Если бы вместо этого имелось одно разделяемое подмножество экземпляров модели предметной области на сервере приложений (которому присущи свои недостатки — подробнее об этом в последующих главах книги), такое решение было бы приемлемым, но только для логики на стороне сервера. Что же касается клиентов, запрашивающих подмножество разделяемых экземпляров модели предметной области, то они должны как-то выразить свои критерии запроса.

Второе предлагаемое решение: фильтрация в базе данных по крупным спискам параметров

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

Первую задачу мог бы решить следующий оператор SQL:

SELECT Id, CustomerName, ...
FROM Customers
WHERE CustomerName LIKE '%aa%'
AND Id IN
(SELECT CustomerId
FROM ReferencePersons
WHERE FirstName = 'Stig')
AND Id IN
(SELECT CustomerId
FROM Orders
WHERE TotalAmount > 1000000)
AND Id IN
(SELECT CustomerId
FROM Orders
WHERE OrderDate BETWEEN '20040601' AND '20040630'
ПРИМЕЧАНИЕ

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

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

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

public IList SearchForCustomers
(string customerNameWithWildCards
, bool mustHaveOrderedSomethingLastMonth
, int minimumOrderAmount
, string fi rstNameOfAtLeastoneReferencePerson)

Возможно, это решение и удовлетворит требованию первого запроса, но не второго. Для этого придется ввести ряд других параметров, как например:

public IList SearchForCustomers
(string customerNameWithWildCards
, bool mustHaveOrderedSomethingLastMonth
, int minimumOrderAmount
, string fi rstNameOfAtLeastoneReferencePerson)
, string country, string town)

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

Еще одна трудность заключается в том, как выразить определенные понятия в списке параметров этим беспомощным способом указания примитивных типов данных. Характерным тому примером служит параметр mustHaveOrderedSomething LastMonth (То, что должно было быть заказано за последний месяц). А как насчет предыдущего месяца или последнего года? Конечно, в качестве параметров можно было бы в данном случае использовать две даты и перенести ответственность за определение временного промежутка между ними на потребителя метода, но что если нас интересуют только покупатели в определенном городе? Как в таком случае должны выглядеть параметры даты? Полагаю, что можно было бы выбрать минимальную и максимальную даты, чтобы образовать как можно больший временной промежуток между ними, но это совсем не очевидный способ выражения “всех дат”.

ПРИМЕЧАНИЕ

Грегори Янг так прокомментировал проблему выражения способа предшествования операторов в списке параметров, сославшись на следующий пример:

(критерий1 и критерий2) или (критерий1 и критерий2)

От такого решения приходится быстро отказываться. Я пришел к этому выводу еще в те времена, когда работал в среде VB6, и поэтому остановил свой выбор на массиве. В первом столбце массива находилось имя поля (например, CustomerName), во втором столбце — оператор (например, оператор Like из перечисления), а в третьем столбце — критерий (например, “*aa*”). Для каждого критерия в массиве была выделена одна строка.

Такое решение позволило преодолеть некоторые трудности, связанные со списком параметров, но у него были свои трудности. С одной стороны, мне не нужно было менять старый код потребителя только потому, что может быть введен новый критерий — это было удобно. Но с другой стороны, данный способ оказался совершенно беспомощным в отношении более развитых критериев, и поэтому мне пришлось отступить на шаг назад и раскрыть схему базы данных для того, чтобы справиться, например, с таким критерием: “Имеются ли заказы на общую сумму не более одного миллиона?”. Для критерия я тогда использовал полный оператор IN.

Решение, основанное на использовании массива, оказалось шагом в верном направлении, но оно могло бы стать еще более гибким, если бы массив был заменен объектами. К сожалению, в VB6 не было возможности создавать компоненты маршализации по значению. Конечно, можно было бы выбрать более гибкую структуру массива, но все это намного естественнее делается в .NET с помощью шаблона Query Object.

Третье предлагаемое решение: применение шаблона Query Object

Принцип, положенный в основу шаблона Query Object , состоит в том, чтобы инкапсулировать критерии в экземпляре объекта запроса (Query), а затем передать этот экземпляр в другой слой, где он преобразуется в требуемый запрос SQL. UML-схема общего решения может выглядеть так, как показано на рис. 2.5.


Рис. 2.5. Блок-схема класса для общего решения с помощью шаблона Query Object

В критерии можно было бы использовать другой запрос (даже если это не совсем очевидно из типичной блок-схемы на рис. 2.5). Подобным образом нетрудно сформировать аналог подзапроса в SQL.

А теперь попытаемся выработать язык для шаблона Query Object применительно к решаемой задаче. Но прежде допустим, что модель предметной области выглядит так, как показано на рис. 2.6.

Посмотрим, как вновь сформированный наивный язык запросов будет выглядеть на C#:

Query q = new Query("Customer");
q.AddCriterion("CustomerName", Op.Like, "*aa*");
Query sub1 = new Query("Order");
sub1. AddCriterion("TotalAmount", Op.GreaterThan, 1000000);
Query sub2 = new Query("Order");
sub2. AddCriterion("OrderDate", Op.Between,
DateTime.Parse("2004-06-01"), DateTime.Parse("2004-06-30"));
q.AddCriterion(sub2);
q.AddCriterion("ReferencePersons.FirstName",Op.Equal, "Stig");


Рис. 2.6. Модель предметной области для рассматриваемого примера

ПРИМЕЧАНИЕ

Параметр конструктора Query является не именем таблицы, а именем класса модели предметной области. Это же относится и к параметрам метода AddCriterion(). Под этим подразумеваются не столбцы таблицы, а свойства и поля класса. В данном случае имена свойств или полей используются в модели предметной области.

Следует также заметить, что в данном конкретном примере не потребовался подзапрос для критерия, связанного с поиском ответственных лиц (ReferencePersons), поскольку в модели предметной области можно перемещаться от объекта Customer к объекту ReferencePerson. С другой стороны, подзапросы потребовались для поиска заказов (Orders) по совсем другой причине.

Дополнительные пояснения

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

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

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

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

ПРЕДУПРЕЖДЕНИЕ

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

Некоторые читатели, имеющие опыт ППО, вероятно, предпочли бы воспользоваться шаблоном Specification (Описание) [Evans DDD] для решения данной задачи, что приводит нас непосредственно к третьей и последней категории шаблонов, которую мы собираемся рассмотреть: шаблонам предметной области.

Шаблоны предметной области

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

По своему назначению эти шаблоны в чем-то пересекаются с шаблонами проектирования и, в частности, с шаблоном Strategy ( Стратегия) [GoF Design Patterns], который может быть отнесен и к категории шаблонов предметной области. Причина такого пересечения заключается в том, что шаблоны, подобные Strategy, отлично подходят для структурирования модели предметной области.

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

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

Например, для поиска покупателей золота можно воспользоваться как объектами запроса, так и описаниями, но решение в обоих случаях окажется разным. С помощью шаблона Object Query выражаются критерии определения покупателей золота, а с помощью шаблона Specification — класс, который может, в частности, называться GoldCustomerSpecification (Описание покупателя золота). В данном случае сами критерии не обнаруживаются и не дублируются, но инкапсулируются в классе, имеющем подходящее описательное имя.

Одним из источников по шаблонам предметной области служит книга [Arlow/ Neustadt Archetype Patterns], но я выбрал пример шаблона предметной области из другого источника: книги Domain-Driven Design [Evans DDD] Эрика Эванса. Этим примером служит шаблон Factory (Фабрика).

ПРИМЕЧАНИЕ

Здесь шаблон Factory рассматривается как шаблон предметной области, а в книге Design Patterns [GoF Design Patterns] также упоминаются шаблоны типа Factory. Но это не должно вводить в заблуждение читателей, знакомых с шаблонами, поскольку шаблоны проектирования сосредоточены в большей степени на специальном, техническом уровне, а шаблоны предметной области — на семантическом уровне предметной области.

Кроме того, они отличаются конкретными целями и типичными реализациями. В книге Design Patterns такие шаблоны называются Factory Method (Фабричный метод) и Abstract Factory (Абстрактная фабрика). В частности, шаблон Factory Method служит для переноса создания экземпляра “правильного” класса в подклассы, а шаблон Abstract Factory — для формирования семейств зависимых объектов.

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

Пример применения шаблона Factory

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

Задача

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

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

Первое предлагаемое решение

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

public Order();

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

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

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

Так или иначе, в C#, Java и прочих языках программирования у нас имеется возможность использовать параметризированные конструкторы, и в этом, собственно, заключается суть первого предлагаемого решения данной задачи. Итак, воспользуемся тремя общедоступными конструкторами класса Order следующим образом:

public Order(Customer customer);
public Order(Order order,
bool trueForCreditFalseForRepeat);
public Order(int orderId);

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

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

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

Второе предлагаемое решение

Как следует из книги Effective Java [Bloch Effective Java], первым и самым лучшим из 57 практических приемов программирования, которые следует взять на вооружение, считается замена конструкторов фабричными методами. В данном случае это может выглядеть следующим образом:

public static Order Create(Customer customer);
public static Order CreateReservation(Customer customer);
public static Order CreateCredit(Order orderToCredit);
public static Order CreateRepeat(Order orderToRepeat);
public static Order Get(int orderId);

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

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

А есть ли у них какие-нибудь недостатки? Их главный недостаток, на мой взгляд, состоит в нарушении ПЕО [Martin PPP], когда код создания находится в самом классе. В книге Эванса [Evans DDD] по этому поводу приводится удачное сравнение с автомобильным двигателем, которому ничего не известно о том, как он был создан, поскольку он за это не отвечает. Только представьте, насколько сложнее стал бы автомобильный двигатель, если бы ему пришлось не только работать, но и создавать самого себя. Этот аргумент особенно справедлив в тех случаях, когда код создания оказывается сложным.

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

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

Третье предлагаемое решение

Итак, перейдем непосредственно к применению шаблона Factory в качестве шаблона предметной области. Начнем с его блок-схемы, приведенной на рис. 2.7.


Рис. 2.7. Блок-схема экземпляра для шаблона Factory

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

anOrder = OrderFactory.Create(aCustomer);
aReservation = OrderFactory.CreateReservation(aCustomer);
aCredit = OrderFactory.CreateCredit(anOldOrder);
aRepeat = OrderFactory.CreateRepeat(anOldOrder);

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

Для того чтобы потребитель смог получить старый заказ, ему нужно сообщить еще кое-что (не только шаблон Factory, но и шаблон Repository). Это более ясный и выразительный способ, но о нем речь пойдет далее.

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

ПРИМЕЧАНИЕ

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

Дополнительные пояснения

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

Для шаблона Factory весьма характерна установка экземпляра объекта в действительное состояние. В частности, мне пришлась по душе возможность устанавливать подэкземпляры с помощью шаблонов Null Object (Пустой объект) [Woolf Null Object], когда это уместно. Возьмем для примера заказ (Order). За отгрузку заказа отвечает транспортировщик(Transpoter).

Сначала заказ не был отгружен (и в этом случае отгрузка нас мало интересовала), поэтому мы не можем предоставить для него какой-либо объект Transpoter, но вместо того чтобы просто оставить пустым свойство Transpoter, мы устанавливаем это свойство в соответствии с пустым (скорее, “не выбранным”) объектом Transpoter. Это всегда позволяет обнаружить описание свойства Transpoter аналогично следующему:

anOrder.Transpoter.Description

Если бы объект Transpoter был пустым, нужно было бы проверить сначала именно этот факт. Для подобных целей как нельзя лучше подходит шаблон Factory. (То же самое можно было бы сделать и с помощью конструктора, но во многих случаях приходится применять шаблоны Null Object, а решиться на это непросто, если имеются другие возможности. Однако не следует забывать об усложнении конструктора.)

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

Применяя шаблоны Factory, можно также скрывать требования к инфраструктуре, которых нельзя избежать.

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

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

ПРИМЕЧАНИЕ

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

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

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

В общем, я не вижу в этом, как правило, особой проблемы.

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

Резюме

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

А теперь пора перейти к рассмотрению особенностей применения РПТ на конкретных примерах.


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