Руководство по стилю программирования и конструированию по
Скачать 7.6 Mb.
|
Размеры структур данных Объявляя массив из 100 элементов, вы раскрыва- ете информацию, которую никто знать не должен. Защищайте право на личную жизнь! Сокрытие информации не всегда требует создания целого класса. Иногда для этого достаточно именованной константы: например, MAX_EMPLOYEES позво- ляет скрыть число 100. Предвосхищение изменений разного масштаба Обдумывая потенциальные изменения системы, проектируйте ее так, чтобы влияние изменений было обратно пропорцио- нально их вероятности. Если вероятность изменения высо- ка, убедитесь, что систему будет легко адаптировать к нему. С большим влиянием на несколько классов системы можно смириться лишь в случае крайне маловероятных изменений. Грамотные проектировщики также принимают во внимание цену предвосхищения изменений. Если изменение малове- роятно, но его легко предугадать, рассмотрите его вниматель- нее, чем более вероятное изменение, которое трудно спла- нировать. Один хороший метод определения областей вероятных изменений подразумевает, что вы должны сначала опреде- лить минимальное подмножество фрагментов программы, необходимых пользователям. Это подмножество составляет ядро системы, и его изменения маловероятны. Затем вы определяете минимальные инкрементные приращения системы. Они могут быть совсем небольшими, даже тривиальными. Вместе с функциональными изменениями рассматривайте также качественные изменения программы: обеспечение безопасности в многопоточной среде, поддержку механизмов локализации и т. д. Эти области потенциальных улучшений являются потенциальными изменениями системы; спроектируйте эти области, используя принципы сокрытия информации. Определив ядро в самом начале, вы поймете, какие компоненты системы на самом деле являются допол- нениями, и сможете с этого момента экстраполировать и скрывать аспекты воз- можных изменений программы. Поддерживайте сопряжение слабым Сопряжение характеризует силу связи класса или метода с другими классами или методами. Наша цель — создать классы и методы, имеющие немногочисленные, непосредственные, явные и гибкие отношения с другими классами, что еще на- Перекрестная ссылка Рассмат- риваемый в этом разделе под- ход к предвосхищению изме- нений не связан с заблаговре- менным проектированием или кодированием (см. подраздел «Программа содержит код, ко- торый может когда-нибудь по- надобиться» раздела 24.2). Дополнительные сведения Это обсуждение основано на подхо- де, описанном в статье «On the design and development of prog- ram families» (Parnas, 1976). ГЛАВА 5 Проектирование при конструировании 97 зывают «слабым сопряжением» (loose coupling)». В контекстах классов и методов концепция сопряжения одна и та же, так что при обсуждении сопряжения буду называть методы и классы «модулями». Сопряжение модулей должно быть достаточно слабым, чтобы одни модули мог- ли с легкостью использовать другие. Например, железнодорожные вагоны соеди- няются с помощью крюков, которые при столкновении двух вагонов защелкива- ются. Представьте, как бы все усложнилось, если бы вагоны нужно было соеди- нять при помощи болтов, набора тросов или если бы вы могли соединить между собой только определенные типы вагонов. Механизм соединения вагонов эффек- тивен потому, что он максимально прост. Соединения между программными мо- дулями также должны быть как можно проще. Старайтесь создавать модули, слабо зависящие от других модулей. Отношения модулей должны напоминать отношения деловых партнеров, а не сиамских близ- нецов. Скажем, метод sin() (синус) сопряжен слабо, так как нужную информа- цию он получает в форме одного значения — угла в градусах. Метод InitVars ( var1, var2, var3, ..., varN ) сопряжен жестче, поскольку многие детали его работы становятся известными вызывающему модулю по передаваемым значениям. Два класса, зависящих от того, как каждый из них использует одну глобальную пере- менную, сопряжены еще жестче. Критерии оценки сопряжения Ниже описаны критерии, позволяющие оценить сопряжение модулей. Объем Объем связи характеризует число соединений между модулями. Чем их меньше, тем лучше, поскольку модуль, имеющий более компактный интерфейс, легче связать с другими модулями. Метод, принимающий один параметр, слабее сопряжен с вызывающими его модулями, чем метод, принимающий шесть пара- метров. Класс, имеющий четыре грамотно определенных открытых метода, сла- бее сопряжен с модулями, которые его используют, чем класс, предоставляющий 37 открытых методов. Видимость Видимостью называют заметность связи между двумя модулями. Про- граммирование не служба в ЦРУ — никто не похвалит вас за удачную маскиров- ку. Оно больше похоже на рекламу: вам следует делать связи между модулями как можно более крикливыми. Передача данных посредством списка параметров формирует очевидную связь, и это удачный вариант. Передача информации дру- гому модулю в глобальных данных является замаскированной и потому неудач- ной связью. Описание связи, осуществляемой через глобальные данные, в доку- ментации делает ее более явной и является чуть более удачным подходом. Гибкость Гибкость характеризует легкость изменения связи между модулями. Идеальная связь должна быть как можно гибче. Гибкость частично определяется другими аспектами связанности, но в то же время отличается от них. Положим, у вас есть метод LookupVacationBenefit(), определяющий длительность отпуска со- трудника на основании даты его приема на работу и должности. Допустим далее, что в другом модуле у вас есть объект employee (сотрудник), содержащий, поми- мо всего прочего, информацию о должности и дате приема на работу, и что этот модуль передает объект employee в метод LookupVacationBenefit(). 98 ЧАСТЬ II Высококачественный код С точки зрения других критериев, эти два модуля кажутся слабо сопряженными: связь двух модулей посредством объекта employee очевидна и является единствен- ной. Теперь предположим, что вам нужно использовать модуль LookupVacation Be - nefit() из третьего модуля, владеющего информацией о дате приема сотрудника на работу и его должности, но хранит ее не в объекте employee. В этот момент модуль LookupVacationBenefit() начинает вести себя гораздо менее дружелюбно, не желая связываться с новым модулем. Чтобы третий модуль мог обратиться к модулю LookupVacationBenefit(), он должен знать о существовании класса Employee. Он мог бы подделать объект employee, используя лишь два поля, но тогда он должен был бы знать внутренние детали работы метода LookupVacationBenefit(): ему была бы необходима уверенность в том, что метод LookupVacationBenefit() использует только два этих поля. Такое реше- ние было бы небрежным и безобразным. Второй вариант мог бы заключаться в таком изменении метода LookupVacationBenefit(), чтобы вместо объекта employee он принимал должность сотрудника и дату его приема на работу. В обоих случа- ях первоначальный модуль оказывается на самом деле гораздо менее гибким, чем казалось сначала. Возможен и счастливый конец этой истории: недружелюбный модуль сможет завести друзей, если пожелает быть гибким — если вместо объекта employee он согласится принимать должность и дату приема сотрудника на работу. Короче, чем проще вызывать модуль из других модулей, тем слабее он сопряжен, и это хорошо, потому что такой модуль более гибок и прост в сопровождении. Создавая структуру программы, делите ее на блоки с учетом их взаимосвязанно- сти. Если бы программа была куском дерева, его следовало бы расщепить парал- лельно волокнам. Виды сопряжения Самые распространенные виды сопряжения описаны ниже. Простое сопряжение посредством данных-параметров Два модуля со- пряжены таким способом, если между ними передаются только элементарные типы данных, причем передаются через списки параметров. Этот вид сопряжения нормален и приемлем. Простое сопряжение посредством объекта Модуль сопряжен с объектом этим способом, если он создает экземпляр данного объекта. С этим видом со- пряжения также все в порядке. Сопряжение посредством объекта-параметра Два модуля сопряжены друг с другом объектом-параметром, если Объект 1 требует, чтобы Объект 2 передал ему Объект 3. Этот вид сопряжения жестче, чем тот вид, при котором Объект 1 требует от Объекта 2 только примитивных типов данных, потому что Объект 2 должен обладать информацией об Объекте 3. Семантическое сопряжение Самый коварный тип сопряжения имеет место тогда, когда один модуль использует не какой-то синтаксический элемент друго- го модуля, а некоторые семантические знания о внутренней работе этого модуля. Некоторые примеры такого вида сопряжения описаны ниже. ГЛАВА 5 Проектирование при конструировании 99 쐽 Модуль 1 передает в Модуль 2 управляющий флаг, определяющий дальнейшую работу Модуля 2. Этот подход подразумевает, что Модуль 1 должен сделать предположения о внутренней работе Модуля 2, а именно о том, что Модуль 2 собирается делать с управляющим флагом. Если Модуль 2 определяет для управляющего флага специфический тип данных (перечисление или объект), этот вид сопряжения, вероятно, будет вполне приемлем. 쐽 Модуль 2 использует глобальные данные после их изменения Модулем 1. При этом Модуль 2 предполагает, что Модуль 1 был вызван в нужное время и изме- нил данные так, как нужно Модулю 2. 쐽 Интерфейс Модуля 1 утверждает, что метод Module1. Initialize() должен быть вызван до метода Module1.Routine(). Модуль 2 знает, что Module1.Routine() как- то вызывает метод Module1.Initialize(), поэтому он просто создает экземпляр Модуля 1 и вызывает Module1.Routine() без предварительного вызова метода Module1.Initialize(). 쐽 Модуль 1 передает Объект в Модуль 2. Модуль 1 знает, что Модуль 2 использует только три метода Объекта из семи, поэтому он инициализирует Объект лишь частично, только теми данными, что нужны этим трем методам. 쐽 Модуль 1 передает в Модуль 2 Базовый Объект. Модуль 2 знает, что на самом деле Модуль 1 передал ему Производный Объект, поэтому он приводит тип Базового Объекта к типу Производного Объекта и вызывает методы, специфи- ческие для Производного Объекта. Семантическое сопряжение опасно тем, что изменение кода в используемом мо- дуле может так нарушить работу использующего модуля, что компилятор этого не определит. Обычно это приводит к очень тонким проблемам, которые никто не соотносит с изменениями используемого модуля, что превращает отладку в сизифов труд. Суть слабого сопряжения в том, что грамотно спроектированный модуль предо- ставляет дополнительный уровень абстракции: разработав его, вы можете прини- мать его как данное. Это снижает общую сложность программы и позволяет сосре- доточиваться в каждый момент времени только на одном аспекте. Если для исполь- зования модуля нужно учитывать сразу несколько аспектов: механизм внутренней работы, изменения глобальных данных, неясную функциональность, — сила абст- ракции исчезает, и модуль перестает облегчать управление сложностью. Классы и методы — главные интеллектуальные инструменты снижения сложности. Если они не упрощают вашу работу, они не исполняют свои обязанности. Старайтесь использовать популярные шаблоны проектирования Шаблоны проектирования — это готовые шаблоны, позво- ляющие решать частые проблемы разработки. Конечно, есть проблемы, требующие совершенно новых решений, но боль- шинство уже встречалось разработчикам, поэтому их можно решить, применяя проверенные подходы, или шаблоны. В число популярных шаблонов проектиро- вания входят Адаптер, Мост, Декоратор, Фасад, Фабричный метод, Наблюдатель, http://cc2e.com/0585 100 ЧАСТЬ II Высококачественный код Одиночка, Стратегия и Шаблонный метод. О шаблонах проектирования см. книгу «Design Patterns» Эриха Гаммы, Ричарда Хелма, Ральфа Джонсона и Джона Влис- сидеса (Gamma, Helm, Johnson, and Vlissides, 1995). Шаблоны имеют ряд достоинств, не характерных для полностью самостоятельного проектирования программы. Шаблоны снижают сложность, предоставляя готовые абстракции Если вы скажете: «В этом фрагменте для создания экземпляров производных классов применяется шаблон “Фабричный метод”», — другие программисты поймут, что ваш код включает богатый набор взаимодействий и протоколов программирова- ния, специфических для названного шаблона. Шаблон «Фабричный метод» позволяет создавать экземпляры любого класса, про- изводного от указанного базового класса, причем отдельные производные классы отслеживаются только самим «Фабричным методом». Обсуждение шаблона «Фа- бричный метод» см. в разделе «Replace Constructor with Factory Method» (Замена конструктора на «Фабричный метод») книги «Refactoring» (Fowler, 1999). Если вы будете использовать шаблоны, другие программисты легко поймут вы-бранный вами подход к проектированию без подробного обсуждения кода. Шаблоны снижают число ошибок, стандартизируя детали популярных решений Проблемы проектирования содержат нюансы, которые полностью проявляются только после решения проблемы один или два раза (или три, или четыре, или…). Шаблоны — это стандартизованные способы решения частых проблем, заключающие мудрость, накопленную за годы попыток решения этих проблем, и исправления неудачных попыток. Так что, с концептуальной точки зрения, применение шаблона проектирования похоже на использование библиотеки кода вместо написания собственного кода. Многие программисты рано или поздно решают создать собственный вариант алгоритма быстрой сортировки, но каковы шансы, что его первая версия окажется безошибочной? Так же и в проектировании: многие проблемы довольно похожи на уже решенные задачи, и при столкновении с ними изобретать велосипед ни к чему. Шаблоны имеют эвристическую ценность, указывая на возможные ва- рианты проектирования Проектировщик, знакомый с популярными шабло- нами, может с легкостью перебрать список шаблонов и спросить себя: «Какие из них соответствуют моей проблеме проектирования?» Перебрать набор известных вариантов гораздо проще, чем создавать собственное решение с нуля. Кроме того, код, основанный на популярном шаблоне, будет понятнее, чем код, полностью разработанный самостоятельно. Шаблоны упрощают взаимодействие между разработчиками, позволяя им общаться на более высоком уровне Шаблоны проектирования не только помогают управлять сложностью, но и способны ускорить обсуждение проектов, позволяя разработчикам размышлять и делиться мыслями на более высоком уровне. Если вы скажете: «Не могу решить, какой шаблон следует использовать в данной ситуации: “Создатель” или “Фабричный метод”», — вы в нескольких словах сообщи- те очень подробную информацию — конечно, если и вам, и вашему собеседнику известны эти шаблоны. Представьте, насколько больше времени потребовалось ГЛАВА 5 Проектирование при конструировании 101 бы для обсуждения деталей кода шаблонов «Создатель» и «Фабричный метод» и сравнения этих двух подходов. Если вы еще не сталкивались с шаблонами проектирования, изучите табл. 5-1, где описаны некоторые из самых популярных шаблонов. Табл. 5-1. Популярные шаблоны проектирования Шаблон Описание Абстрактная фабрика Поддерживает создание наборов родственных объектов пу- (Abstract Factory) тем определения вида набора, но не вида каждого отдельно- го объекта. Адаптер (Adapter) Преобразует интерфейс класса в другой интерфейс. Мост (Bridge) Создает интерфейс и реализацию, так что их можно изме- нять независимо друг от друга. Компоновщик Состоит из объекта, содержащего дополнительные объекты (Composite) такого же типа, позволяя клиентскому коду взаимодейство- вать с объектом верхнего уровня и не заботиться о деталь- ных объектах. Декоратор (Decorator) Динамически назначает объекту виды ответственности без создания отдельных подклассов для каждой возможной конфигурации видов ответственности. Фасад (Facade) Предоставляет согласованный интерфейс к коду, который в противном случае не предоставлял бы согласованного интерфейса. Фабричный метод Создает экземпляры классов, производных от конкретного (Factory Method) базового класса, причем отдельные производные классы от- слеживаются только «Фабричным методом». Итератор (Iterator) Этот серверный объект предоставляет доступ к каждому элементу набора в последовательном порядке. Наблюдатель (Observer) Поддерживает синхронизацию нескольких объектов, при которой объект уведомляет набор связанных объектов об изменениях любого члена набора. Одиночка (Singleton) Предоставляет глобальный доступ к классу, который может иметь один и только один экземпляр. Стратегия (Strategy) Определяет набор динамически взаимозаменяемых алгоритмов или видов поведения. Шаблонный метод Определяет структуру алгоритма, оставляя некоторые (Template Method) детали реализации подклассам. Если раньше вы не встречались с шаблонами проектирования, при взгляде на табл. 5-1 у вас может возникнуть мысль: «Почти все эти идеи мне уже знакомы». Этим во многом и объясняется ценность шаблонов проектирования. Они известны большинству опытных программистов, а присвоение шаблонам запоминающихся названий позволяет быстро и эффективно делиться мыслями. С шаблонами связаны две ловушки. Первая — насильственная адаптация кода к какому-нибудь шаблону. Иногда легкое изменение кода в соответствии с извест- ным шаблоном может сделать код более понятным. Но если адаптация кода к стандартному шаблону требует слишком крупного изменения, это может привести к усложнению программы. 102 ЧАСТЬ II Высококачественный код Вторая — применение шаблона, продиктованное не целесообразностью, а жела- нием испытать шаблон в деле. Вообще применение шаблонов проектирования — это эффективный инструмент управления сложностью. Некоторые хорошие книги по этой теме указаны в конце главы. |