Инструменты, которыми мы располагаем как .NET-разработчики, поистине впечатляют. Вы можете позволить себе выбирать наиболее подходящий из многочисленных фреймворков пользовательского интерфейса, таких как UWP, WPF, Windows Forms, и различных фреймворков пользовательского интерфейса для веб-клиентов, среди которых можно выделить Angular и React. И у нас даже есть несколько вариантов взаимодействия с базами данных, таких как Entity Framework, Dapper и т.д.

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

В этом и заключается предназначение опенсорсного фреймворка CSLA .NET, распространяемого под лицензией MIT: обеспечить первоклассную поддержку бизнес-логики, которая с большой долей вероятности раскидана по слоям пользовательского интерфейса и доступа к данным. CSLA появился в середине 1990-х годов в виде freeware фреймворка, поддерживающего DCOM, и в том контексте freeware проекта в 2002 году я адаптировал его под .NET. В наши дни “freeware” трансформировалось в опенсорс, и много лет назад оригинальная лицензия была изменена на общепринятую лицензию MIT. Вы можете найти CSLA .NET на официальном сайте https://cslanet.com или на GitHub.

Архитектура приложения с CSLA .NET 

CSLA .NET предназначен для внедрения в проект определенной архитектуры, которую вы можете наблюдать на Рисунке 1. Эта архитектура имеет четкое разделение между интерфейсом и слоями, предназначенными для управления этим интерфейсом. Как правило, речь идет о HTML или JSON и контроллерах или моделях представления. Она также имеет четкое разделение между слоями доступа к данным и слоями хранения данных, в качестве которых зачастую выступают Entity Framework или ADO.NET и SQL Server. Частью архитектуры, на которой фокусируется CSLA .NET, является слой бизнес-логики в самом центре.

Рисунок 1: Многослойная архитектура CSLA .NET
Рисунок 1: Многослойная архитектура CSLA .NET

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

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

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

CSLA использует другой подход, превращая модель во вместилище для бизнес-логики. Это особенно целесообразно при использовании .NET, поскольку CSLA также делает вашу модель привязываемой (bindable) к каждому фреймворку пользовательского интерфейса, поддерживаемому .NET (от Windows Forms с ASP.NET до Blazor с WebAssembly). Если ваша модель поддерживает привязку данных, а также инкапсулирует всю вашу бизнес-логику, вы не можете не отметить тот факт, что благодаря этому создавать высокоинтерактивные пользовательские интерфейсы, сохраняя при этом полное разделение ответственности между слоями представления и бизнес-логики, намного легче.

Также CSLA четко регламентирует, где и как вы должны вызывать свой слой доступа к данным. Опять же, замысел этого заключается в том, чтобы помочь вам сохранить разделение ответственности путем формализации самих концепций слоев бизнес-логики и доступа к данным. CSLA не заботит, какие технологии вы используете для взаимодействия с базами данных, поэтому вы можете использовать Dapper, Entity Framework, читсый ADO.NET или любые другие технологии, которые вам подходят. Важно то, что CSLA помогает хранить код доступа к данным в отдельном слое, обособленном от слоев бизнес-логики и представления.

Пример простого Blazor-приложения 

Лучший способ продемонстрировать вам CSLA — это создать приложение, а самый современный тип технологии пользовательского интерфейса на данный момент — это Blazor, использующий WebAssembly для запуска .NET на любом современном браузере. На момент написания статьи для создания этого приложения требуется Visual Studio 2019 Preview и .NET Core 3.1 Preview. Вы можете найти код самой последней версии этого примера в GitHub-репозитории CSLA .NET.

Сразу оговорюсь, что эти же концепции архитектуры, кода бизнес-логики и доступа к данным будут применимы без каких-либо изменений к любому другому фреймворку пользовательского интерфейса .NET, включая ASP.NET Razor Pages, MVC, WPF, Xamarin и другие. Каталог /Samples в GitHub-репозитории CSLA также содержит примеры для вышеперечисленных фреймворков пользовательского интерфейса.

Структура решения

Большинство клиентских Blazor-приложений включают клиентский проект (.Client), который компилируется в WebAssembly, и этот код запускается в браузере на клиентском устройстве. А также у них есть серверный проект (.Server), который используется для развертывания кода на клиенте и предоставления конечных точек. Также у них есть общий проект (.Shared), содержащий код, который вам нужно развертывать как на клиенте, так и на сервере. Типичная структура решения (solution) представлена на рисунке 2.

Фигура 2: Решение Blazor и CSLA в Visual Studio
Фигура 2: Решение Blazor и CSLA в Visual Studio

Это та же самая модель CSLA, которая использовалась .NET на протяжении более 23 лет, и поэтому она идеально подходит для работы с Blazor. Я всегда рекомендую Class Library проект, который создает DLL-библиотеку, содержащую бизнес-логику, с развертыванием этой сборки на клиенте и сервере.

Также обратите внимание, что в этом решении есть DataAccess-проект, помогающий поддерживать разделение ответственности между слоем бизнес-логики (проект BlazorExample.Shared) и кодом, используемым для взаимодействия с хранилищем данных. В этом примере хранилище данных — мок in-memory базы данных, реализованный с помощью коллекций, но с таким же успехом этом может быть SQL Server, доступ к которому осуществляется через Dapper. В этой статье я не буду вдаваться в подробности относительно слоя доступа к данным, но покажу, как и где он вызывается.

Слой бизнес-логики

Проект BlazorExample.Shared содержит три класса бизнес-логики: PersonList, PersonInfo и PersonEdit. Классы PersonList и PersonInfo нужны, чтобы предоставить доступный только для чтения список сведений о людях, доступных пользователю. Оба они используются страницей ListPersons.razor. Класс PersonEdit предоставляет свойства доступные для чтения и записи и инкапсулирует бизнес-правила, необходимые для того, чтобы пользователь мог редактировать информацию о человеке. Он используется страницей EditPerson.razor.

В нашем проекте есть также несколько классов-правил, реализующих бизнес-правила: InfoText, CheckCase, LetterCount и NoZAllowed. Одной из наиболее важных фич CSLA является его обработчик правил, который обрабатывает правила из пространства имен System.ComponentModel.DataAnnotations, а также правила, реализованные в виде классов.

Поскольку проект BlazorExample.Shared использует CSLA, он ссылается на NuGet-пакет Csla.

Класс PersonInfo очень прост - он предоставляя свойства доступные только для чтения посредством модели объявления свойств CSLA, как показано в Листинге 1.

Листинг 1: Свойство Name с доступом только для чтения

public static readonly  PropertyInfo<string> NameProperty = RegisterProperty<string>(nameof(Name));

[Display(Name = "Person name")]
public string Name
{
    get { return GetProperty(NameProperty); }
    private set { LoadProperty(NameProperty, value); }
}

В CSLA есть очень четкие предписания касательно объявления свойств, поэтому все они следуют одному и тому же шаблону. Это повышает удобочитаемость и сопровождаемость кода, а также способствует замыслу о том, что бизнес-правила должны быть реализованы с использованием обработчика правил CSLA, а не случайного кода в сеттера свойств. Обратите внимание на использование атрибута Display, который нужен для предоставления более емкого имени свойства. Это хороший пример того, как CSLA поддерживает использование атрибутов System.ComponentModel.DataAnnotations.

Более интересным является класс PersonEdit, поскольку он реализует свойства доступные для чтения-записи, к которым и присоединяются бизнес-правила. Например, в следующем фрагменте кода показано свойство PersonEdit:

public static readonly PropertyInfo<string> NameProperty = RegisterProperty<string>(nameof(Name));

[Display(Name = "Person name")]
[Required]
public string Name
{
    get { return GetProperty(NameProperty); }
    set { SetProperty(NameProperty, value); }
}

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

Правила присоединяются к свойствам в методе AddBusinessRules, как показано в следующем фрагменте кода:

protected override void AddBusinessRules()
{
    base.AddBusinessRules();
    BusinessRules.AddRule(new InfoText(NameProperty, "Person name (required)"));
    BusinessRules.AddRule(new CheckCase(NameProperty));
    BusinessRules.AddRule(new NoZAllowed(NameProperty));
    BusinessRules.AddRule(new LetterCount(NameProperty, NameLengthProperty));
}

Как видите, правило прикрепляется к свойству, посредством создания инстанса правила и указания статического дескриптора PropertyInfo в качестве параметра. Для некоторых правил могут потребоваться и другие параметры.

Некоторые правила способны предоставлять пользователю определенную информацию, предупреждения или сообщения об ошибках. Слой сообщений об ошибках, как, например, для атрибута Required, также могут препятствовать сохранению объекта слоя бизнес-логики, если объект считается невалидным. Листинг 2 показывает, как можно легко реализовать правило с предупреждением. Классы правил должны реализовать интерфейс IBusinessRule или являться подклассами базового класса BusinessRule. В обоих случаях они реализуют метод Execute или ExecuteAsync, содержащий правило.

Листинг 2: Правило, возвращающее сообщение с предупреждением 

protected override void Execute(IRuleContext context)
{
    var text = (string)ReadProperty(context.Target, PrimaryProperty);
    if (string.IsNullOrWhiteSpace(text)) return;
    var ideal = text.Substring(0, 1).ToUpper();
    ideal += text.Substring(1).ToLower();
    if (text != ideal)
        context.AddWarningResult("Check capitalization");
}

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

Но правила нужны не только для проверок; они также могут манипулировать значениями свойств. Листинг 3 показывает простое правило, которое подсчитывает количество символов в одном строковом свойстве и присваивает результат другому целочисленному свойству.

Листинг 3: Реализация правила LetterCount

protected override void Execute(IRuleContext context)
{
    var text = (string)ReadProperty(context.Target, PrimaryProperty);
    var count = text.Length;
    context.AddOutValue(AffectedProperties[1], count);
}

Это правило также считывает значение свойства первого свойства (PrimaryProperty), а затем запрашивает обработчик правил CSLA установить значение другого свойства (AffectedProperties[1]) равным длине первого свойства.

Но правила нужны не только для проверок; они также могут манипулировать значениями свойств.

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

Взаимодействие со слоем доступа к данным

Еще один очень важный аспект касательно слоя бизнес-логики заключается в том, что он опирается на CSLA для формализации того, как и когда вызывать слой доступа к данным. Например, в Листинге 4 показано, как класс PersonEdit реализует вызываемый CSLA метод для получения данных из слоя доступа к данным.

Листинг 4: Метод Fetch класса PersonEdit

[Fetch]
private void Fetch(int id, [Inject]DataAccess.IPersonDal dal)
{
    var data = dal.Get(id);
    using (BypassPropertyChecks)
        Csla.Data.DataMapper.Map(data, this);
    BusinessRules.CheckRules();
}

Фреймворк CSLA управляет жизненным циклом доменных объектов и по мере необходимости вызывает методы с атрибутами Create, Fetch, Insert, Update и Delete (среди прочих). На вас же остается ответственность за реализацию этих методов для взаимодействия с вашим слоем доступа к данным и выполнения запрашиваемых действий. Обратите внимание на то, что в этом примере ссылка на слой доступа к данным предоставляется через внедрение зависимостей, как определено в стандартном файле приложения Startup.cs.

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

Теперь давайте посмотрим, как эти классы бизнес-логики используются в контексте создания пользовательского интерфейса Blazor, уделив особое внимание их поддержке привязки данных.

Слои интерфейса и управления интерфейсом

Наш проект BlazorExample.Client реализует клиентское приложение Blazor, которое может быть запущено любым современным браузером, включая Chrome, Firefox, Safari и Edge; на персональных компьютерах, ноутбуках, планшетах и ​​телефонах. Поскольку приложения Blazor создаются с использованием .NET, сборка из BlazorExample.Shared может быть развернута на клиентском устройстве с целью создания интерактивного пользовательского опыта.

Приложения Blazor используют вариант синтаксиса Razor, который уже много лет используется ASP.NET MVC. Каждая страница Blazor определяется в файле с расширением .razor. Например, страница Pages/EditPerson.razor предоставляет пользовательский интерфейс для создания или редактирования человека на основе класса бизнес-логики PersonEdit. Следующий фрагмент кода показывает, как определяются пространства имен, внедрение зависимостей и навигация по страницам.

@page "/EditPerson"
@page "/EditPerson/{id}"
@using BlazorExample.Shared
@inject Csla.Blazor.ViewModel<PersonEdit> vm
@inject NavigationManager NavigationManager
@attribute [Authorize(Roles = "Admin")]

Хотя CSLA в первую очередь ориентирован на слой бизнес-логики, он также предоставляет некоторые вспомогательные типы для оптимизации взаимодействия с каждым типом фреймворка пользовательского интерфейса, доступного на .NET. Этот проект ссылается на NuGet-пакет Csla.Blazor и поэтому имеет доступ к этим вспомогательным типам, включая класс ViewModel, который можно внедрить на страницы при объявлении в стандартном файле Startup.cs. ViewModel абстрагирует общий повторяющийся код, который вам придется писать почти на каждой странице.

Страница использует синтаксис Razor для создания пользовательского интерфейса. Сюда входит использование привязки данных к ViewModel и PersonEdit. Листинг 5 демонстрирует, как свойство Name отображается для пользователя с помощью компонента Blazor TextInput.

Листинг 5: Привязка к свойству Name из объекта PersonEdit

@if (vm.GetPropertyInfo<string>(nameof(vm.Model.Name)).CanRead)
{
  <tr>
    <td>@(vm.GetPropertyInfo<string>(nameof(vm.Model.Name)).FriendlyName)</td>
    <td>
      <TextInput Property="@(vm.GetPropertyInfo<string>(nameof(vm.Model.Name)))" />
    </td>
  </tr>
}

Код в Листинге 5 иллюстрирует, как разделение ответственности между слоями представления и бизнес логики помогает создать гибкий пользовательский интерфейс. Обратите внимание, что этот блок кода отображается только в том случае, если текущий пользователь может считывать свойство, благодаря свойству CanRead, предоставляемому типом ViewModel. Здесь также используется свойство FriendlyName для отображения более емкого имени свойства из атрибута Display в классе бизнес-логики PersonEdit. А компонент TextInput предоставляет информацию о свойстве, которое пользователь может редактировать.

Компонент TextInput — это настраиваемый компонент пользовательского интерфейса, который я создал на основе модели компонентов Blazor. Он реализован в файле Shared/TextInput.razor, как показано в Листинге 6.

Листинг 6: Компонент пользовательского интерфейса TextInput Blazor

<div>
  <input @bind-value="Property.Value" @bind-value:event="oninput" disabled="@(!Property.CanWrite)" /><br />
  <span class="text-danger">@Property.ErrorText</span>
  <span class="text-warning">@Property.WarningText</span>
  <span class="text-info">@Property.InformationText</span>
</div>
 
@code {
  [Parameter]
  public Csla.Blazor.PropertyInfo<string> Property { get; set; }
}

Компоненты Blazor — это механизм, с помощью которого вы можете создавать реюзабельные элементы пользовательского интерфейса. Компоненту, приведенному выше, требуется в виде параметра объект Csla.Blazor.PropertyInfo, который он использует в качестве источника привязки для различных элементов пользовательского интерфейса в Razor-разметке.

В коде выше элемент ввода привязан к свойству Value с параметром @bind-value:event="oninput", который указывает, что базовая модель должна обновляться при каждом нажатии клавиши. Также интересно, что элемент ввода будет отключен, если текущий пользователь не авторизован для изменения значения свойства, путем проверки значения CanWrite, предоставленного CSLA. Наконец, вам нужно обратить внимание, как ошибка, предупреждение и информационный текст отображаются пользователю. Это значения, сгенерированные правилами, прикрепленными к свойству Name в классе бизнес-логики PersonEdit.

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

На странице EditPerson.razor есть еще одна деталь, на которую я хочу обратить ваше внимание, а именно то, как кнопка “Save” активируется только в том случае, если сам объект бизнес-логики может быть сохранен. Это продемонстрировано в Листинге 7.

Как и в предыдущем фрагменте кода, вы можете видеть, как свойство disabled элемента определяется на основе свойства IsSavable, предоставленного CSLA. По умолчанию CSLA считает объект бизнес-логики “сохраняемым”, если не нарушены никакие правила проверок на ошибки, объект был изменен и текущий пользователь имеет право на сохранение объекта.

Листинг 7: Автоматическое отключение кнопки сохранения

<button @onclick="vm.SaveAsync" disabled="@(!vm.Model.IsSavable)">Save person</button>

Рисунок 3 — это пример рабочего Blazor-приложения, где я ввел значение имени, которое триггерит все правила. Обратите внимание, что кнопка “Save” отключена, потому что свойство Name в настоящее время нарушает правило проверки на ошибку.

Рисунок 3: Страница EditPerson с нарушенными проверочными правилами 
Рисунок 3: Страница EditPerson с нарушенными проверочными правилами 

Заключение

Цель CSLA .NET — предоставить превосходное вместилище для бизнес-логики, достойное первоклассного пользовательского интерфейса и возможностей доступа к данным, предоставляемым экосистемой .NET. Вы можете использовать CSLA для создания единообразной, поддерживаемой бизнес-логики, которая инкапсулирует проверку, авторизацию и алгоритмическую обработку. Полученная бизнес-сборка может работать везде, где можно запустить .NET, включая серверы Linux и Windows, контейнеры, Windows и мобильные клиенты. А с Blazor вы можете запускать свою бизнес-логику в любом современном браузере на любом устройстве.


Сегодня вечером пройдет открытое занятие «Принцип работы «Сборщика Мусора» в .NET». На этом уроке мы:
— разберемся с тем, как хранятся объекты в памяти в .NET;
— познакомимся с принципом выделения физической памяти для приложений;
— поймем принцип работы сборщика мусора (поколения, стратегии, карточный стол);
— начнем отличать деструкторы от финализаторов и научимся использовать Disposable Pattern.

Записаться можно на странице онлайн-курса «C# ASP.NET Core разработчик».

Комментарии (0)