Совершенный код. Совершенный код. Мастер-класс. Стив Макконнелл. Руководство по стилю программирования и конструированию по
Скачать 5.88 Mb.
|
ЧАСТЬ II Высококачественный код что некоторые программисты скорее раскрыли бы все закрытые данные класса, чем написали 10 дополнительных строк для защиты его секретов. Вопрос «Что этот класс должен скрывать?» обнажает самую суть проблемы про- ектирования интерфейса. Если функцию или данные можно включить в открытый интерфейс класса, не раскрыв его секретов, сделайте это. В противном случае воздержитесь от такого решения. Размышление о том, чт у скрыть, способствует принятию удачных решений на всех уровнях проектирования. Оно подталкивает к применению именованных констант вместо чисел на уровне конструирования, помогает выбирать удачные имена ме- тодов классов и их параметров и указывает на грамотные варианты декомпозиции и реализации взаимодействия классов и подсистем на уровне системы. Почаще задавайте себе вопрос «Что мне скрыть?», и вы удивитесь, сколь# ко проблем проектирования растает на ваших глазах Определите области вероятных изменений Исследования показали, что всем лучшим проектировщикам свойственно предвосхищать изменения (Glass, 1995). Обес- печение легкости адаптации программы к возможным изме- нениям относится к самым сложным аспектам проектирова- ния. Его цель заключается в изоляции нестабильных областей, позволяющей ограничить следствия изменений одним мето- дом, классом или пакетом. Вот как проходит подготовка к изменениям. 1. Определите элементы, изменение которых кажется вероятным. Если вы выработали адекватные требования, они включают список потенциальных изменений и оценки вероятности каждого из них. В этом случае определить вероятные изменения легко. Если требования не описывают потенциальные изменения, ниже вы найдете список областей, которые меняются чаще всего независимо от типа проекта. 2. Отделите элементы, изменение которых кажется вероятным. Создайте отдельный класс для каждого нестабильного компонента, определенного в п. 1, или разработайте классы, включающие несколько нестабильных компонентов, изменение которых скорее всего будет одновременным. 3. Изолируйте элементы, изменение которых кажется вероятным. Спро- ектируйте интерфейсы между классами так, чтобы они не зависели от по- тенциальных изменений. Спроектируйте интерфейсы так, чтобы изменения ограничивались только внутренними частями классов. Изменение класса должно оставаться незаметным для любых других классов. Интерфейс класса должен защищать его секреты. Ниже описано несколько областей, изменяющихся чаще всего. Бизнес'правила Необходимость изменения ПО часто объясняется изменениями бизнес#правил. Оно и понятно: конгресс может изменить систему налогообложения, проф- союзы — пересмотреть условия контрактов и т. д. Если вы соблюдаете принцип сокрытия информации, логика, осно- ванная на этих правилах, не будет распространена на всю Дополнительные сведения Под- ход, описанный в этом разде- ле, взят из статьи «Designing Software for Ease of Extension and Contraction» (Parnas, 1979). Перекрестная ссылка Один из самых эффективных способов предвосхищения изменений — табличное управление (см. гла- ву 18). CC2_Part2_ch5_2010.indd 94 22.06.2010 12:31:37 ГЛАВА 5 Проектирование при конструировании 95 программу. Она будет скрыта в одном темном уголке системы, пока не придет время ее изменить. Зависимости от оборудования Примерами модулей, зависимых от оборудова- ния, могут служить интерфейсы между программой и разными типами мониторов, принтеров, клавиатур, дисководов, звуковых плат и сетевых устройств. Изолируй- те зависимости от оборудования в отдельной подсистеме или отдельном классе. Это облегчает адаптацию программы к новой аппаратной среде, а также помогает раз- рабатывать ПО для нестабильных версий устройств. Вы можете разработать ПО, моделирующее взаимодействие с конкретным устройством, и создать подсистему аппаратного интерфейса, использующую эту модель, пока устройство нестабиль- но или недоступно. Когда устройство будет готово к работе, подсистему интерфейса можно будет отключить от модели и подключить к устройству. Ввод'вывод На чуть более высоком в сравнении с аппаратными интерфейсами уровне проектирования частой областью изменений является ввод#вывод. Если ваше приложение создает собственные файлы данных, его усложнение вполне может потребовать изменения формата файлов. Аспекты формата ввода#вывода данных, относящиеся к пользовательскому уровню, такие как позиционирование и число полей на странице, их последовательность и т. д., изменяются не менее часто. В общем, анализ всех внешних интерфейсов на предмет возможных изменений — благоразумная идея. Нестандартные возможности языка Большинство версий языков поддер- живает нестандартные расширения, облегчающие работу программистов. Расшире- ния — палка о двух концах, потому что в другой среде — будь то другая аппаратная платформа, реализация языка другим производителем или новая версия языка, выпущенная тем же производителем, — они могут оказаться недоступны. Если вы применяете нестандартные расширения языка, скройте работу с ними в отдельном классе, чтобы его можно было заменить при адаптации приложения к другой среде. Аналогично, используя библиотечные методы, доступные не во всех средах, скройте их за интерфейсом, поддерживающим все нужные среды. Сложные аспекты проектирования и конструирования Скрывайте слож- ные аспекты проектирования и конструирования, потому что их частенько при- ходится реализовывать заново. Отделите их и минимизируйте влияние, которое может оказать их неудачное проектирование или конструирование на остальные части системы. Переменные статуса Переменные статуса характеризуют состояние програм- мы и изменяются чаще, чем большинство других видов данных. Так, разработчи- ки, определившие переменную статуса ошибки как булеву переменную, вполне могут позднее прийти к выводу, что для этого лучше было бы использовать пере- числение со значениями ErrorType_None, ErrorType_Warning и ErrorType_Fatal. Использование переменных статуса можно сделать более гибким и понятным минимум двумя способами. В качестве переменных статуса примените не булевы переменные, а перечис- ления. Диапазон поддерживаемых переменными статуса состояний часто при- ходится расширять, что в случае перечисления требует лишь перекомпиляции CC2_Part2_ch5_2010.indd 95 22.06.2010 12:31:38 96 ЧАСТЬ II Высококачественный код программы, а не масштабной ревизии всех фрагментов кода, выполняющих проверку переменной. Вместо непосредственной проверки переменной используйте методы досту- па. Так вы сохраните возможность реализации более сложного механизма определения состояния. Например, если вы захотите проверять комбинацию переменной статуса ошибки и переменной текущего функционального состо- яния, вам будет легко реализовать это, если проверка будет скрыта в методе, и гораздо сложнее, если механизм проверки будет жестко закодирован во мно- гих местах программы. Размеры структур данных Объявляя массив из 100 элементов, вы раскрыва- ете информацию, которую никто знать не должен. Защищайте право на личную жизнь! Сокрытие информации не всегда требует создания целого класса. Иногда для этого достаточно именованной константы: например, MAX_EMPLOYEES позво- ляет скрыть число 100. Предвосхищение изменений разного масштаба Обдумывая потенциальные изменения системы, проектируйте ее так, чтобы влияние изменений было обратно пропорцио- нально их вероятности. Если вероятность изменения высо- ка, убедитесь, что систему будет легко адаптировать к нему. С большим влиянием на несколько классов системы можно смириться лишь в случае крайне маловероятных изменений. Грамотные проектировщики также принимают во внимание цену предвосхищения изменений. Если изменение малове- роятно, но его легко предугадать, рассмотрите его вниматель- нее, чем более вероятное изменение, которое трудно спла- нировать. Один хороший метод определения областей вероятных изменений подразумевает, что вы должны сначала опреде- лить минимальное подмножество фрагментов программы, необходимых пользователям. Это подмножество составляет ядро системы, и его изменения маловероятны. Затем вы определяете минимальные инкрементные приращения системы. Они могут быть совсем небольшими, даже тривиальными. Вместе с функциональными изменениями рассматривайте также качественные изменения программы: обеспечение безопасности в многопоточной среде, поддержку механизмов локализации и т. д. Эти области потенциальных улучшений являются потенциальными изменениями системы; спроектируйте эти области, используя принципы сокрытия информации. Определив ядро в самом начале, вы поймете, какие компоненты системы на самом деле являются допол- нениями, и сможете с этого момента экстраполировать и скрывать аспекты воз- можных изменений программы. Поддерживайте сопряжение слабым Сопряжение характеризует силу связи класса или метода с другими классами или методами. Наша цель — создать классы и методы, имеющие немногочисленные, непосредственные, явные и гибкие отношения с другими классами, что еще на- Перекрестная ссылка Рассмат- риваемый в этом разделе под- ход к предвосхищению изме- нений не связан с заблаговре- менным проектированием или кодированием (см. подраздел «Программа содержит код, ко- торый может когда-нибудь по- надобиться» раздела 24.2). Дополнительные сведения Это обсуждение основано на подхо- де, описанном в статье «On the design and development of prog- ram families» (Parnas, 1976). CC2_Part2_ch5_2010.indd 96 22.06.2010 12:31:38 ГЛАВА 5 Проектирование при конструировании 97 зывают «слабым сопряжением» (loose coupling)». В контекстах классов и методов концепция сопряжения одна и та же, так что при обсуждении сопряжения буду называть методы и классы «модулями». Сопряжение модулей должно быть достаточно слабым, чтобы одни модули мог- ли с легкостью использовать другие. Например, железнодорожные вагоны соеди- няются с помощью крюков, которые при столкновении двух вагонов защелкива- ются. Представьте, как бы все усложнилось, если бы вагоны нужно было соеди- нять при помощи болтов, набора тросов или если бы вы могли соединить между собой только определенные типы вагонов. Механизм соединения вагонов эффек- тивен потому, что он максимально прост. Соединения между программными мо- дулями также должны быть как можно проще. Старайтесь создавать модули, слабо зависящие от других модулей. Отношения модулей должны напоминать отношения деловых партнеров, а не сиамских близ- нецов. Скажем, метод sin() (синус) сопряжен слабо, так как нужную информа- цию он получает в форме одного значения — угла в градусах. Метод InitVars ( var1, var2, var3, ..., varN ) сопряжен жестче, поскольку многие детали его работы становятся известными вызывающему модулю по передаваемым значениям. Два класса, зависящих от того, как каждый из них использует одну глобальную пере- менную, сопряжены еще жестче. Критерии оценки сопряжения Ниже описаны критерии, позволяющие оценить сопряжение модулей. Объем Объем связи характеризует число соединений между модулями. Чем их меньше, тем лучше, поскольку модуль, имеющий более компактный интерфейс, легче связать с другими модулями. Метод, принимающий один параметр, слабее сопряжен с вызывающими его модулями, чем метод, принимающий шесть пара- метров. Класс, имеющий четыре грамотно определенных открытых метода, сла- бее сопряжен с модулями, которые его используют, чем класс, предоставляющий 37 открытых методов. Видимость Видимостью называют заметность связи между двумя модулями. Про- граммирование не служба в ЦРУ — никто не похвалит вас за удачную маскиров- ку. Оно больше похоже на рекламу: вам следует делать связи между модулями как можно более крикливыми. Передача данных посредством списка параметров формирует очевидную связь, и это удачный вариант. Передача информации дру- гому модулю в глобальных данных является замаскированной и потому неудач- ной связью. Описание связи, осуществляемой через глобальные данные, в доку- ментации делает ее более явной и является чуть более удачным подходом. Гибкость Гибкость характеризует легкость изменения связи между модулями. Идеальная связь должна быть как можно гибче. Гибкость частично определяется другими аспектами связанности, но в то же время отличается от них. Положим, у вас есть метод LookupVacationBenefit(), определяющий длительность отпуска со- трудника на основании даты его приема на работу и должности. Допустим далее, что в другом модуле у вас есть объект employee (сотрудник), содержащий, поми- мо всего прочего, информацию о должности и дате приема на работу, и что этот модуль передает объект employee в метод LookupVacationBenefit(). CC2_Part2_ch5_2010.indd 97 22.06.2010 12:31:38 98 ЧАСТЬ II Высококачественный код С точки зрения других критериев, эти два модуля кажутся слабо сопряженными: связь двух модулей посредством объекта employee очевидна и является единствен- ной. Теперь предположим, что вам нужно использовать модуль LookupVacation Be - nefit() из третьего модуля, владеющего информацией о дате приема сотрудника на работу и его должности, но хранит ее не в объекте employee. В этот момент модуль LookupVacationBenefit() начинает вести себя гораздо менее дружелюбно, не желая связываться с новым модулем. Чтобы третий модуль мог обратиться к модулю LookupVacationBenefit(), он должен знать о существовании класса Employee. Он мог бы подделать объект employee, используя лишь два поля, но тогда он должен был бы знать внутренние детали работы метода LookupVacationBenefit(): ему была бы необходима уверенность в том, что метод LookupVacationBenefit() использует только два этих поля. Такое реше- ние было бы небрежным и безобразным. Второй вариант мог бы заключаться в таком изменении метода LookupVacationBenefit(), чтобы вместо объекта employee он принимал должность сотрудника и дату его приема на работу. В обоих случа- ях первоначальный модуль оказывается на самом деле гораздо менее гибким, чем казалось сначала. Возможен и счастливый конец этой истории: недружелюбный модуль сможет завести друзей, если пожелает быть гибким — если вместо объекта employee он согласится принимать должность и дату приема сотрудника на работу. Короче, чем проще вызывать модуль из других модулей, тем слабее он сопряжен, и это хорошо, потому что такой модуль более гибок и прост в сопровождении. Создавая структуру программы, делите ее на блоки с учетом их взаимосвязанно- сти. Если бы программа была куском дерева, его следовало бы расщепить парал- лельно волокнам. Виды сопряжения Самые распространенные виды сопряжения описаны ниже. Простое сопряжение посредством данных'параметров Два модуля со- пряжены таким способом, если между ними передаются только элементарные типы данных, причем передаются через списки параметров. Этот вид сопряжения нормален и приемлем. Простое сопряжение посредством объекта Модуль сопряжен с объектом этим способом, если он создает экземпляр данного объекта. С этим видом со- пряжения также все в порядке. Сопряжение посредством объекта'параметра Два модуля сопряжены друг с другом объектом#параметром, если Объект 1 требует, чтобы Объект 2 передал ему Объект 3. Этот вид сопряжения жестче, чем тот вид, при котором Объект 1 требует от Объекта 2 только примитивных типов данных, потому что Объект 2 должен обладать информацией об Объекте 3. Семантическое сопряжение Самый коварный тип сопряжения имеет место тогда, когда один модуль использует не какой#то синтаксический элемент друго- го модуля, а некоторые семантические знания о внутренней работе этого модуля. Некоторые примеры такого вида сопряжения описаны ниже. CC2_Part2_ch5_2010.indd 98 22.06.2010 12:31:38 ГЛАВА 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 передал ему Производный Объект, поэтому он приводит тип Базового Объекта к типу Производного Объекта и вызывает методы, специфи- ческие для Производного Объекта. Семантическое сопряжение опасно тем, что изменение кода в используемом мо- дуле может так нарушить работу использующего модуля, что компилятор этого не определит. Обычно это приводит к очень тонким проблемам, которые никто не соотносит с изменениями используемого модуля, что превращает отладку в сизифов труд. Суть слабого сопряжения в том, что грамотно спроектированный модуль предо# ставляет дополнительный уровень абстракции: разработав его, вы можете прини- мать его как данное. Это снижает общую сложность программы и позволяет сосре- доточиваться в каждый момент времени только на одном аспекте. Если для исполь- зования модуля нужно учитывать сразу несколько аспектов: механизм внутренней работы, изменения глобальных данных, неясную функциональность, — сила абст- ракции исчезает, и модуль перестает облегчать управление сложностью. Классы и методы — главные интеллектуальные инструменты снижения сложности. Если они не упрощают вашу работу, они не исполняют свои обязанности. |