воскресенье, 14 февраля 2016 г.

Принцип единственности ответственности


Принцип единственности ответственности

Формулировка: не должно быть больше одной причины для изменения класса

Что является причиной изменения логики работы класса? Видимо, изменение отношений между классами, введение новых требований или отмена старых. Вообще, вопрос о причине этих изменений лежит в плоскости ответвенности, которую мы возложили на наш класс. Если у объекта много ответвенности, то и меняться он будет очень часто. Таким образом, если класс имеет больше одной ответственности, то это ведет к хрупкости дизайна и ошибкам в неожиданных местах при изменениях кода.



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

1. Active Record
Проблема
Суть MyGeneration (ORM) состоит в том, что по таблицам базы данных, она генерирует бизнес-сущности. Возьмем для примера сущность пользователя - Account. Сценарий использования выглядит так:

   1:  // создание пользователя
   2:  Accounts account = new Accounts();
   3:  account.AddNew();
   4:  account.Name = "Name";
   5:  account.Save();
   6:   
   7:  // загрузка объекта по Id
   8:  Accounts account = new Accounts()
   9:  account.LoadByPrimaryKey(1);
  10:   
  11:  // загрузка связной коллекции при обращении к свойству объекта
  12:  var list = account.Roles;

Шаблон Active Record может быть успешно использован в небольших проектах с простой бизнес-логикой. Практика показывает, что когда проект разрастается, то из-за смешанной логики внутри доменных объектов возникает много дублирования в коде и непредвиденных ошибок. Обращения к базе данных довольно сложно проследить, когда они скрыты, например, за свойством объекта account.Roles.
В данном случае объект Account имеет несколько ответственностей:
  1. является объектом домена и хранит бизнес-правила, например, связь с коллекцией ролей
  2. является точкой доступа к базе данных
Решение
Простым и действенным выходом является использование шаблона Repository. Хранилищу AccountRepository мы оставляем работу с базой данных и получаем «чистый» доменный объект.
   1:  // создание пользователя
   2:  var account = new Account();
   3:  account.Name = "Name";
   4:  accountRepository.Save(account);
   5:   
   6:  // загрузка пользователя по Id
   7:  var account = accountRepository.GetById(1);
   8:   
   9:  // загрузка со связной коллекцией
  10:  // пример из LLBLGen Pro
  11:  var account = accountRepository.GetById(1, new IPath[]{new Path<Account>(Account.PrefetchPathRoles)});

2. Валидация данных
Проблема
Если вы сделали хотя бы один проект, то перед вами наверняка стояла проблема валидации данных. Например, проверка введенного адреса эл. почты, длины имени пользователя, сложности пароля и т.п. Для валидации объекта резонно возникает первая реализация:

   1:  public class Product
   2:  {
   3:      public int Price { get; set; }
   4:   
   5:      public bool IsValid()
   6:      {
   7:          return Price > 0;
   8:      }
   9:  }
  10:   
  11:  // проверка на валидность
  12:  var product = new Product { Price = 100 };
  13:  var isValid = product.IsValid();

Такой подход является вполне оправданным в данном случае. Код простой, тестированию поддается, дублирования логики нет.
Теперь наш объект Product начал использовать в некоем CustomerService, который считает валидным продукт с ценой больше 100 тыс. рублей. Что делать? Уже сейчас понятно, что нам придется изменять наш объект продукта, например, таким образом:

   1:  public class Product
   2:  {
   3:      public int Price { get; set; }
   4:   
   5:      public bool IsValid(bool isCustomerService)
   6:      {
   7:          if (isCustomerService == true)
   8:              return Price > 100000;
   9:   
  10:          return Price > 0;
  11:      }
  12:  }
  13:   
  14:  // используем объект продукта в новом сервисе
  15:  var product = new Product { Price = 100 };
  16:  var isValid = product.IsValid(true);

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

   1:  public interface IProductValidator
   2:  {
   3:      bool IsValid(Product product);
   4:  }
   5:   
   6:  public class ProductDefaultValidator : IProductValidator
   7:  {
   8:      public bool IsValid(Product product)
   9:      {
  10:          return product.Price > 0;
  11:      }
  12:  }
  13:   
  14:  public class CustomerServiceProductValidator : IProductValidator
  15:  {
  16:      public bool IsValid(Product product)
  17:      {
  18:          return product.Price > 100000;
  19:      }
  20:  }
  21:   
  22:  public class Product
  23:  {
  24:      private readonly IProductValidator validator;
  25:   
  26:      public Product() : this(new ProductDefaultValidator())
  27:      {
  28:      }
  29:   
  30:      public Product(IProductValidator validator)
  31:      {
  32:          this.validator = validator;
  33:      }
  34:   
  35:      public int Price { get; set; }
  36:   
  37:      public bool IsValid()
  38:      {
  39:          return validator.IsValid(this);
  40:      }
  41:  }
  42:   
  43:  // обычное использование
  44:  var product = new Product { Price = 100 };
  45:   
  46:  // используем объект продукта в новом сервисе
  47:  var product = new Product (new CustomerServiceProductValidator()) { Price = 100 };

Имеем объект Product отдельно, а любое количество всяческих валидаторов отдельно.
В дополнение хочу посоветовать книгу Применение DDD и шаблонов проектирования. Проблемно-ориентированное проектирование приложений с примерами на C# и .NET. В ней очень подробно рассмотрен вопрос валидации данных.

3. God object
Проблема
Предел нарушения принципа единственности ответственности – God object. Этот объект знает и умеет делать все, что только можно. Например, он делает запросы к базе данных, к файловой системе, общается по протоколам в сеть и содержить тонну бизнес-логики. В пример приведу объект, который называется ImageHelper:

   1:  public static class ImageHelper
   2:  {
   3:      public static void Save(Image image)
   4:      {
   5:          // сохранение изображение в файловой системе
   6:      }
   7:   
   8:      public static int DeleteDuplicates()
   9:      {
  10:          // удалить из файловой системы все дублирующиеся изображения и вернуть количество удаленных
  11:      }
  12:   
  13:      public static Image SetImageAsAccountPicture(Image image, Account account)
  14:      {
  15:          // запрос к базе данных для сохранения ссылки на это изображение для пользователя
  16:      }
  17:   
  18:      public static Image Resize(Image image, int height, int width)
  19:      {
  20:          // изменение размеров изображения
  21:      }
  22:   
  23:      public static Image InvertColors(Image image)
  24:      {
  25:          // изменить цвета на изображении
  26:      }
  27:   
  28:      public static byte[] Download(Url imageUrl)
  29:      {
  30:          // загрузка битового массива с изображением с помощью HTTP запроса
  31:      }
  32:   
  33:      // и т.п.
  34:  }
Кажется, что границы ответственности у него вообще нет. Он может сохранять в базу данных, причем знает правила назначения изображений пользователям. Может скачивать изображения. Знает, как хранятся файлы изображений и может работать с файловой системой.
Каждая ответственность этого класса ведет к его потенциальному изменению. Получается, что этот класс будет очень часто менять свое поведение, что затруднит его тестирование и тестирование компонентов, которые его используют. Такой подход снизит работоспособность системы и повысит стоимость ее сопровождения.

Решение
Решением является разделить этот класс по принципу единственности ответственности: один класс на одну ответственность.
   1:  public static class ImageFileManager
   2:  {
   3:      public static void Save(Image image)
   4:      {
   5:          // сохранение изображение в файловой системе
   6:      }
   7:   
   8:      public static int DeleteDuplicates()
   9:      {
  10:          // удалить из файловой системы все дублирующиеся изображения и вернуть количество удаленных
  11:      }
  12:  }
  13:   
  14:  public static class ImageRepository
  15:  {
  16:      public static Image SetImageAsAccountPicture(Image image, Account account)
  17:      {
  18:          // запрос к базе данных для сохранения ссылки на это изображение для пользователя
  19:      }
  20:  }
  21:   
  22:  public static class Graphics
  23:  {
  24:   
  25:      public static Image Resize(Image image, int height, int width)
  26:      {
  27:          // изменение размеров изображения
  28:      }
  29:   
  30:      public static Image InvertColors(Image image)
  31:      {
  32:          // изменить цвета на изображении
  33:      }
  34:  }
  35:   
  36:   
  37:  public static class ImageHttpManager
  38:  {
  39:      public static byte[] Download(Url imageUrl)
  40:      {
  41:          // загрузка битового массива с изображением с помощью HTTP запроса
  42:      }
  43:  }

Вся информация взята с этого блога.

Комментариев нет:

Отправить комментарий