Принцип единственности ответственности
Формулировка: не должно быть больше одной причины для изменения класса
Что является причиной изменения логики работы класса? Видимо, изменение отношений между классами, введение новых требований или отмена старых. Вообще, вопрос о причине этих изменений лежит в плоскости ответвенности, которую мы возложили на наш класс. Если у объекта много ответвенности, то и меняться он будет очень часто. Таким образом, если класс имеет больше одной ответственности, то это ведет к хрупкости дизайна и ошибкам в неожиданных местах при изменениях кода.
Примеры
Сценариев, где можно встретить нарушение этого принципа очень много. Вот несколько самых популярных. Примеры будут приводиться с обозначением ошибки в дизайне, после чего будет приведено решение проблемы.
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 имеет несколько ответственностей:
- является объектом домена и хранит бизнес-правила, например, связь с коллекцией ролей
- является точкой доступа к базе данных
Решение
Простым и действенным выходом является использование шаблона 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: }
Что является причиной изменения логики работы класса? Видимо, изменение отношений между классами, введение новых требований или отмена старых. Вообще, вопрос о причине этих изменений лежит в плоскости ответвенности, которую мы возложили на наш класс. Если у объекта много ответвенности, то и меняться он будет очень часто. Таким образом, если класс имеет больше одной ответственности, то это ведет к хрупкости дизайна и ошибкам в неожиданных местах при изменениях кода.
Примеры
Сценариев, где можно встретить нарушение этого принципа очень много. Вот несколько самых популярных. Примеры будут приводиться с обозначением ошибки в дизайне, после чего будет приведено решение проблемы.
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;
В данном случае объект Account имеет несколько ответственностей:
- является объектом домена и хранит бизнес-правила, например, связь с коллекцией ролей
- является точкой доступа к базе данных
Простым и действенным выходом является использование шаблона 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)});
Проблема
Если вы сделали хотя бы один проект, то перед вами наверняка стояла проблема валидации данных. Например, проверка введенного адреса эл. почты, длины имени пользователя, сложности пароля и т.п. Для валидации объекта резонно возникает первая реализация:
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 };
В дополнение хочу посоветовать книгу Применение 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: }
Вся информация взята с этого блога.
Вся информация взята с этого блога.
Комментариев нет:
Отправить комментарий