Бьерн Страуструп. Язык программирования С++. Специальное издание (2011) (1004033), страница 177
Текст из файла (страница 177)
Подумайте, какой общности именования и функциональности можно добиться для всех классов компонента. 23.4. Процесс разработки 829 Это — манифест минимализма. Ясно, что легче добавить, не думая, все функции, которые когда-либо могут понадобиться, и сделать их виртуальными,но чем больше функций, тем более вероятно, что они останутся неиспользованными и только затруднят реализацию системы и ее дальнейшие модификации.
В частности, функции, которые непосредственно читают или записывают некоторую часть состояния объекта класса ограничивают класс единственной стратегией развития и сильнейшим образом снижают потенциал развития системы. Такие функции понижают уровень абстракции от концепции до конкретного варианта реализации. Большое количество функций лишь добавляет работы реализующему код программисту и проблемы проектировщику системы при ее дальнейшем совершенствовании. Гораздо лучше добавить функцию в тот момент, когда четко выявилась необходимость в этой функции, чем удалять функцию при осознании ее ненужности. Нужно принимать решение о виртуальности функции самым что ни на есть явным образом (а не в виде детали реализации), ибо это критически влияет на применение класса и на его взаимосвязи с другими классами.
Объекты класса всего лишь с одной виртуальной функцией имеют нетривиальную структуру по сравнению с объектами таких языков, как С или Еопгап. Класс с единственной виртуальной функцией потенциально может действовать как интерфейс к еще не определенному классу, а виртуальные функции подразумевают зависимость от еше не определенных классов (524.3.2.1).
Отметим также, что минимализм на самом деле требует от проектировщика больше работы, а не меньше. При выборе операций важно сосредоточиться на том, что нужно сделать, а не на том, как это сделать. То есть нам нужно сосредоточиться на желательном поведении, а не на деталях реализации. Бывает полезно классифицировать операции с точки зрения того, как они используют внутреннее состояние объектов: ° Фундаментальные олераиии: конструкторы, деструкторы и операции копирования. ° лИнспекторыск операции, не модифицирующие состояние объектов.
° Модификаторы: операции, модифицирующие состояние объектов. ° Преобразования: операции, производящие объект другого типа, отталкиваясь от значения (состояния) объекта, к которому они применяются. ° Итераторы: операции, служащие для доступа к последовательности объектов, хранящихся в контейнере. Перечисленные классификационные категории не ортогональны друг другу.
Например, итератор может быть также инспектором или модификатором. Эти категории являются просто классификацией, которая помогла многим людям совершенствоваться в проектировании интерфейсов. Естественно, возможны и другие классификации. Такие классификации помогают согласовывать классы компонента. Язык С++ помогает отличать инспекторов от модификаторов за счет поддержки константных и неконстантных функций-членов. Аналогично, непосредственно в самом языке поддерживаются конструкторы, деструкторы, операции присваивания и операции приведения (преобразования) типов.
830 Глава 23. Общий взгляд на разработку программ. Проектирование 23.4.3.3. Этап 3: выявление зависимостей Уточните классы в плане их зависимостей. Различные виды зависимостей обсуждаются в 324.3. В контексте проектирования ключевыми являются зависимости параметризация, наследования и использования.
Для каждой из них важно ответить на вопрос о том, что означает, что класс ответственен за какое-то одно из свойств системы. Чтобы классу быть ответственным за что-то, ему не обязательно самому содержать все необходимые для этого данные, или непосредственно выполнять все требуемые операции. Более того, классы с фиксированной ответственностью за отдельные свойства системы чаще всего переправляют идущие на них запросы другим классам, которые отвечают за решение частных подзадач.
Естественно, тут можно переборщить и создать чрезвычайно неэффективную и труднопонимаемую архитектуру, в рамках которой никакая работа не делается иначе, как порождением каскада обращений объектов за сервисом друг к другу. То, что можно сделать на месте, должно делаться сразу на этом месте. Необходимость рассмотрения вопросов наследования и отношений использования на этапе проектирования (а не реализации) вытекает из перехода от простого использования классов к представлению концепций задачи. Отсюда следует, что единицей проекта является компонент (з23.4.3, э24.4), а не индивидуальный класс. Параметризация — часто приводящая к применения шаблонов — это способ явного представления неявных зависимостей, позволяющая представить альтернативы без введения новых концепций. Часто имеется возможность выбора между тем, чтобы оставить что-то зависеть от контекста, или представить в виде ветви дерева наследования, или воспользоваться параметром (324.4.1).
23.4.3.4. Этап 4: определение интерфейсов Определите интерфейсы. Закрытые Грпчаге) функции обычно не рассматриваются на этапе проектирования. Вопросы реализации на этапе проектирования по большей части связаны с зависимостями, возникшими на этапе 3.
Кроме того, я вывел эмпирическое правило, гласящее: «если для класса не возможны по крайней мере две реализации, то с этим классом что-то не так». Это, скорее всего, не представление концепции, а замаскированная определенная реализация. Во многих случаях, рассмотрение возможности медленной эволюции класса есть приближение к ответу на вопрос «достаточно ли независим интерфейс класса от реализации?», Открытые базовые классы, а также друзья класса являются частью интерфейса; см.
также 311.5 и 324.4.2. Обеспечение раздельных интерфейсов для наследования и для обычных клиентов путем предоставления защищенных и открытых интерфейсов является упражнением, заслуживающим поощрения. На данном шаге выявляются и специфицируются точные типы аргументов. В идеале нужно стремиться к тому, чтобы максимально полно статически типизировать интерфейсы в рамках типов прикладной задачи; см. 324.2.3 и 324.4.2. В процессе определения интерфейсов выявляйте классы, чьи операции, как кажется, поддерживают несколько уровней абстракции. Например, некоторые функции-члены класса Р(!е могут принимать аргументы типа Р((е с(еясг(ргог, в то время как другие — строковые описания имен файлов.
Операции, принимающие Л(е деясНргог, и операции, принимающие имена файлов, работают на разных уровнях абстракции, так что может возникнуть вопрос, относятся ли они к одному 831 23.4 Процесс разработки и тому же классу. Может лучше организовать два класса, каждый из которых поддерживает либо файловый дескриптор, либо имена файлов.
В типичном случае, все операции класса должны поддерживать одинаковый уровень абстракции. Если это не так, то стоит подумать о реорганизации этого класса и родственных ему классов. 23.4.3.5. Реорганизация иерархии классов На этапах 1 и 3 мы изучаем структуру классов и классовую иерархию с целью убедиться, что они адекватны нашим нуждам. В общем случае 100%-ой адекватности нет, и мы вынуждены заняться реорганизацией этих структур с целью их улучшения. Наиболее общими типами реорганизации классов являются вычленение общих частей двух классов и вынос этой общей части в новый класс, или разделение старого класса на два новых класса.
В обоих случаях мы приходим к трем классам: одному базовому и двум производным. Когда такую реорганизацию следует делать? Каковы признаки того, что такая реорганизация принесет пользу? К сожалению, простых и однозначных ответов на эти вопросы нет. Ничего удивительного, ибо мы ищем ответы не на частные вопросы о мелких деталях реализации, а на вопросы об изменениях базовых концепций системы.
Здесь самая нетривиальная задача заключается в том, чтобы найти общность между классами и вычленить их общую часть. Точного критерия общности не существует, но этот критерий в любом случае должен затрагивать концепции, например, общность моделей использования, сходство наборов операций, сходство реализаций и даже то обстоятельство, что оба класса одновременно рассматриваются при обсуждении проекта.
И наоборот, класс может стать кандидатом для расщепления на два класса, если разные подмножества операций этого класса имеют разные модели использования, если они имеют доступ к разным частям представления данных и даже если по поводу класса возникают разные дискуссии. Заметим также, что иногда превращение схожих классов в шаблон является систематизированным способом предоставления альтернатив Я24.4.1).
Часто проблемы с организацией классовых иерархий возникают из-за плохого именования классов и вытекающих отсюда трудностей в использовании этих имен в процессе обсуждения проекта. Действительно, если при обсуждении проекта ощущается, что имена из иерархии классов звучат нелепо, имеется необходимость и потенциальная возможность организовать иерархию по-другому.
Заметьте, что тем самым я хочу сказать, что два человека при обсуждении и анализе иерархии классов намного лучше, чем один. В последнем же случае нужно попытаться составить письменное руководство по проекту с явным выписыванием всех имен классов иерархии. Одна из самых важных задач проектирования — предоставить интерфейсы, которые могут оставаться стабильными при изменении самих классов (323.4.2).