Формулировка: клиенты не должны зависеть от методов, которые они не используют
Как и при использовании других принципов проектирования классов мы пытаемся избавиться от ненужных зависимостей в коде, сделать код легко читаемым и легко изменяемым.
Примеры
Лишняя абстракция в наследовании
Проблема
Речь идет о базовых классах, которые вынуждают своих наследников знать и делать слишком много. Печально известный пример – класс
MembershipProvider. Для использования этого класса нужно реализовать 27 абстрактных методов и свойств.
Рассмотрим пример из жизни. Это, конечно, не скопированный код один в один, но очень приближено к тому, что было. Есть базовый класс для аудиторов EntityAuditor. Он унаследован от класса AuditorBase, который предоставляет ORM, и реализует методAuditEntityFieldSet этого базового класса. Также EntityAuditor добавляет свой абстрактный метод CreateLogRow, который используется в методе AuditEntityFieldSet и должен быть переопределен в конкретных реализациях:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
public abstract class EntityAuditor : AuditorBase
{
public override void AuditEntityFieldSet(IEntityCore entity, int fieldIndex, object originalValue)
{
CreateLogRow(...
}
protected abstract LogRowEntity CreateLogRow( decimal ? fieldId, string oldValue, string newValue, IEntityCore entity);
}
|
После этого начинаем реализовывать наследников. Например, создадим аудитор для класса Product:
1
2
3
4
5
6
7
|
public class ProductAuditor : EntityAuditor
{
protected override LogRowEntity CreateLogRow( decimal ? fieldId, string oldValue, string newValue, IEntityCore entity)
{
}
}
|
Сейчас добавлению наследников ничего не мешает. Теперь представим, что в методе AuditEntityFieldSet понадобилась дополнительная логика, при которой нужно вызвать метод UpdateDuplicates. Этот метод также является абстрактным и требует реализации в наследниках:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
|
public abstract class EntityAuditor : AuditorBase
{
public override void AuditEntityFieldSet(IEntityCore entity, int fieldIndex, object originalValue)
{
CreateLogRow(...
UpdateDuplicates(...
}
protected abstract LogRowEntity CreateLogRow( decimal ? fieldId, string oldValue, string newValue, IEntityCore entity);
protected abstract void UpdateDuplicates(IEntityCore entity, decimal fieldId, object current);
}
public class ProductAuditor : EntityAuditor
{
protected override LogRowEntity CreateLogRow( decimal ? fieldId, string oldValue, string newValue, IEntityCore entity)
{
}
protected override void UpdateDuplicates(IEntityCore entity, decimal fieldId, object current)
{
}
}
public class AccountAuditor : EntityAuditor
{
protected override LogRowEntity CreateLogRow( decimal ? fieldId, string oldValue, string newValue, IEntityCore entity)
{
}
protected override void UpdateDuplicates(IEntityCore entity, decimal fieldId, object current)
{
}
}
|
EntityAuditor требует реализации метода UpdateDuplicates даже в тех наследниках, где он не нужен, как, например, в AccountAuditor. Проблема в том, что частный случай (UpdateDuplicates), который используется только в половине наследников, мы сделали общим, т.е. обязательным для всех наследников нашего EntityAuditor. Получается, что чем больше наследников будет у EntityAuditor, тем больше бесполезного кода мы будем писать, тем больше наследники будут знать лишнего о своем базовом классе. Это может сильно помешать нам в дальнейшем при рефакторинге или изменении логики в EntityAuditor.
Решение
В данном случае решение очень простое. Если наследникам класса EntityAuditor не нужна функция UpdateDuplicates, то и реализовывать ее они не должны. В С# это делается простой заменой ключевого слова abstract на virtual:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
public abstract class EntityAuditor : AuditorBase
{
public override void AuditEntityFieldSet(IEntityCore entity, int fieldIndex, object originalValue)
{
CreateLogRow(...
UpdateDuplicates(...
}
protected abstract LogRowEntity CreateLogRow( decimal ? fieldId, string oldValue, string newValue, IEntityCore entity);
protected virtual void UpdateDuplicates(IEntityCore entity, decimal fieldId, object current)
{
}
}
|
Теперь наследники отчищены от ненужной связности:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
public class ProductAuditor : EntityAuditor
{
protected override LogRowEntity CreateLogRow( decimal ? fieldId, string oldValue, string newValue, IEntityCore entity)
{
}
protected override void UpdateDuplicates(IEntityCore entity, decimal fieldId, object current)
{
}
}
public class AccountAuditor : EntityAuditor
{
protected override LogRowEntity CreateLogRow( decimal ? fieldId, string oldValue, string newValue, IEntityCore entity)
{
}
}
|
В других случаях могут быть другие решения этой проблемы. Если у вас есть примеры из практики, давайте разбирать их в комментариях.
«Жирный» интерфейс
Проблема
У нас есть интерфейс ISpecification. С помощью него мы можем узнать подходит ли продукт заявке – метод IsSuitable и является ли поле продукта измененным – методIsFieldChanged:
1
2
3
4
5
6
|
public interface ISpecification
{
bool IsSuitable(Product realty, Offer offer);
bool IsFieldChanged(Product oldValue, Product newValue);
}
|
Чем является наш модуль для сторонних клиентов? Он является набором интерфейсов, с помощью которых модуль может быть использован. В данном случае проблема заключается в том, что клиентом первой функции является консольное приложение, а второй – класс хранилища:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
public interface ISpecification
{
bool IsSuitable(Product realty, Offer offer);
bool IsFieldChanged(Product oldValue, Product newValue);
}
/// <summary>
/// Хранилище для продуктов
/// </summary>
public class ProductRepository : IRepository<product>
{
public void Save(Product product)
{
specification.IsFieldChanged(...
}
}
/// <summary>
/// Программа расчета подходящих продуктов
/// </summary>
public class Program
{
public static void Main( string [] args)
{
specification.IsSuitable(...
}
}
</product>
|
Допустим, что мы уже написали несколько конкретных спецификаций:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
class PriceSpecification : ISpecification
{
public bool IsSuitable(Product realty, Offer offer)
{
}
public bool IsFieldChanged(Product oldValue, Product newValue)
{
}
}
...
|
Уже видно, что полученный результат нас не устраивает. При рефакторинге или изменении логики в консольном приложении и методе IsSuitable, нам придется затронуть все классы, которые реализовали интерфейс ISpecification. Например, представьте, что будет если в метод IsSuitable мы захотим добавить еще один параметр? А если конкретных спецификаций накопилось уже с десяток? Основная мысль в том, что теперь различные части системы зависят друг от друга, хоть и косвенно. Консольное приложение зависит от логики хранилища и наоборот.
Решение
Главное правило в данном случае звучит так: если клиенты интерфейса разделены, то и интерфейс должен быть разделен соответствующим образом.
После разделения получаем:
1
2
3
4
5
6
7
8
9
|
public interface ISpecification
{
bool IsSuitable(Product realty, Offer offer);
}
public interface IChangeFieldDetector
{
bool IsFieldChanged(Product oldValue, Product newValue);
}
|
Теперь консольное приложение работает интерфейсом ISpecification, а хранилище работает с интерфейсом IChangeFieldDetector. Проблема с зависимостью решена.
Кроме этого, мы решили еще и проблему с наследниками первой реализацииISpecification. Теперь класс может реализовывать только один интерфейс, за счет чего его на много проще поддерживать:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
class PriceSpecification : ISpecification
{
public bool IsSuitable(Product realty, Offer offer)
{
}
}
class PriceChangeFieldDetector : IChangeFieldDetector
{
public bool IsFieldChanged(Product oldValue, Product newValue)
{
}
}
|