.NET
September 12, 2024

Принцип единственной ответственности в ASP .NET Core MVC

В этой статье поговорим про одну из важных составляющих ООП в целом и приложений на C# в частности - про принцип единственной ответственности.

Принцип единственной ответственности гласит, что каждый класс должен иметь одну четко определенную задачу. Рассмотрим на примере, где этот принцип не соблюдается:

Пример кода, где не соблюдается принцип единой ответственности.

Вроде в коде все нормально, а что не так?

А не так в нем то, что контроллер, который отвечает за взаимодействие передаваемых из фронта (интерфейса сайта) или api данных и другими компонентами приложения, в данном случае, выполняет много дополнительных, не свойственных действий.

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

Упрощенная схема взаимодействия выглядит так:

Схема взаимодействия классов в приложении ASP .Net Core

Контроллер "общается" только с сервисами. Сервисы взаимодействуют с контроллером, другими сервисами и репозиториями. Репозитории взаимодействуют только с базой данных и сервисами их использующими.

Такой подход позволяет:

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

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

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

Теперь рассмотрим первоначальный пример, но уже с разделением ответственности:

public class UserController : Controller
{
    private readonly IUserService _userService;

    public UserController(IUserService userService)
    {
        _userService = userService;
    }

    public IActionResult Index()
    {
        var users = _userService.GetUsers();
        return View(users);
    }

    public IActionResult Create(string fullName, int age, string email)
    {
        if (!ModelState.IsValid)
        {
            return View();
        }

        _userService.CreateUser(fullName, age, email);
        return RedirectToAction("Index");
    }

    public IActionResult Delete(int id)
    {
        _userService.DeleteUser(id);
        return RedirectToAction("Index");
    }
}

public interface IUserService
{
    IEnumerable<User> GetAll();
    void Create(string fullName, int age);
    void Delete(int id);
}

public class UserService : IUserService
{
    private readonly UserRepository _repository;
    private readonly EmailService _emailService;

    public UserService(UserRepository repository, EmailService emailService)
    {
        _repository = repository;
        _emailService = emailService;
    }

    public IEnumerable<User> GetAll()
    {
        return _repository.GetAll();
    }

    public void Create(string fullName, int age, string email)
    {
        var user = new User(fullName, age, email)
        
        _repository.Save(user);

        _emailService.SendWelcomeEmail(email);
    }

    public void Delete(int id)
    {
        var email = _repository.Get(id).Email;
        
        _repository.Delete(id);

        _emailService.SendGoodByeEmail(email);
    }
}

public class UserRepository
{
    private readonly DbContext _dbContext;

    public UserRepository(DbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public IEnumerable<User> GetAll()
    {
        return _dbContext.Users.ToList();
    }

    public User Get(int id)
    {
        return _dbContext.Users.Where(u => u.id == id).First();
    }

    public void Save(User user)
    {
        _dbContext.Users.Add(user);
        _dbContext.SaveChanges();
    }

    public void Delete(int id)
    {
        var user = _dbContext.Users.Find(id);
        if (user != null)
        {
            _dbContext.Users.Remove(user);
            _dbContext.SaveChanges();
        }
    }
}

public class EmailService
{
    public void SendWelcomeEmail(string email)
    {
        // код отправки электронной почты
    }

    public void SendGoodByeEmail(string email)
    {
        // код отправки электронной почты
    }
}

Теперь в репозитории происходит только выполнение CRUD операций, в сервисе вызываются методы репозитория и сервиса отправки электронной почты, и отдаются данные в контроллер , а контроллер занимается передачей запросов и возвратом результата в пользовательский интерфейс.

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

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