msiu_logo
Кафедра информационных систем и технологий
http://edu.msiu.ru

Введение в объектно-ориентированное проектирование

А.Г. Верещагин

28 марта 2014

Введение

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

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

Прежде всего, опытному разработчику понятно, что не нужно решать каждую новую задачу с нуля. Вместо этого следует стараться повторно использовать те решения, которые оказались удачными в прошлом. Во многих объектно-ориентированных системах можно встретить повторяющиеся паттерны, состоящие из классов и взаимодействующих объектов. С их помощью решаются конкретные задачи проектирования, в результате чего объектно-ориентированный дизайн становится более гибким, элегантным, и им можно воспользоваться повторно. Проектировщик, знакомый с паттернами, может сразу же применять их к решению новой задачи, не пытаясь каждый раз «изобретать велосипед».

Пять основных принципов дизайна классов (SOLID)

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

Принцип единственной обязанности (Single responsibility principle)

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

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

Принцип открытости/закрытости (Open/closed principle)

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

Вернёмся к примеру с модулем для отчётов. Есть потребность в выборке отчётов по некоторому критерию, например, названию или объёму. Неправильным решением будет реализовывать отдельные методы выборки по каждому из критериев, так как список критериев может неограниченно расширяться. В соответствии с принципом O реализация класса для выборки не должна меняться, вместо этого её можно настраивать, передавая различные критерии, которые теперь фигурируют в виде объектов.

Принцип подстановки Барбары Лисков (Liskov substitution principle)

Данный принцип гласит: объекты в программе могут быть заменены их наследниками без изменения свойств программы. Иначе этот принцип можно сформулировать так: поведение наследуемых классов не должно противоречить поведению, заданному базовым классом. Дело в том, что один из главных способов реализации принципа O — это программирование на уровне интерфейсов. Если некоторый объект используется через свой интерфейс, то его легко можно заменить на другой объект с таким же интерфейсом. Для примера с отчётами это означает, что класс для выборки должен использовать объекты критериев через общий интерфейс. Если один из классов для критериев нарушит принятые для интерфейса соглашения, его невозможно будет адекватно использовать в классе для выборки. Это будет нарушением принципа L.

Принцип разделения интерфейса (Interface segregation principle)

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

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

Принцип обращения зависимостей (Dependency inversion principle)

Неразумное объектно-ориентированное проектирование приводит к тому, что в системе накапливаются множественные связи между классами, следствием чего становится:

Множество зависимостей модуля верхнего уровня от модулей нижнего уровня можно обратить, так, что они превращаются в единичные зависимости последних. Такая операция называется обращением зависимостей. Принцип обращения зависимостей гласит: модули верхнего уровня не должны зависеть от модулей нижних уровней; модули всех уровней должны зависеть от абстракций. Абстракции не должны зависеть от деталей; детали должны зависеть от абстракций. Данный принцип, по сути, следствие из принципов O и L. Это частный, но очень важный случай, который стоит рассмотреть отдельно.

Рассмотрим связт трёх компонентов: класс построения отчётов ReportBuilder, класс отправления отчётов Reporter и класс для отправления отчётв по электронной почте EmailReportSender. Reporter использует ReportBuilder для построения набора отчётов и EmailReportSender для их отправки. Класс Reporter является классов верхнего уровня, и он «знает и умеет» слишком много. Тем самым нарушается принцип S. Зависимость от конкретных классов, а не абстракций нарушает принцип O. В будущем может понадобиться использовать класс Reporter для отправки отчётов по SMS — такая модификация вызовет проблемы.

Введём интерфейсы IReportBuilder и IReportSender, описывающие методы создания и отправки отчётов. Классы ReportBuilder и EmailReportSender должны реализовывать соответствующие интерфейсы. Класс Reporter должен использовать интерфейсы вместо конкретных классов, которые могут быть переданы в конструктор. Такая архитектура удовлетворяет принципу D и является более гибкой.

В случае перехода к программированию на уровне интерфейсов появляется вопрос о том, где и как конструировать конкретные объекты. Вызов конструктора — операция, требующая знания конкретного класса, а не абстракции. Но задачу конструирования можно переложить на специализированные объекты-фабрики, для которых так же возможно определить интерфейс. Так даже конструирование становится гибким и настраиваемым процессом. Но даже фабрики надо конструировать. Начальные установки всё равно необходимы. Решить проблему можно с помощью использования конфигураций (например, файлов с настройками).

Принцип KISS (Keep it short and simple)

Принцип KISS можно перевести просто и красноречиво: не усложняй. Это фундаментальный принцип не только проектирования, но и разработки, хотя сфера его применимости не ограничена и этим. Распространёнными приёмами гибкого и «правильного» проектирования очень легко начать злоупотреблять, что добавит системе лишней сложности. Число классов увеличится, не исключено, что увеличится и объём кода. У гибкости есть собственная «цена». Решения необходимо принимать взвешенно, и это будет не только прагматично, но и рационально.

Введение в паттерны проектирования

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