Принцип единственной ответственности в ASP .NET Core MVC
В этой статье поговорим про одну из важных составляющих ООП в целом и приложений на C# в частности - про принцип единственной ответственности.
Принцип единственной ответственности гласит, что каждый класс должен иметь одну четко определенную задачу. Рассмотрим на примере, где этот принцип не соблюдается:
Вроде в коде все нормально, а что не так?
А не так в нем то, что контроллер, который отвечает за взаимодействие передаваемых из фронта (интерфейса сайта) или api данных и другими компонентами приложения, в данном случае, выполняет много дополнительных, не свойственных действий.
То есть, контроллер - некий диспетчер. Принял данные, отдал их на обработку, получил результат обработки и отдал обратно во фронтенд или api. Он не производит вычислений, формирований строк и т.д. И контроллер не должен знать о внутреннем устройстве приложения дальше, чем на один шаг. Соответственно, он никак не может взаимодействовать напрямую с базой данных, в данном случае с контекстом.
Упрощенная схема взаимодействия выглядит так:
Контроллер "общается" только с сервисами. Сервисы взаимодействуют с контроллером, другими сервисами и репозиториями. Репозитории взаимодействуют только с базой данных и сервисами их использующими.
- четко упорядочить структуру приложения;
- упростить тестирование;
- определить задачи для каждого компонента приложения;
Попробуем провести аналогию с реальным миром. Представим, что у нас есть автомобиль, в котором так же есть холодильник. И для того, чтобы приготовить еду нам нужно идти к машине. Каждый раз. А если сломается машина, мы отвезем ее в сервис. Вместе с холодильником. Не очень удобно, не так ли? В данном примере автомобиль выполняет две функции - свою непосредственную по перемещению владельца из точки А в точку Б и вторую, несвойственную ему функцию хранения продуктов.
Именно для того, чтобы пользоваться каждой функцией полноценно и не зависеть от других компонентов, нужно разделять их ответственность (читай функционал).
Теперь рассмотрим первоначальный пример, но уже с разделением ответственности:
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 операций, в сервисе вызываются методы репозитория и сервиса отправки электронной почты, и отдаются данные в контроллер , а контроллер занимается передачей запросов и возвратом результата в пользовательский интерфейс.
В таком подходе может быть больший по объему код, но он лучше тестируется, каждый компонент выполняет свою роль, а код в целом менее связан.
Таким образом, соблюдения принципа единственной ответственности делает код более читаемым, легко поддерживаемым, обновляемым и тестируемым. Пишите чистый код, до встречи!