Thursday, July 22, 2010

SOLID. Принципы проектирования классов

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

SOLID – это акроним названий пяти основных принципов проектирования классов сформулированных Робертом Мартином: Single Responsibility, Open Closed, Liskov Substitution, Interface Segregation и Dependency Inversion.


SRP: Single Responsibility Principle (принцип единственной ответственности).

Класс должен иметь одну и только одну причину для модификации.

Этот принцип говорит о том, что по-хорошему каждый класс должен решать только одну задачу. Это совсем не значит, что в классе должен быть всего один метод. Это означает, что методы класса должны быть связаны одной общей целью. Давайте рассмотрим пример. Допустим, у нас есть некий класс Order, который умеет управлять элементами заказа, посчитывать сумму заказа, сохранять заказ в базу и загружать из базы, а также печатать заказ:

Этот класс нарушает принцип единственной ответственности, т.к. он решает сразу три задачи:

  1. управляет элементами заказа
  2. умеет работать с базой для сохранения и загрузки заказа
  3. умеет распечатывать заказ на принтере

Вынеся из класса Order функционал, не касающийся работы с элементами заказа, мы получим следующие классы, каждый из которых не нарушает SRP.

Это очень простой пример и тут достаточно очевидно, сколько и каких именно обязанностей было у исходного класса Order. В реальной ситуации выделение у класса нескольких ответственностей может быть гораздо менее очевидным и однозначным. Для таких случаев существует правило: Старайтесь выделять ответственности, которые изменяются независимо друг от друга. Вот как в нашем примере. Если вдруг изменится вывод заказа на печать, то достаточно будет поправить только PrintManager, а два других класса останутся без изменений.


OCP: Open/Closed Principle (принцип открытия/закрытия)


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

Этот принцип говорит о том, что классы нужно проектировать так, чтобы впоследствии иметь возможность изменять поведение класса, не изменяя его код. Давайте посмотрим на класс PrintManager из предыдущего примера.

У него есть всего один метод Print, который в каком-то виде печатает заказ. Допустим, в один прекрасный день нам понадобится печатать заказ в трех разных формах. Самый простой вариант модифицировать PrintManager следующим образом.

Для расширения поведения класса PrintManager мы полезли изменять его код. И каждый раз при появлении новой формы для печати мы опять будем изменять PrintManager. Это явное нарушение принципа OCP.

Решить проблему добавления форм для печати при этом, не нарушая OCP можно, например вот так. Определим интерфейс IOrderFormatter. Классы реализующие этот интерфес будут отвечать за форматирование отчета в определенный вид.

Теперь PrintManager нужно изменить следующим образом:

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


LSP: Liskov Substitution Principle (принцип замещения Лисков)


Функции, которые используют ссылки на базовые классы, должны иметь возможность использовать объекты производных классов, не зная об этом.

Давайте посмотрим на метод Print класса PrintManager, который мы создали в предыдущем разделе. Предполагается, что он использует свой второй параметр через ссылку на интерфейс, не зная ничего о его конкретном типе.

Как только в методе Print появится код для приведения параметра formatter к какому-то конкретному типу, это будет нарушением принципа LSP.

Еще одним видом нарушения принципа LSP является ошибочное наследование. Подробные примеры можно посмотреть у Роберта Мартина здесь и у Александра Бындю здесь.


ISP: Interface Segregation Principle (принцип изоляции интерфейса)


Клиент не должен вынужденно зависеть от элементов интерфейса, которые он не использует.

Давайте рассмотрим пример. У нас есть интерфейс IBird, который реализует методы актуальные вроде бы для всех птиц: летать, чирикать и кушать.

Когда мы реализуем этот интерфейс в классе Воробей, то все никаких проблем при этом не возникнет. Дальше мы реализуем интерфейс для Утки. И столкнемся с тем, что с чириканьем у нее как-то не сложилось. Для Пингвина все еще печальнее. Он не умеет ни чирикать, ни летать. Получается что, только потому, что Пингвин реализует IBird, ему надо реализовать у себя два метода, которые ему на самом деле абсолютно не нужны. Зато он умеет плавать, но наш интерфейс такого метода не содержит.

Решить проблему можно разделив широкий интерфейс IBird на более узкие и специализированные на конкретной задаче. SRP для интерфейсов, если хотите.

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

На практике проблема неадекватно широких интерфейсов может быть еще хуже чем в примере с птицами. Одним из самых ярких примеров является MembershipProvider, который содержит почти три десятка абстрактных методов, а в большинстве ситуаций нужно реализовать от силы парочку.


DIP: Dependency Inversion Principle (принцип обращения зависимости)

  1. Модули верхних уровней не должны зависеть от модулей нижних уровней. Оба типа модулей должны зависеть от абстракций.
  2. Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.

Давайте рассмотрим пример. Допустим, у нас есть класс Manager, который для выполнения некоторой работы умеет создавать экземпляр класса Worker и делегировать ему работу.

DIP1

Manager содержит жесткую ссылку на Worker, что нарушает принципы DIP и OCP. Например, если у конструктора Worker изменится список параметров, то придется вносить соответствующие изменения в Manager, хотя изменения в Worker его не должны касаться. Кроме того, если вдруг для выполнения работы нам потребуется использовать какою-то иную имплементацию Worker’a, например, SeasonedWorker, то опять придется изменять класс Manager.

Для того чтобы отвязать класс Manager от конкретной реализации Worker’a давайте создадим интерфейс IWorker и изменим классы Worker и SeasonedWorker так чтобы они реализовывали IWorker.

Teперь давайте модифицируем Manager, так чтобы он хранил ссылку на IWorker и получал ее через конструктор.

Теперь зависимости между классами выглядят так:

DIP2

В классе Manager больше нет жесткой ссылки на конкретный класс Worker, вместо этого он содержит ссылку на абстракцию IWorker. Теперь Manager соответствует требованиям и DIP и OCP.

 

Ссылки:

10 comments:

  1. Хорошая статья, нужная.

    ReplyDelete
  2. большой пасиб теперь мой говнакод становиться все лучше и лучше

    ReplyDelete
  3. Вопрос по принципу SRP (хочется уточнить, правильно ли я понимаю принцип?):
    Нужно максимально стремиться специализировать класс, например, отделить бизнес-логику от сохранения данных в БД.
    Также следует специализировать все открытые методы класса, которые составляют его интерфейс. Например, метод: int CalculateDiscount(int price) - вычисляет скидку и ничего более.
    Если же для успешной реализации такого метода требуется дополнительная бизнес-логика, то ее нужно вынести в отдельные вспомогательные private-методы, которые не являются открытым интерфейсом класса.

    ReplyDelete
  4. Спасибо.
    Похоже наконец понял зачем нужны интерфейсы.

    ReplyDelete
  5. Отличная статья! Действительно железные принципы, которые упрощают жизнь.

    ReplyDelete
  6. Всегда считал интерфейсы чем-то бесполезным, спасибо за просвещение

    ReplyDelete
  7. Благодарю за понятное и яркое разъяснение ISP: Interface Segregation Principle (принцип изоляции интерфейса)!

    ReplyDelete
  8. Похерилась статья( картинок нет, примеры кода пропущены (((

    ReplyDelete