Правила наследования типов. Принцип «Open-Closed Principle».



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

Какую цель мы преследуем, когда применяем этот принцип? Как известно программные проекты в течение свой жизни постоянно изменяются. Изменения могут возникнуть, например, из-за новых требований заказчика или пересмотра старых. В конечном итоге потребуется изменить код в соответствии с текущей ситуацией.

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

Отсюда можно сделать вывод, что нашей целью является разработка системы, которая будет достаточно просто и безболезненно меняться. Другими словами, система должна быть гибкой. Например, внесение изменений в библиотеку общую для 4х проектов не должно быть долгим («долгим» является разным промежутком времени для конкретной ситуации) и уж точно не должно вести к изменениям в этих 4х проектах.

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

Примеры

Без абстракций

Проблема

Самый простой пример нарушения принципа открытости/закрытости – использование конкретных объектов без абстракций. Предположим, что у нас есть объект SmtpMailer. Для логирования своих действий он использует Logger, который записывает информацию в текстовые файлы.

1: public class Logger

2: {

3: public void Log(string logText)

4: {

5:     // сохранить лог в файле

6: }

7: }

8:  

9: public class SmtpMailer

10: {

11: private readonly Logger logger;

12:  

13: public SmtpMailer()

14: {

15:     logger = new Logger();

16: }

17:  

18: public void SendMessage(string message)

19: {

20:     // отсылка сообщения

21:  

22:     logger.Log(string.Format("Отправлено '{0}'", message));

23: }

24: }

И тоже самое происходит в других классах, которые используют Logger. Такая конструкция вполне жизнеспособна до тех, пока мы не решим записывать лог SmptMailer'a в базу данных. Для этого нам надо создать класс, который будет записывать все логи не в текстовый файл, а в базу данных:

1: public class DatabaseLogger

2: {

3: public void Log(string logText)

4: {

5:     // сохранить лог в базе данных

6: }

7: }

А теперь самое интересное. Мы должны изменить класс SmptMailer из-за изменившегося бизнес-требования:

1: public class SmtpMailer

2: {

3: private readonly DatabaseLogger logger;

4:  

5: public SmtpMailer()

6: {

7:        logger = new DatabaseLogger();

8: }

9:  

10: public void SendMessage(string message)

11: {

12:     // отсылка сообщения

13:  

14:     logger.Log(string.Format("Отправлено '{0}'", message));

15: }

16: }

Но ведь по принципу единственности ответственности не SmptMailer отвечает за логирование, почему изменения дошли и до него? Потому что нарушен наш принцип открытости/закрытости. SmptMailer не закрыт для модификации. Нам пришлось его изменить, чтобы поменять способ хранения его логов.

Решение

В данном случае защитить SmtpMailer поможет выделение абстракции. Пусть SmtpMailer зависит от интерфейса ILogger:

1: public interface ILogger

2: {

3: void Log(string logText);

4: }

5:  

6: public class Logger : ILogger

7: {

8: public void Log(string logText)

9: {

10:     // сохранить лог в файле

11: }

12: }

13:  

14: public class DatabaseLogger : ILogger

15: {

16: public void Log(string logText)

17: {

18:     // сохранить лог в базе данных

19: }

20: }

21:  

22: public class SmtpMailer

23: {

24: private readonly ILogger logger;

25:  

26: public SmtpMailer(ILogger logger)

27: {

28:     this.logger = logger;

29: }

30:  

31: public void SendMessage(string message)

32: {

33:     // отсылка сообщения

34:  

35:     logger.Log(string.Format("Отправлено '{0}'", message));

36: }

37: }

Теперь смена логики логирования уже не будет вести к модификации SmtpMailer'а.

Проверка типа абстракции

Проблема

Этот пример в разных вариациях все не раз видели в коде. Его хоть в рамку можно вешать, как самое популярное нарушение проектирования. У нас есть иерархия объектов с абстрактным родительским классом 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: }

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

 


Дата добавления: 2018-05-13; просмотров: 360; Мы поможем в написании вашей работы!

Поделиться с друзьями:






Мы поможем в написании ваших работ!