В один прекрасный обыкновенный четверг в одной команде разработчиков появились разногласия по поводу некоторых архитектурных решений, реализация которых была утверждена приказом сверху, а не родилась в ходе аргументированного спора. По прошествии некоторого времени подобный спор возник опять, уже на новом проекте. Дискуссия становилась все жарче, и для прояснения ситуации и достижения просветления даже были привлечены сторонние высшие силы. Последнее достигнуто не было, и все же, тем же волевым решение, был принят один из вариантов развития бытия. Но мудрый Каа один из участников обсуждения решил не оставлять в команде неразрешенных споров — почвы для возникновения конфликтов в будущем, раскола команды и другого мордобития, и предложил все решить всеобщей пьянкой составлением данного документа, который поможет членам команды достичь просветления и снова стать мягкими и пушистыми.
В данном документе мне было предложено описать преимущества и недостатки двух подходов, достичь консенсуса и воцарить мир и справедливость во Вселенной.
Ниже я и попытаюсь в меру своих интеллектуальных возможностей это сделать (поэтому буду использовать очень простые слова и выражения) и вынести на суд кровавых мясников почтенной публики.
Волевой путь, ведущий в бездну достижения нирваны
Имеем набор сервисов, которые занимаются обработкой данных, используют репозитории для хранения данных. Т.к. репозитории зачастую источники медленные, то некоторые сервисы используют временные хранилища, такие как Session и Cache. А т.к. сервисы используются не только из web-приложений, то напрямую эти временные хранилища использовать нельзя (например, консольное приложение не имеет сессии вообще), поэтому работу с ними реализуют тоже сервисы. (То есть нет четкого понятия, что такое сервис в нашей архитектуре и его высшее предназначение не обозначено. По сути дела, при таком подходе — сервис это класс, который что-то делает)
рис.1 Иерархия классов первого подхода
Пример реализации всего этого счастья можно скачать на Git Hub
Что мне здесь не нравится:
Во-первых, мне не нравится отсутствие четких соглашений о делегировании полномочий: и то сервис, и другое, а занимаются совсем разными вещами. Один занимается обработкой данных, другой их временным хранением.
Во-вторых, мне не нравится класс SessionDataServiceBase, а именно метод T Get(string key, Func getData), который не только работает с сессией, но и занимается обработкой данных. Предполагается по названию ISessionDataService (да и вообще как это изначально задумывалось), что он является только оберткой над Session и максимум, что может, так это возвращать значение по умолчанию. Если обратится к истории развития этого объекта, то изначально метод T Get(string key, Func getData) (или его аналог) не был описан в интерфейсе. Он был реализован в каждом классе, который использовал реализацию данного интерфейса (и с одной стороны, по моему мнению — это правильно). У данного подхода был минус — это повторяемость кода. Это было мною замечено, когда в одном из сервисов я решил добавить использование сессии. После краткого исследования было обнаружено большое количество дублированного кода, точнее, это дублирование было во ВСЕХ сервисах. Это нарушило мою целостность восприятия вселенной и не позволило жить мне в гармонии с ней. Делегировать сессионному сервису какую-то дополнительную работу с данными мне показалось неправильным, и было принято решение вынести это в какой-то базовый класс, чтобы убрать этот диссонанс из моей души (подробности в описании второго пути к истине).
Третья проблема, которая мне была явлена Господом нашим Богом, его святейшеством двоичным кодом — это ключи. Им присваивали значения (не побоюсь признаться, и за мной одно время был грех) в стиле “кто в лес, кто по дрова”. И метод void Clear(string[] sessionKeyPrefix) давал иногда совершенно неожиданные результаты. Были и другие проблемы, но мы их не будем касаться, решение этих проблем тоже давало грамотное наследование.
Проблема четвертая — тестирование. После перевода волевым решением на данную структуру, все юнит-тесты методов использующих под капотом T Get(string key, Func getData) упали. Мокнуть я их по быстрому не смог, даже с помощью нашего гуру юнит-тестирования, и мне было предложено на них вообще забить, что не есть хорошо на мой взгляд.
А теперь о преимуществах — о них мы не можем ничего сказать, то есть, в смысле я, потому что я за второй путь, исполненный благочестия и совершенства, ведущий в совершеннейшую нирвану, при которой код преобразуется напрямую в двоичные кода, минуя унизительный и скучный процесс преобразования в CIL.
Путь осмеянный современниками, как и все истинно великие вещи
Этот подход основывается прежде всего на соглашениях и ограничениях. Предполагается, что сервис — это класс, который обрабатывает полученные данные и сохраняет результаты этой обработки в постоянный хранилищах, с которыми работает через репозиторий, причем репозиторий у него один, и сервис ничего не знает о том, как он хранит данные. Ему все равно, база данных, файл, сторонний сервис на просторах интернета, etс.
По-хорошему, тут было бы правильно сделать какой-то базовый класс или интерфейс, который бы показывал, что этот класс отвечает именно за обработку и пересылку данных между хранилищем и конечным потребителем. Но, к сожалению, это не сделано, и, как видим в первом подходе, у нас появляется класс (HttpContextBasedSessionDataService), названный сервисом, но не имеющий репозитория, и не обрабатывающий данные. Поэтому, будем предполагать, что мы все же имеем базовую сущность для сервисов, будет она интерфейсом IService
Теперь о HttpContextBasedSessionDataService и подобный ему классах. Появился он по причине того, что репозитории — медленные источники данных, во-первых, и их нужно использовать как можно реже, так как это всегда узкое место, во-вторых. Поэтому, не плохо бы некоторые данные хранить под рукой — и тут появляется новый вид классов, они не обрабатывают данные, не имеют репозитория, всего лишь обеспечивают доступ ко временным хранилищам. В приципе, это ближе к репозиториям, чем к сервисам, только репозиториям временного хранилища, таких, например, как Application, Cache, Session. Назовем базовую сущность IShorttermStore и примем то, что в названиях подобных классов не будет упоминаться слово Service.
А теперь попробуем на основании этих выкладок построить такую иерархию классов, которая устранит недостатки предыдущей реализации, и вынесем часть функциональности по логике работы с временными хранилищами в базовый сервис класс, а именно — работу с ключами и логику ленивой инициализации.
И вот теперь взглянем на диаграмму классов, построенной согласно этой концепции:
рис.2 Иерархия классов второго подхода
Пример реализации всего этого счастья можно скачать на Git Hub
Здесь устранены недостатки, которыми, по моему мнению, грешит первый подход. Юнит-тесты работают без каких-то дополнительных моков.
Теперь о мнимых недостатках.
Если вспомнить аргументы спорщиков, то тут есть недостаток в том, что если класс хочет также использовать кроме Session так же и Cache, то возникают проблемы. Но на самом деле проблем в этом нет: если возникла такая потребность, то может стоит подумать о SOLID, в частности о букве I в этой аббревиатуре (Interface segregation principle) и не делать монстров, способных “и вышивать, и на машинке тоже”.
Больше о недостатках я ничего пока не могу сказать, так как память избирательна и запоминает только светлое и хорошее, а вовсе не критику. Прошу начать разбивать меня в пух и прах, а то что-то очень все хорошо получается.
Михайличенко Алексей, Software .Net Developer, Tech Lead
Автор: Zfort Group