Архитектура приложений: концептуальные слои и договоренности по их использованию

Просто о NET | создано: 03.02.2017 | опубликовано: 03.02.2017 | обновлено: 13.01.2024 | просмотров: 10041

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

Пример договоренностей

Разберем для примера общеизвестный фреймворк ASP.NET MVC. Если вы работали с этой системой, то наверное обратили внимание на то, что в проекте, который создается Visual Studio по умолчанию присутствуют папки Models, Views, Controllers. Это первая договоренность, что каждый тип файла (класса) лежит в своей собственной папке.

В первых версиях фреймворка ASP.NET MVC контролеры могли лежать только в папке Controllers. Позже это ограничение было снято.

Eсли при разработки какого-либо метода контролера вы попробуете создать метод и открыть его, не создав представление (View) этого метода, то система выдаст вам сообщение об ошибке:

Обратите внимание на то, где система попыталась найти "потерянное" представление, прежде чем выдать сообщение. Такое поведение также предопределено договоренностями фреймворка. В данном случае, правило гласит, что если представление (View) не найдено в папке контролера Home, то следует ее поискать сначала в папке Shared, потом всё то же самое, но уже с другим расширением для другого движка рендеринга (при наличии подключенных нескольких движков рендеринга разметки). На самом деле договоренностей подобного рода в ASP.NET MVC очень большое количество. И тем больше вы их будете встречать, чем глубже будете погружаться в "дебри" фреймворка.

ASP.NET MVC фреймворк как и все фреймворки, во всяком случае в большинстве своем, построены на основе парадигмы "соглашения по конфигрурации" (configuration over convention).

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

Суть данной статьи показать, что использование упомянутой выше парадигмы применительно к коллективной разработке, может существенно ускорить (упростить) процесс разработки приложений, особенно если в разработке учавствует большое количество разработчиков. "Соглашения по конфигурации" позволяют решить вопросы, на которые зачастую тратятся драгоценные минуты, часы и даже дни и недели. Например, часто возникает вопрос, как правильно назвать создаваемый класс, свойство, метод, проект, переменную, решение (solution)? Сколько разработчиков, столько и вариантов, если не договорится заранее о правилах именования. А если в коллектив пришел новый разработчик, то как он может начать писать корректный код, не зная о правилах, по которым этот код пишется? Безусловно, существуют огромное количество вспомогательных утилит (например, StyleCop), которые упрощают задачу, но и они бывают бессильны в некоторых случаях.

Надо признать, что с каждым годом задачи для разработчиков усложняются в силу сложности задач, которые решает бизнес. Более сложные задачи требуют более комплексных подходов в их решении. Зачастую бывает недостаточно создать приложение (программу, систему), обычно требуется ее поддерживать и развивать после выпуска окончательной версии. И если в вашем коллективе существуют подобные правила и договоренности, вам будет гораздо проще найти место в коде, правильный контролер и метод. Договоренности и правила именования помогают быстрее ориентироваться в коде.

Немного истории

В качестве примера нарастания сложности, приведу эволюцию паттернов Business Logic, которые описывают, как и где дожна быть релизована бизнес-логика. Паттерны для организации бизнес-уровеня (бизнес-логика) с течением времени претерпели существенные изменения, от простого к сложному. Более того, на этом эволюция не остановилась. 

Каждый из них подробно описал Martin Fowler, поэтому я не буду на них останавливаться подробно. Эволюция говорит о том, что программировать системы, которые манипулируют данными становится всё сложнее. Надо понимать, что использование паттернов проектирования для построения архитектуры сложных систем существенно упрощает дальнейшую поддержку системы (maintainability) и введение нового функционала, но при этом разработка сильно усложняется. Для упрощения этой самой разработки, я и предлагаю использовать договоренности правила, то есть "соглашения по конфигурации" но применительно к процессу написания кода.

Что говорить, если уже не только среди разработчиков, но и среди заказчиков всё чаще и чаще слышатся подобные слова и фразы: ORM, Repository, Business Layer, Abstraction​ и Dependency Injection, MVVM и другие.

Среди разработчиков ходят давнишние споры относительно того, следует ли использовать дополнительную "прослойку" типа ORM между базами данных и DAL. Например, на sql.ru или на habrahabr.ru частенько поднимаются темы. Лейтмотивом в подобных спорах звучит мысль: "... для сложных проектов использование ORM существенно сокращает рутину...".

Так повелось, что с некоторого времени я перестал разделять проекты на сложные и простые, взяв за основу использование ORM для проектов любой сложности. Особенно если учесть, что ORM позволяет использовать подход CodeFirst (в частности, EntityFramework).

Осмелюсь предположить, что вы уже знакомы с паттернами проектирования, и в частности, с паттернами Repository и UnitOfWork, именно на их основе я и буду говорить о договоренностях для построения уровня доступа к данным (Data Access Layer). А также предположу, что вы имеете представление о Dependency Injection

Использование паттернов не дает гарантии, что ваш код будет написан правильно.

Первичные правила и договоренности по именованию

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

Не устаю повторять фразу "всё уже придумано за нас", повторю ее и в данном контексте. В мире разработки уже существуют договоренности об именовании, например от компания Microsoft (а также более "свежий" вариант C# Coding Conventions | Microsoft Docs) или те, другой вариант в Wikipedia. За основу для своих правил я взял договоренности от Microsoft. Итак, начнем.

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

Название любого проекта должно начинаться с {CompanyName}. Данное правило актуально для разработчиков компании {CompanyName}.

После первого слова {CompanyName} через точку должно быть указано имя проекта.

За названием проекта обязательно должна следовать название платформы.

После указанной платформы части внутренней архитектуры проекта.

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

X Запрещается использовать дефисы и другие символы.

X Запрещается использовать сокращения и общеизвестные аббревиатуры

X Запрещается использовать цифры в именованиях.

X Рекомендуется избегать использования сложно составных названий в указанных частях именования

Правила именования переменных:

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

Читабельность важнее краткости. Имя свойства CanScrollHorizontally лучше, чем ScrollableX (непрозрачными ссылка на ось x).

X Запрещается использовать знаки подчеркивания, дефисы и другие символы.

X Запрещается использовать венгерскую нотацию.

X ИЗБЕГАЙТЕ использования имен идентификаторов, конфликтующих с ключевыми словами широко используемых языков программирования.

X Не используйте аббревиатуры или сокращения как часть имени идентификатора. Например, используйте GetWindow вместо GetWin.

X Запрещается использовать акронимы, которые не являются общепринятым и даже в том случае, если они находятся, только при необходимости.

Используйте семантически значимые имена вместо зарезервированных слов языка для имен типов. Например GetLength является именем лучше, чем GetInt.

Используйте имя универсального типа CLR, а не имя конкретного языка, в редких случаях, когда идентификатор не имеет семантического значения за пределами своего типа. Например, преобразование в метод Int64 должен быть назван ToInt64, а не ToLong (поскольку Int64 — это имя среды CLR для C#-псевдоним long). В следующей таблице представлены некоторые базовые типы данных с помощью имен типов среды CLR (а также соответствующие имена типов для C#, Visual Basic и C++).

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

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

{CompanyName}.ProjectForManagingContent

{CompanyName}.Project.API.V1

{CompanyName}.ProjectForManaging

{CompanyName}.ManagingSystem

{COMPANYNAME}.WEB.PORTAL

MVC.Site.Utils.{CompanyName}

{CompanyName}.Client.System

Примеры именования пространства имен и проектов:

Для примера именованя решения возмем несуществующий сайт http://project.company.ru. Проект портала на платформе ASP.NET MVC должен иметь следующие пространство имен.

Название решения (solution):

{CompanyName}.Project

Названия проектов (projects) по типу принадлежности к уровню абстракции:

{CompanyName}.Project.Contracts

{CompanyName}.Project.Models

{CompanyName}.Project.Data

{CompanyName}.Project.Globalization

{CompanyName}.Project.Utils

{CompanyName}.Project.Android

{CompanyName}.Project.MacOS

{CompanyName}.Project.UWP

{CompanyName}.Project.WebAPI

{CompanyName}.Project.WPF

Названия проектов (projects) при использовании Unit-тестирования:

{CompanyName}.Project.Contracts.Tests

{CompanyName}.Project.Models.Tests

{CompanyName}.Project.Data.Tests

{CompanyName}.Project.Globalization.Tests

{CompanyName}.Project.Utils.Tests

{CompanyName}.Project.Android.Tests

{CompanyName}.Project.MacOS.Tests

{CompanyName}.Project.UWP.Tests

{CompanyName}.Project.API.Tests

{CompanyName}.Project.WPF.Tests

Абстрактные уровни Data Access Layer

Собственно говоря, мы подошли к ключевому моменту статьи. Я буду приводить примеры применительно к платформе ASP.NET и, в часности, к проекту MVC 5, однако, примите к сведению, что ничего не мешает использовать данную концепцию в WPF-приложении или в каком-либо еще, например, JavaScript-приложения на основе Single Page Application (SPA).

Итак, в своих проектов я использую следующие уровни абстракции для уровня бизнес-логики:

Это базовые понятия для большинства проектов, но надо признаться, что были проекты, для которых приходилось добавлять уровни выше над уровнем Manager. Теперь обо всем по порядку. Давайте для пущей наглядности возьмем конкретные сущности и сделаем для них обёртку из классов для бизнес-уровня. Предположим, что у нас в проекте есть сущности Category, Post, Tag, Comment. Похоже на сущности для реализации блога? Так оно и есть.

Repository

Надеюсь, вы прочитали описание на картинке для Repository​, лишь поясню, что базовые операции это: чтение из базы списка, чтение из базы одной сущности по идентификатору, вставка новой записи в базу, редактирование существующей в базе записи, удаление записи. Некоторые Repository могут иметь дополнительные методы, например, метод Clear для репозитория LogRepository. Итак, для каждой из указанных сущностей создаем свой репозиторий: CategoryRepository, PostRepository, TagRepository, CommentRepository​.

Правило именования для Repository: название сущности в единственном числе перед словом Repository.

У меня есть ReadableRepositoryBase<T> класс, который реализует методы PagedList<T>() и GetById(). Таком образом, унаследовавшись от этого класса, репозиторий наследник приобретает возможность получать запись по идентификатору и коллекцию разбитую на страницы. А также если наследник от ReadableRepositoryBase<T>, который называется WritableRepositoryBase<T>​ и имеет методы Create, Update, Delete. Конкретную реализацию я приводить не буду, ибо это не есть цель моей статьи, но если кому-то потребуется, отпишитесь в комментариях.

Такой подход дает мне возможность наследоваться от одного или от второго абстрактного класса, реализуя ролевой доступ к сущности на этапе разработки. Итак, у нас уже есть четыре класса, которые могут управлять (CRUD) каждый своей сущностью.

Provider

Далее в иерархии уровней бизнес-логики доступа к данным идет Provider. Говоря о нем, приведу пример взаимосвязи между сущностями Tag​ и Post​. Допустим, что метка (tag) связана с записью блога (post) в отношении "многое-ко-многим" и метки для записи блога обязательны. Так вот, получается, что при добавлении записи блога, мне придется каким-то образом обрабатывать метки (tags). Данный функционал подпадает под паттерн Unit of Work, который управляет централизовано несколькими сущностями. Таким образом, PostProvider​ частично реализует паттерн Unit of Work.

Правило именования для Provider: название сущности указывается в единственном числе перед словом Provider.

Провайдеры через вливание зависимостей принимают в конструктор Repository. То есть провайдеры управляют репозиториями. Таким образом, мы подошли к первому правилу Dependency Injection для уровней Data Access Layer.

Правило Dependency Injection: В конструкторы классов Provider могут вливаться как зависимости объекты Repository.

Забыл упомянуть, что в классы Repository должны вливаться DbContext (EntityFramework) или его абстрактный представитель. 

Правило Dependency Injection: В классы Repository могут вливаться DbContext (EntityFramework) или абстракция на этот класс.

Manager

Следующим на очереди уровень Manager. Менеджеры служат для управления связанными сущностями (в том числе и провайдерами), но на более высоком уровне абстракции. Допустим, что мне нужно изменить видимость категории, которую я хочу скрыть. Но скрывая категорию, я должен скрыть и записи в этой категории. Решая поставленную задачу, я создам класс CategoryManager​, в котором создам метод SetVisibilityForCategory. В классе PostProvider я создам метод SetVisibilityForPostsByCategoryId(int categoryId)​, который будет использовать класс PostRepository устанавливать свойство IsVisisble​ у всех записей блога принадлежащий выбранной категории. То есть, просто изменять значение одного свойства, а это обычная операция обновления (update).

Правило Dependency Injection: В классы Manager могут вливаться как зависимости объекты Provider.

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

Controller

Говоря об ASP.NET MVC нельзя не упомянуть о контролерах, которые являются основой в фреймворке, вокруг которых и построена вся инфраструктура. В MVC-контролер (API-контроллер тоже) могут вливаться зависимости типа Repository, Provider, Manager.

Правило Dependency Injection: В классы Manager могут также вливаться как зависимости объекты Repository.

Хочу заметить, что в большестве случаев, операции, которые вы будете вызывать из контролера должны находитcя на уровне Provider и Manager, потому что эти два уровня совместно реализуют Unit of Work, а операции из Repository зачастую не участвуют в бизнес-процессах "большого" приложения.

Правило именования для Controller: название сущности указывается во множественном числе перед словом Controller.

Service

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

Правило Dependency Injection: В классы Repository, Provider, Manager могут вливаться объекты Service.

Таким образом, для каждого из перечисленных уровней (Repository, Provider, Manager) может возникнуть потребность инъекции какого либо сервиса. Хорошим примером может служит LogService. Протоколирование (logging) уместно на любом из уровней логики. В Repository запись в журнал о том, что запись создана. Provider может записать данные о добавленнии новых меток при добавлении записи в блог. Manager может записать информацию о том, что при сокрытии категории, также скрыты и все записи в этой категории. В общем, всё хорошо в меру.

Заключение

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

Стоит также отметить, что три уровня абстракции дают широкие возможности для выбора места реализации методов бизнес-процессов, так сказать, распределенная бизнес-логика. А вот решение на каком уровне реализовывать конкретный бизнес-процесс вам уже предстоит решить самостоятельно, ибо всё зависит от задач, которые вы перед собой ставите. Но в крайнем случае можно спросить и у архитектора системы. :)

Видео по этой статье доступно на канале в youtube, а также доступна статья.

Ссылки