На старт

Дамы на старте

Данной статьей мы даем старт непосредственно разработке Arachne CMS. Тот, кто ожидает пространных рассуждений об архитектуре системы управления контентом «с высоты птичьего полета» — обманется. Действие будет разворачиваться в духе гибких методологий разработки. И, поскольку сейчас мы в блоге, то прямо с его разработки и начнем, постепенно раскручивая маховик функциональности.

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

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

К источнику данных

В момент написания эта статья была обычной динамической страницей на ASP.NET MVC. Контент-менеджмента в ней не было ни на грош. Поэтому начнем с того, что выделим этот контент, определив его тип в виде примитивного пока класса BlogPost.

public class BlogPost { public string Title { get; set; } public string Content { get; set; } }

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

@model BlogPost <h1>@Model.Title</h1> <div>@Model.Content</div>

Теперь контроллер должен передать контент этой статьи шаблону.

public class BlogController : Controller { public ActionResult Get(string id) { BlogPost model = new BlogPost() { Title = "Заголовок статьи", Content = "Содержание статьи" }; return View(model); } }

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

Репозиторий

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

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

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

public interface IBlogRepository { BlogPost GetPost(string id); }

Если полагаться на то, что конкретная реализация интерфейса репозитория передается нам от IoC-контейнера, то контроллер бы стал таким:

public class BlogController : Controller { private IBlogRepository _Repository = null; public ActionResult Get(string id) { BlogPost model = _Repository.GetPost(id); return View(model); } public BlogController(IBlogRepository repository) { _Repository = repository; } }

Но так ли хорош этот паттерн? Попробуем рассмотреть его под микроскопом принципов SOLID.

SRP — Single responsibility principle

Первым будет принцип единственной обязанности. Когда репозиторий ограничивается только примитивными операциями CRUD нам кажется, что все в порядке. Но, как только система начинает обрастать сложными агрегатами, выражаясь терминологией DDD, репозитории начинают тянуть на себя все больше обязанностей. Сейчас наш класс работает только со статьями блога, завтра начнет управлять комментариями, послезавтра — тегами. Даже пытаясь сделать множество узкоспециализированных репозиториев мы все равно придем к тому, что некоторые сущности будут «сидеть на двух стульях», создавая дополнительные обязанности. В нашем примере будет весьма непросто расцепить статьи и комментарии к ним, чтобы в репозитории к одному не было операций, затрагивающих другого.

OCP — Open Close principle

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

В качестве примера, иллюстрирующего нарушения двух приведенных принципов приходит на ум книга NET Domain-Driven Design with C#, активно использующая этот паттерн. В третьей главе авторы реализуют управление проектами и создают IProjectRepository. В четвертой, когда создается репозиторий по работе с контактами, происходит такое изменение кода:

public interface IProjectRepository : IRepository<Project> { ...Код из репозитория третьей главы... void SaveContact(ProjectContact contact); }

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

LSP — Liskov substitution principle

Стараясь придти в соответствие с принципом SRP, у нас в проекте начинают плодиться репозитории, старающиеся сконцентрироваться на одной обязанности. Их много, они очень похожи, но не полностью идентичны. И вот, уже с другой стороны, начинает давить принцип DRY. Находясь между этой «Сциллой и Харибдой» мы начинаем выстраивать замысловатую иерархию классов. А там, где много неочевидного наследования, растет риск споткнуться о принцип подстановки Лисков.

ISP — Interface segregation principle

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

DIP — Dependency inversion principle

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

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

Асинхронность

В мире многопроцессорных, многоядерных систем, объединенных в кластеры, асинхронность — это ключ к производительности. Способствует ли этому репозиторий? Увы. Вариант, который очевидно напрашивается — использовать паттерн AMI. Но он потребует от нас удвоения числа всех методов. И в .NET наш репозиторий мог бы стать таким:

public interface IBlogRepository { IAsyncResult BeginGetPost(string id, AsyncCallback callback); void EndGetPost(IAsyncResult result); }

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

repository.BeginGetPost("unique-post-alias", result => { BlogPost post = repository.EndGetPost(result); ... } );

Увы, здесь у паттерна Репозиторий тоже нет сильных сторон.

Сквозная функциональность

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

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

Итоги

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