воскресенье, 10 июля 2016 г.

SOLID - Single(единственность ответственности) Open-closed(открытости-закрытости) Lisk(замещение Лисков) Interface segregation(разделения интерфейса) Dependency inversion


Принцип единственности ответственности - не должно быть больше одной причины для изменения класса

Проблема


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

   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 отдельно, а любое количество всяческих валидаторов отдельно.

Избыточное увлечение


Ведет к Принципу размытой ответственности. Чрезмерная любовь к SRP ведет к обилию мелких классов/методов и размазыванию логики между ними.

Принцип открытости/закрытости - программные сущности (классы, модули, функции и т.д.) должны быть открыты для расширения, но закрыты для изменения


Проблема


У нас есть иерархия объектов с абстрактным родительским классом AbstractEntity и класс Repository, который использует абстракцию. При этом вызывая метод Save у Repository мы строим логику в зависимости от типа входного параметра:
   1:  public abstract class AbstractEntity
   2:  {
   3:  }
   4:   
   5:  public class AccountEntity : AbstractEntity
   6:  {
   7:  }
   8:   
   9:  public class RoleEntity : AbstractEntity
  10:  {
  11:  }
  12:   
  13:  public class Repository
  14:  {
  15:      public void Save(AbstractEntity entity)
  16:      {
  17:          if (entity is AccountEntity)
  18:          {
  19:              // специфические действия для AccountEntity
  20:          }
  21:          if (entity is RoleEntity)
  22:          {
  23:              // специфические действия для RoleEntity
  24:          }
  25:      }
  26:  }
Из кода видно, что объект Repository придется менять каждый раз, когда мы добавляем в иерархию объектов с базовым классом AbstractEntity новых наследников или удаляем существующих. Условные операторы будут множится в методе Save и тем самым усложнять его.

Решение


Конкретизируя классы методом is или typeof мы должны сразу понять, что наш код начал «попахивать». Чтобы решить данную проблему, необходимо логику сохранения конкретных классов из иерархии AbstractEntity вынести в конкретные классы Repository. Для этого мы должны выделить интерфейс IRepository и создать хранилища AccountRepository и RoleRepository:
   1:  public abstract class AbstractEntity
   2:  {
   3:  }
   4:   
   5:  public class AccountEntity : AbstractEntity
   6:  {
   7:  }
   8:   
   9:  public class RoleEntity : AbstractEntity
  10:  {
  11:  }
  12:   
  13:  public interface IRepository<T> where T : AbstractEntity
  14:  {
  15:      void Save(T entity);
  16:  }
  17:   
  18:  public class AccountRepository : IRepository<AccountEntity>
  19:  {
  20:      public void Save(AccountEntity entity)
  21:      {
  22:          // специфические действия для AccountEntity
  23:      }
  24:  }
  25:   
  26:  public class RoleRepository : IRepository<RoleEntity>
  27:  {
  28:      public void Save(RoleEntity abstractEntity)
  29:      {
  30:          // специфические действия для RoleEntity
  31:      }
  32:  }

Теперь наши изменения будут локализованы в конкретных объектах.

Избыточное увлечение


Ведет к Принципу фабрики-фабрик: Чрезмерная любовь к OCP ведет к переусложненным решениям с чрезмерным числом уровней абстракции.

Принцип замещения Лисков - наследники должны соответствовать базовому классу 


Это не так просто понять, ибо вроде бы у нас классы-наследники как раз и создаются чтобы переопределять базовые функции. Но тут принцип в том что переопределение не должно быть настолько координальным , что в корне не позволяет использовать базовый класс ! Кстати в предыдущем примере был нарушен в том числе и этот принцип.Дело в том, что внутри класса Repository мы оперируем не только абстрактной сущностью AbstractEntity, но и унаследованными типами. А это значит, что в данном случае подтипы AccountEntity и RoleEntity не могут быть заменены типом, от которого они унаследованы.

Мы хотим реализовать свой список с интерфейсом IList. Его особенностью будет то, что все записи в нем дублируются.

1
2
3
4
5
6
7
8
9
10
11
public class DoubleList<T> : IList<T>
{
    private readonly IList<T> innerList = new List<T>();
  
    public void Add(T item)
    {
        innerList.Add(item);
        innerList.Add(item);
    }
  
    ... 

Данная реализация не представляет никакой опасности, если рассматривать ее изолированно. Взглянем на использование этого класса с точки зрения клиента. Клиент, абстрагируясь от реализаций, пытается работать со всеми объектами типа IList одинаково:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[Fact]
public void CheckBehaviourForRegularList()
{
    IList<int> list = new List<int>();
  
    list.Add(1);
  
    Assert.Equal(1, list.Count);
}
  
[Fact]
public void CheckBehaviourForDoubleList()
{
    IList<int> list = new DoubleList<int>();
  
    list.Add(1);
  
    Assert.Equal(1, list.Count); // fail
}
Поведение списка DoubleList отличается от типичных реализаций IList. Получается, что наш DoubleList не может быть заменен базовым типом. Это и есть нарушение принципа замещения Лисков.
Рассмотрим пример, где в публичный метод SomeMethod передается абстракция IList:
1
2
3
4
5
6
7
8
9
10
11
12
13
public void SomeMethod(IList<string> list)
{
    List<string> urls = urlService.GetUrls();
    foreach(string url in urls)
    {
        if (SomeBoolLogic(url))
            list.Add(url);
    }
    if (urls.Count > list.Count)
        throw Exception();
}

Результаты сравнения свойств Count (11 строка) будут отличаться в зависимости от того, какой из наследников интерфейса IList будет передан в метод SomeMethod. Если вы передадите экземпляр класса List, то элементов будет меньше, а если DoubleList, то больше (функция Add добавляет по два объекта за раз). Т.е. результат этой функции будет зависеть от конкретной реализации IList.

Программист, который привык, что эта функция работает с любым наследником IList, посидев однажды в дебагере, поймет, что иногда (когда приходит DoubleList) эта функция не работает. Программист добавит код-костыль, чтобы поправить последствия неправильного наследования DoubleList от IList:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public void SomeMethod(IList<string> list)
{
    List<string> urls = urlService.GetUrls();
    foreach(string url in urls)
    {
        if (SomeBoolLogic(url))
            list.Add(url);
    }
    // начало костыля
    int realCount;
    if (list is DoubleList)
        realCount = list.Count / 2;
    else
        realCount = list.Count;
    // конец костыля
    if (urls.Count > realCount)
        throw Exception();
}
Нарушение контракта интерфейса IList привело к использованию оператора is (или as). Если какой-то разработчик создаст TripleList, унаследует его от IList, то еще больше усложнит жизнь своего проекта.

Корни проблемы лежат в непонимании принципов ООП. Вы должны свободно использовать абстракцию у себя в коде, без лишних проверок на тип. Использование операторов is и as должно быть очень веско обосновано, т.к. чаще всего они являются первыми признаками нарушения LSP.

Избыточное увлечение


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

Принцип разделения интерфейса -  клиенты не должны зависеть от методов, которые они не используют


Проблема


Речь идет о базовых классах, которые вынуждают своих наследников знать и делать слишком много. Печально известный пример – класс MembershipProvider. Для использования этого класса нужно реализовать 27 абстрактных методов и свойств.

Решение


В данном случае решение очень простое. В С# это делается простой заменой ключевого слова abstract на virtual в базовом классе.

Избыточное увлечение


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

Принцип инверсии зависимости


  • Модули верхнего уровня не должны зависеть от модулей нижнего уровня. Оба должны зависеть от абстракции.
  • Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.
Классикой применения данного принципа являются паттерны DI и |IoT.

Избыточное увлечение


Ведет к Принципу инверсии сознания или DI-головного мозга. Интерфейсы выделяются для каждого класса и пачками передаются через конструкторы. Понять, где находится логика становится практически невозможно.

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

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