В один прекрасный обыкновенный четверг в одной команде разработчиков появились разногласия по поводу некоторых архитектурных решений, реализация которых была утверждена приказом сверху, а не родилась в ходе аргументированного спора. По прошествии некоторого времени подобный спор возник опять, уже на новом проекте. Дискуссия становилась все жарче, и для прояснения ситуации и достижения просветления даже были привлечены сторонние высшие силы. Последнее достигнуто не было, и все же, тем же волевым решение, был принят один из вариантов развития бытия. Но мудрый Каа один из участников обсуждения решил не оставлять в команде неразрешенных споров — почвы для возникновения конфликтов в будущем, раскола команды и другого мордобития, и предложил все решить всеобщей пьянкой составлением данного документа, который поможет членам команды достичь просветления и снова стать мягкими и пушистыми.
В данном документе мне было предложено описать преимущества и недостатки двух подходов, достичь консенсуса и воцарить мир и справедливость во Вселенной.
Ниже я и попытаюсь в меру своих интеллектуальных возможностей это сделать (поэтому буду использовать очень простые слова и выражения) и вынести на судкровавых мясников почтенной публики.
Волевой путь, ведущий в бездну достижения нирваны
Имеем набор сервисов, которые занимаются обработкой данных, используют репозитории для хранения данных. Т.к. репозитории зачастую источники медленные, то некоторые сервисы используют временные хранилища, такие как 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
В данном документе мне было предложено описать преимущества и недостатки двух подходов, достичь консенсуса и воцарить мир и справедливость во Вселенной.
Ниже я и попытаюсь в меру своих интеллектуальных возможностей это сделать (поэтому буду использовать очень простые слова и выражения) и вынести на суд
Волевой путь
Имеем набор сервисов, которые занимаются обработкой данных, используют репозитории для хранения данных. Т.к. репозитории зачастую источники медленные, то некоторые сервисы используют временные хранилища, такие как 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