Сервер объектов

Шестеренки в работе

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

Как известно, CMS активно эксплуатируют протокол HTTP. И будь мы математиками, то сказали бы, что контент-менеджмент система — это функция y=f(x). Где x - HTTP-запрос, а y - HTTP-ответ.

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

POST http://www.arachne-cms.com/account/register?return=/blog/some-post Host: www.arachne-cms.com Connection: keep-alive Content-Length: 77 Cache-Control: max-age=0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8 Origin: http://www.rcu.ru User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Content-Type: application/x-www-form-urlencoded Accept-Encoding: gzip,deflate,sdch Accept-Language: en-US,en;q=0.8,ru;q=0.6,bg;q=0.4 Cookie: TRACKING=BFBFE8F0-82A8-436B-88F8-53E2F5E52E6C Mail=stanislav%40yarmonov.pro&Password=mypassword&FirstName=Станислав

В ответ на этот запрос, в недрах Arachne CMS закрутились бы всевозможные шестеренки и сформировали HTTP-ответ. Например, такой:

HTTP/1.1 200 OK Cache-Control: private Content-Type: text/html; charset=utf-8 Server: Microsoft-IIS/8.0 X-AspNetMvc-Version: 4.0 X-AspNet-Version: 4.0.30319 X-Powered-By: ASP.NET Date: Thu, 20 Feb 2014 21:37:58 GMT Content-Length: 100 <!DOCTYPE HTML> <html> <head> <title>Успешная регистрация в системе</title> </head> <body> <p> Здравствуйте, Станислав! На ваш адрес электронной почты stanislav@yarmonov.pro отправлено письмо с инструкциями по активации. </p> </body> </html>

От текста к моделям

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

HttpRequestModel request = new HttpRequestModel() { Method = "POST", URI = "http://www.arachne-cms.com/account/login", ...остальные HTTP-заголовки... Registration = new RegistrationRequestModel() { Mail = "stanislav@yarmonov.pro", Password = "mypassword", FirstName = "Станислав", ... } }

Объектный же ответ мог стать таким:

HttpResponseModel response = new HttpResponseModel() { Status = 200, CacheControl = CacheControl.Private, ...Остальные HTTP-заголовки... Registration = new RegistrationResponseModel() { FirstName = "Станислав", Mail = "stanislav@yarmonov.pro", Activation = "8ED98ED9-6855-42EC-B194-2086C38F7E4D", ... } }

Безусловно, с такими рафинированными моделями гораздо удобней работать. Фактически, мы сняли с жизненного цикла запроса толстый текстовый слой и выделили две обязанности — преобразование HTTP-запроса в модель и модели в HTTP-ответ. В ASP.NET за первую обязанность отвечают model binders, а за вторую — view engines (включая популярный Razor).

Выделение таких обязанностей удобно тем, что завтра мы можем захотеть добавить активную работу с JSON-моделями. А может реализовать странное — редактирование контента через FTP или WebDAV. Для этого нам будет достаточно заменить наши преобразователи из текста в объектную систему координат, а вся остальная система продолжит работать как ни в чем не бывало.

Объектный сервер

Если рассмотреть схематический код типового контроллера ASP.NET MVC ниже, то создается ощущение в его бесполезности:

public ActionResult Login(RegistrationRequestModel request) { AccountModel response = ...трансформация объектного запроса в объектный же ответ...; return View(response); }

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

public interface IObjectServer { IResponseModel<TResponse> Get<TResponse, TRequest>(IRequestModel<TRequest> request); } /* А так мы могли бы его использовать */ HttpRequestModel request = ...получаем модель запроса...; IObjectServer server = ...получаем доступ к серверу...; HttpRequestModel response = server.Get(request);

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

public class HttpRequestHandler : IRequestHandler<HttpRequestModel, HttpResponseModel> { private IObjectServer _Server = null; IResponseModel<HttpResponseModel> IRequestHandler.Process(HttpRequestModel request) { ...занимаемся вопросами диспетчеризации... /* Процесс регистрации — не наша обязанность и мы ее делегируем другому обработчику */ response.Registration = _Server.Get(request.Registration); ...продолжаем решать вопросы диспетчеризации... } public HttpRequestHandler(IObjectServer server) { _Server = server; } }

В этом примере связь между HttpRequestModel и RegistrationModel, HttpResponseModel и AccountModel — жёсткая. Чтобы примеры были простыми и очевидными. Но в реальных условиях мы такие связи будем разрывать.

Назад к хранилищу

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

public class RegistrationRequestHandler : IRequestHandler<RegistrationRequestModel, RegistrationResponseModel> { RegistrationResponseModel IRequestHandler.Process(RegistrationRequestModel request) { /* Отправляем запрос в базу данных на регистрацию */ RegistrationResponseModel response = _Server.Get(new DbRequest(request)); /* Если регистрация прошла успешно, то отправляем письмо на электронную почту */ if (response.Succeed) { response.Mail = _Server.Get(new MailRequest(request, "TemplateRegistered")); } return response; } }

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

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

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

SRP — Single Responsibility Principle

Лежит в основе идеи и выражен как единственным методом интерфейса IObjectServer, так и общей нацеленностью каждого класса-обработчика на реализацию только одного специфичного контракта по преобразованию конкретного типа запроса в конкретный тип ответа.

OCP — Open Close Principle

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

LSP — Liskov Substitution Principle

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

ISP — Interface Segragation Principle

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

DIP — Dependency Inversion Principle

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

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

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

Используя современные возможности TPL мы можем подмешать в интерфейс IResponseModel метод GetAwaiter и реализовать всё в асинхронном стиле, используя такие наипростейшие конструкции:

SomeResponseModel model = await _Server.Get(request);

Здесь ключевое слово await не блокирует поток, а создает продолжение, к которому поток вернется, когда модель будет получена. То есть, мы можем отправить запрос в базу, письмо SMTP-серверу не останавливаясь в ожидании их ответа. И продолжать выполнение других важных задач.

Если завтра мы захотим реализовать в объектном сервере паттерн Enterprise Service Bus, то и это возможно. Послезавтра наши SQL выборки могут трансформироваться в Map-Reduce. Практически мы не ограничены в выборе возможностей.

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

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

Риски

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

public class DbCreateObjectRequest<TKey, TModel> : IRequestHandler<TModel, TKey> { IResponseModel<TKey> Process(IRequestModel<TModel> request) { /* Осуществляем передачу модели в базу, руководствуясь нашими конвенциями. Например, из имени типа TModel с суффиксом «_Create» формируется имя хранимой процедуры (как пример Account_Create). А свойства модели передаются в качестве одноименных параметров этой хранимой процедуре с префиксом «@@» (например, @@Mail, @@FirstName и другие). */ ... return objectId; } }

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

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

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