Внедрение зависимостей в .Net Марка Симана 3 — Сквозные аспекты приложения, перехват, декоратор +3


Зависимости между слоями приложения | Внедрение конструктора, время жизни | Сквозные аспекты приложения, перехват, декоратор

В двух предыдущих заметках мы рассмотрели основные части веб-приложения. У нас есть объект реализующий бизнес логику – MyService. Есть IRepository, отвечающий за взаимодействие с БД. Не хватает ролевой модели и логирования.

Декоратор


Есть мнение, что в MVC веб-приложениях проверку прав удобно делать прямо в начале метода контроллера. Например:

[HttpPost]
public void DeleteProduct(int id) 
{
    if (!Thread.CurrentPrincipal.IsInRole("ProducManager")
        throw new UnauthorizedAccessException();

    this.MyService.DeleteProduct(id);
}

Листинг 1. Проверка прав в методе контроллера

или с помощью атрибута

[PrincipalPermission(SecurityAction.Demand,Role = "ProductManager")]
[HttpPost]
public void DeleteProduct(int id) 
{
    this.MyService.DeleteProduct(id);
}

Листинг 2. Проверка прав в методе контроллера с помощью атрибута

У такого подхода есть свои плюсы. Не самый очевидный из них это то, что проверка прав вынесена из MyService. Получается разделение ответственности: ядро бизнес логики выполняет только свою задачу и не занимается проверкой прав. Говоря «научно популярным» языком следует принципу единственной ответственности (Single Responsibility Principle, SOLID).

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

На помощь приходит паттерн «Декоратор».

Мы уже договорились, что

  • Программировать лучше в соответствии с интерфейсом (Dependency Inversion Principle, SOLID). Поэтому в контроллере используется не сам MyService а интерфейс.
  • Зависимости лучше внедрять через конструктор.

Код контроллера в Листинге 3 удовлетворяет обоим замечаниям:

public class MyController
{
    private readonly IMyService MyService;

    public class MyController(IMyService service)
    {
        if(service == null) 
            throw new ArgumentNullException(nameof(service));
        
        MyService = service
    }

    [HttpPost]
    public void DeleteProduct(int id) 
    {
        this.MyService.DeleteProduct(id);
    }
}

Листинг 3. MyController использует IMyService

Мы можем сделать декоратор MySecurityService: IMyService (листинг 4), в котором есть проверка прав. И, так как наш код следует принципу подстановки Лисков, использовать его в контролере. При этом нам не потребуется менять что-то в MyController.
Принцип подстановки Лисков (Liskov Substitution Principle из SOLID) (Лисков это фамилия)

Клиент должен рассматривать все реализации абстракции как эквивалентные. Мы должны иметь возможность заменять одну реализацию на другую, не разрушая потребителя.


class MySecurityService : IMyService
{
    private readonly IMyService MyService;
    private readonly IUserInfo UserInfo;
    public MySecurityService(IMyService service, IUserInfo userInfo)
    {
        if(service == null) 
            throw new ArgumentNullException(nameof(service));

        if(userInfo == null) 
            throw new ArgumentNullException(nameof(userInfo));
        
        MyService = service;
        UserInfo = userInfo;
    }

    public void DeleteProduct(int id) 
    {
        if(UserInfo.IsInRole("ProductManager"))
            throw new UnauthorizedAccessException(nameof(service));

        this.MyService.DeleteProduct(id);
    }
}

Листинг 4. Декоратор MySecurityService содержит только проверку прав и ничего не знает о реализации DeleteProduct.

или так, с атрибутом:

class MySecurityService : IMyService
{
    private readonly IMyService MyService;
    public MySecurityService(IMyService service)
    {
        if(service == null) 
            throw new ArgumentNullException(nameof(service));
        
        MyService = service;
    }

    [PrincipalPermission(SecurityAction.Demand,Role = "ProductManager")]
    public void DeleteProduct(int id) 
    {
        this.MyService.DeleteProduct(id);
    }
}

Листинг 5. Декоратор MySecurityService с использование атрибута

Обратите внимание, не стоит использовать PrincipalPermission атрибут прямо в MyService. Лучше если MyService не будет связан с конкретной реализацией проверки прав.

Таким же образом, с помощью декораторов, можно добавить логирование и обработку ошибок. Ниже пример (только для понимания) сборки полной функциональности: декораторы вкладываются друг в друга.

IMyService service = new MyService(...);
service = new MySecurityService(service, ...);
service = new MyLoggerService(service, ...);
service = new MyExceptionSaveService(service, ...);

Листинг 6. Расширение функционала с помощью декораторов. Гипотетический пример.

Отметим, что декораторы могут находиться в разных проектах (в разных сборках). Можно, например, иметь отдельные декораторы обработки ошибок для веб-приложения и мобильного клиента.

Отступление:

В литературе можно встретить термин «Перехват». Вот листинги 1 и 2 по сути реализуют “Перехват”: перехватывают управление, делают что-то и отдают управление основному коду. «Декоратор» тоже реализует «Перехват».

Части программы, которые распространяются на многие функции приложения, такие как логирование, безопасность, кеширование и т.п. называют “Аспектами” или «Сквозными аспектами».

Если на собеседовании вас спросят про логирование/кеширование/проверку прав, можно завернуть фразу типа: я считаю, что для реализации сквозных аспектов приложения лучше использовать перехваты, основанные на паттерне Декоратор. Конечно, для этого в коде должен соблюдаться принцип подстановки Барбары Лисков.

Заключение


Мы рассмотрели типичные задачи разработки веб-приложения:

— Как разбить приложение на слои
— Как организовать взаимодействие между классами
— Как реализовать сквозные аспекты приложения

Сделали подводку к лучшему понимаю темы “внедрение зависимостей”. Зная зачем нужно использовать ВЗ подключение ioc-контейнера будет делом техники.




К сожалению, не доступен сервер mySQL