Выявление классов и выбор операций
Опыт показывает, что процесс выделения классов и объектов является последовательным, итеративным. Очень важно, следовательно, с самого начала по возможности приблизиться к правильным решениям, чтобы сократить число последующих шагов приближения к истине. Для оценки качества классов и объектов, выделяемых в системе, можно предложить следующие пять критериев:
- зацепление;
- связность;
- достаточность;
- полнота;
- примитивность.
Зацепление – это степень глубины связей между отдельными модулями, классами и объектами. Систему с сильной зависимостью между модулями гораздо сложнее воспринимать и модифицировать. Сложность системы может быть уменьшена путем уменьшения зацепления между отдельными модулями. Существует определенное противоречие между явлениями зацепления и наследования. С одной стороны, желательно избегать сильного зацепления классов; с другой стороны, механизм наследования, тесно связывающий подклассы с суперклассами, помогает выгодно использовать сходство абстракций.
Связность – это степень взаимодействия между элементами отдельного модуля, класса или объекта, характеристика его насыщенности. Наименее желательной является связность по случайному принципу, когда в одном классе или модуле собираются совершенно независимые абстракции. Наиболее желательной является функциональная связность, при которой все элементы класса или модуля тесно взаимодействуют в достижении определенной цели.
Под достаточностью подразумевается наличие в классе или модуле всего необходимого для реализации логичного и эффективного поведения. Иначе говоря, компоненты должны быть полностью пригодны к использованию. Для примера рассмотрим класс set (множество). Операция удаления элемента из множества в этом классе, очевидно, необходима, но будет ошибкой не включить в этот класс и операцию добавления элемента. Нарушение требования достаточности обнаруживается очень быстро, как только создается клиент, использующий абстракцию.
Под полнотой подразумевается наличие в интерфейсной части класса всех характеристик абстракции. Идея достаточности предъявляет к интерфейсу минимальные требования, а идея полноты охватывает все аспекты применения абстракции. Полнота является субъективным фактором, и разработчики часто ею злоупотребляют, добавляя в интерфейс такие операции, которые можно реализовать на более низком уровне.
Из этого вытекает требование примитивности. Примитивными являются только такие операции, которые требуют доступа к внутренней реализации абстракции. Так, в примере с классом set операция добавления к множеству элемента примитивна, а операция добавления четырех элементов не будет примитивной, так как вполне эффективно реализуется через операцию добавления одного элемента. Конечно, эффективность тоже вещь субъективная. Операция, которая требует прямого доступа к структуре данных, примитивна по определению. Операция, которая может быть описана в терминах существующих примитивных операций, но ценой значительно больших вычислительных затрат, также является кандидатом на включение в разряд примитивных (например, операция добавления к множеству другого множества).
Описание интерфейса класса или модуля – трудная работа. Обычно первое приближение делается, исходя из структурного смысла класса, а затем, когда появляются клиенты класса, интерфейс уточняется, модифицируется и дополняется. В частности может возникнуть потребность в создании новых классов или в изменении взаимодействия существующих.
В пределах каждого класса принято иметь только примитивные операции, отражающие отдельные аспекты поведения. Такие методы называются точными. Принято также отделять методы, не связанные между собой. Это облегчает образование подклассов с переопределением поведения. Решение о количестве методов может быть обусловлено двумя причинами: описание поведения в одном методе упрощает интерфейс, но усложняет и увеличивает размеры самого метода; расщепление метода усложняет интерфейс, но делает каждый из методов проще. Обычно операции объявляются как методы класса, к объектам которого относятся данные действия, но многие языки допускают описание операций в виде свободных подпрограмм.
В объектно-ориентированном проектировании принято рассматривать методы класса как единое целое, поскольку все они взаимодействуют друг с другом для реализации протокола абстракции. Таким образом, определив поведение, нужно решить, в каком из классов это поведение реализуется. Критериями для принятия решения служат следующие вопросы:
Повторная используемость: Будет ли это поведение полезно более чем в одном контексте?
Сложность: Насколько трудно реализовать такое поведение?
Применимость: Насколько данное поведение характерно для класса, в который мы хотим включить поведение?
Знание реализации: Надо ли для реализации данного поведения знать секреты класса?
При ответе на эти вопросы нужно использовать принципы проектирования. Большинство принципов ориентированы на уменьшение сложности, создание как можно более простого кода. Принцип KISS (keep it simple stupid, keep it short and simple, делай проще) требует избегать усложнений кода или функциональности, если в этом нет необходимости. Принцип DRY (don’t repeat youself, не повторяйся) или DIE (duplication is evil, повторение — зло) требует избегать повторения кода («copy-paste»). Принцип YAGNI (you ain’t gonna need it, вам это не понадобится) требует реализовывать только поставленные задачи, не отвлекаясь на задачи, которые возможно потребуется решать в будущем. Принцип «Less is more» (меньше — лучше) объявляет простоту реализации и простоту интерфейса более важными, чем любые другие свойства системы. Также на поддержание простоты направлены правила «не заставляйте меня думать», «пиши код для сопровождающего», «принцип наименьшего удивления», «избегай преждевременной оптимизации», «повторное использование кода – это хорошо», «делай самую простейшую вещь, которая скорее всего заработает». Пять принципов ООП получили отдельную аббревиатуру SOLID:
- Single responsibility principle (принцип единственности ответственности) — один класс отвечает за один функционал.
- Open/closed principle (принцип открытости/закрытости) — сущности должны быть открыты к расширению, но закрыты — к изменению, т. е. новая функциональность должна приводить к добавлению новых подклассов, методов, а не к изменению существующего кода.
- Liskov substitution principle (принцип подстановки Барбары Лисков) — сущность, использующая объект, который реализует определенный интерфейс, должна иметь возможность использовать другой объект с тем же интерфейсом, не зная о факте подмены.
- Interface segregation principle (принцип разделения интерфейса) — клиентский код не должен зависеть от методов, которые не использует. Лучше иметь несколько интерфейсов для разных клиентов с небольшим числом методов, чем один — с большим.
- Dependency inversion principle (принцип инверсии зависимостей) — модули высшего порядка не должны зависеть от модулей низшего порядка, и те, и другие должны зависеть от абстракций; детали должны зависеть от абстракций, но не наоборот.