Шаблон проектирования «Спецификация»

Disclaimer

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

О шаблонах проектирования

Википедия дает следующее определение:

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

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

Шаблон «Спецификация» – является шаблоном поведения приложения. Результатом выполнения будет являтся булевская переменная, подав которую на вход оператора условного перехода можно управлять поведением программы.

Ниже будут описаны приемы с помощью которых вы сможете:

  • Сделать  код более читабельным и кратким;
  • Избежать дубликации кода;
  • Облегчить внесение изменений в реализацию.

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

Область применения

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

Итак, допустим у нас есть класс для товаров Ware.

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

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

Техника использования шаблона «Спецификация» будет показана на простом примере. В тесте использован DSL (Domain Specific Language) про который можно прочитать в более ранних статьях. В исходном коде можно будет найти его реализацию.

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

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

Сейчас можно занятся рефакторингом (переписыванием, улучшением) метода CountExpiredWares. Для этого мы создадим новый класс спецификации WareExpireDateSpecification.

Помним, что «нет теста – нет кода» и поэтому метод у нас всегда возвращает false, пока не напишем тесты.

И реализация спецификации:

В теле доменного метода теперь можно использовать спецификацию.

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

Улучшение шаблона «Спецификация»

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

В таком случае, эталон для проверки передается в конструктор спецификации.

Композиция спецификаций

Предположим, что у нас есть метод в доменном сервисе, который осуществляет подсчет товаров по каким-то критериям для специальной акции.

Акций может быть сколько угодно и по разным условиям, так же может быть и различное число условий на товар.  Неужели придется писать бесчисленное количество методов с разным входным набором параметров или условий? Вовсе нет. Шаблон «Спецификация» позволит нам написать примерно следующий код метода CountSpecialWares:

Вторым параметром будет идти любая комбинация спецификаций, которая может быть применима для класса Ware.

Для комбинирования спецификаций предлагаю сделать 2 класса,

  • AndSpecification
  • OrSpecification

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

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

Для класса OrSpecification проверяем до первого успешно пройденного условия проверки.

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

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

Еще один пример, который может быть полезен в реальности – поиск по шаблону. Допустим, у вас есть форма для поиска товара, где заполняются все или только некоторые поля. По нажатию на кнопку «Поиск», вы можете сформировать новый товар с информацией введенной пользователем и передать его параметром в конструктор спецификации.

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

Плюсом такого подхода является то, что при изменении доменного объекта, вам надо будет изменить только метод IsSatisfiedBy в паттерне, а не выискивать по всему коду использование поиска и его редактирование. Если вы пропустите какое-то место, компилятор вам не подскажет, ведь весь код правилен, все указанные поля существуют. То, что появились новые поля и они существенны для поиска его не волнует.

Best Known Methods (BKM)

Пустая спецификация

В процессе практического использования оказалась полезной пустая (Null) спецификация, которая всегда возвращает true для любого объекта.

Переопределение операций

Для того, чтобы постоянно не писать AndSpecification или OrSpecification, можно использовать перегруженные операторы & и | соответственно. Т.е. запись вида

можно превратить в

Реализация будет находится в абстрактном классе Specification.

Выделение спецификации из набора

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

Пример использования и проверка через тест:

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

Hard’n’Heavy!

2 комментарий на “Шаблон проектирования «Спецификация»

  1. Андрей приветствую. Спасибо за хорошие статьи.
    Читая посты, мелькнула мысль, а не Женя 40in пишет их, оказалось его знакомый)) пропитаны общими идеями).
    P.S. отличный блог

Оставить комментарий