Руководство по стилю программирования и конструированию по
Скачать 7.6 Mb.
|
Важность управления сложностью Программные проекты редко терпят крах по техническим причинам. Чаще всего провал объясняется неадекватной выработкой требований, неудачным планированием или неэффективным управлением. Если же провал обусловлен все-таки преимущественно технической причиной, очень часто ею оказывается неконтролируемая сложность. Иначе говоря, приложение стало таким сложным, что разработчики перестали по-настоящему понимать, что же оно делает. Если работа над проектом достигает момента, после которого уже никто не может полностью понять, как изменение одного фрагмента программы повлияет на другие фрагменты, прогресс прекращается. Управление сложностью — самый важный технический аспект разработки ПО. По-моему, управление сложностью настолько важно, что оно долж- но быть Главным Техническим Императивом Разработки ПО. Сложность — не новинка в мире разработки ПО. Один из пионеров информати- ки Эдсгер Дейкстра обращал внимание на то, что компьютерные технологии — Перекрестная ссылка В ранних средах несущественные пробле- мы проявляются сильнее, чем в зрелых (см. раздел 4.3). Есть два способа разработки проекта приложения: сделать его настолько простым, чтобы было очевидно, что в нем нет недостатков, или сделать его таким сложным, чтобы в нем не было очевидных недостатков. Ч. Э. Р. Хоар (C. A. R. Hoare) 76 ЧАСТЬ II Высококачественный код единственная отрасль, заставляющая человеческий разум охватывать диапазон, простирающийся от отдельных битов до нескольких сотен мегабайт информации, что соответствует отношению 1 к 10 9 , или разнице в девять порядков (Dijkstra, 1989). Такое гигантское отношение просто ошеломляет. Дейкстра выразил это так: «По сравнению с числом семантических уровней средняя математическая теория кажется почти плоской. Создавая потребность в глубоких концептуальных иерар- хиях, компьютерные технологии бросают нам абсолютно новый интеллектуальный вызов, не имеющий прецедентов в истории». Разумеется, за прошедшее с 1989 г. время сложность ПО только выросла, и сегодня отношение Дейкстры вполне может характеризоваться 15 порядками. Дейкстра пишет, что ни один человек не обладает ин- теллектом, способным вместить все детали современной компьютерной программы (Dijkstra, 1972), поэтому нам — разработчикам ПО — не следует пытаться охватить всю программу сразу. Вместо этого мы должны попытаться ор- ганизовать программы так, чтобы можно было безопасно работать с их отдельными фрагментами по очереди. Целью этого является минимизация объема программы, о кото- ром нужно думать в конкретный момент времени. Можете считать это своеобразным умственным жонглированием: чем больше умственных шаров программа заставляет под- держивать в воздухе, тем выше вероятность того, что вы уроните один из них и допустите ошибку при проектиро- вании или кодировании. На уровне архитектуры ПО сложность проблемы можно снизить, разделив систему на подсистемы. Несколько несложных фрагментов информации понять проще, чем один сложный. В разбиении сложной проблемы на простые фрагменты и заключается цель всех методик проектирования ПО. Чем более независимы подсистемы, тем безопаснее сосредоточиться на одном аспек- те сложности в конкретный момент времени. Грамотно определенные объекты разделяют аспекты проблемы так, чтобы вы могли решать их по очереди. Пакеты обеспечивают такое же преимущество на более высоком уровне агрегации. Стремление к краткости методов программы помогает снизить нагрузку на ин- теллект. Этому же способствует написание программы в терминах проблемной области, а не низкоуровневых деталей реализации, а также работа на самом вы- соком уровне абстракции. Суть сказанного в том, что программисты, компенсирующие изначальные ограни- чения человеческого ума, пишут более понятный и содержащий меньшее число ошибок код. Как бороться со сложностью? Чаще всего причинами неэффективности являются: 쐽 сложное решение простой проблемы; 쐽 простое, но неверное решение сложной проблемы; 쐽 неадекватное сложное решение сложной проблемы. Одним из симптомов того, что вы погрязли в чрезмерной слож- ности, является упрямое приме- нение метода, нерелевантность которого очевидна по крайней мере любому внешнему наблю- дателю. При этом вы уподобляе- тесь человеку, который при по- ломке автомобиля в силу своей некомпетентности не находит ничего лучшего, чем заменить воду в радиаторе и выбросить окурки из пепельниц. Ф. Дж. Плоджер (P. J. Plauger) ГЛАВА 5 Проектирование при конструировании 77 Как указал Дейкстра, сложность современного ПО обусловлена самой его приро- дой, поэтому, как бы вы ни старались, вы все равно столкнетесь со сложностью, присущей самой проблеме реального мира. Исходя из этого, можно предложить двойственный подход к управлению сложностью: 쐽 старайтесь свести к минимуму объем существенной сложности, с ко- торым придется работать в каждый конкретный момент времени; 쐽 сдерживайте необязательный рост несущественной сложности. Как только вы поймете, что все остальные технические цели разработки ПО вто- ричны по отношению к управлению сложностью, многие принципы проектиро- вания окажутся простыми. Желательные характеристики проекта Высококачественные проекты программ имеют несколько общих характеристик. Если вы сумеете достичь всех этих целей, ваш проект на самом деле будет очень хорош. Не- которые цели противоречат другим, но это и есть одна из задач проектирования — объединение конкурирующих целей в удачном наборе компромиссов. Некоторые аспек- ты качества проекта — надежность, производительность и т. д. — описывают и качество программы, тогда как другие являются внутренними характеристиками проекта. Вот список таких внутренних характеристик проекта. Минимальная сложность В силу только что описанных причин главной целью проектирования должна быть ми- нимизация сложности. Избегайте создания «хитроумных» проектов: как правило, их трудно понять. Вместо этого соз- давайте «простые» и «понятные» проекты. Если при работе над отдельным фраг- ментом программы проект не позволяет безопасно игнорировать большинство остальных фрагментов, он неудачен. Простота сопровождения Проектируя приложение, не забывайте о програм- мистах, которые будут его сопровождать. Постоянно представляйте себе вопросы, которые будут возникать у них при взгляде на создаваемый вами код. Думайте о таких программистах как о своей аудитории и проектируйте систему так, чтобы ее работа была очевидной. Слабое сопряжение Слабое сопряжение (loose coupling) предполагает сведение к минимуму числа соединений между разными частями программы. Для проек- тирования классов с минимальным числом взаимосвязей используйте принципы адекватной абстракции интерфейсов, инкапсуляцию и сокрытие информации. Это позволит максимально облегчить интеграцию, тестирование и сопровожде- ние программы. Расширяемость Расширяемостью системы называют свойство, позволяющее улучшать систему, не нарушая ее основной структуры. Изменение одного фраг- мента системы не должно влиять на ее другие фрагменты. Внесение наиболее вероятных изменений должно требовать наименьших усилий. Работая над проблемой, я ни- когда не думаю о красоте. Я думаю только о решении про- блемы. Но если полученное ре- шение некрасиво, я знаю, что оно неверно. Р. Бакминстер Фуллер (R. Buckminster Fuller) Перекрестная ссылка Эти ха- рактеристики связаны с общими атрибутами качества ПО (см. раздел 20.1). 78 ЧАСТЬ II Высококачественный код Возможность повторного использования Проектируйте систему так, чтобы ее фрагменты можно было повторно использовать в других системах. Высокий коэффициент объединения по входу При высоком коэффициенте объединения по входу (fan-in) к конкретному классу обращается большое число других классов. Это значит, что система предусматривает интенсивное использо- вание вспомогательных низкоуровневых классов. Низкий или средний коэффициент разветвления по выходу Это означает, что конкретный класс обращается к малому или среднему числу других классов. Высокий коэффициент разветвления по выходу (fan-out) (более семи) говорит о том, что класс использует большое число других классов и, возможно, слишком сложен. Ученые обнаружили, что низкий коэффициент разветвления по выходу выгоден как в случае вызова методов из метода, так и в случае вызова методов из класса (Card and Glass, 1990; Basili, Briand, and Melo, 1996). Портируемость Проектируйте систему так, чтобы ее можно было легко адап- тировать к другой среде. Минимальная, но полная функциональность Этот аспект подразумевает от- сутствие в системе лишних частей (Wirth, 1995; McConnell, 1997). Вольтер говорил, что книга закончена не тогда, когда в нее больше нечего добавить, а когда из нее ничего нельзя выбросить. При разработке ПО это верно вдвойне, потому что до- полнительный код необходимо разработать, проанализировать, протестировать, а также пересматривать при изменении других фрагментов программы. Кроме того, в будущих версиях приложения придется поддерживать обратную совместимость с дополнительным кодом. Опасайтесь вопроса: «Эту функцию реализовать легко — почему бы этого не сделать?» Стратификация Под стратификацией понимают разделение уровней декомпо- зиции, позволяющее изучить систему на любом отдельном уровне и получить при этом согласованное представление. Проектируйте систему так, чтобы ее можно было изучать на отдельных уровнях, игнорируя другие уровни. Например, если вы создаете современную систему, кото- рая должна использовать большой объем старого, плохо спроектированного кода, напишите уровень, отвечающий за взаи модействие со старым кодом. Спроектируйте этот уровень так, чтобы он скрывал плохое качество старого кода, предоставляя более новым уровням согласованный набор сервисов. Пусть осталь- ные части системы работают с этими классами вместо старого кода. Такой подход сулит два преимущества: 1) он изолирует плохой код и 2) если вы когда-нибудь решите выбросить старый код или выполнить его рефакторинг, вам не придется изменять новый код за исключением промежуточного уровня. Соответствие стандартным методикам Чем экзо- тичнее система, тем сложнее будет другим программистам понять ее. Попытайтесь придать всей системе привычный для разработчиков облик, применяя стандартные популяр- ные подходы. Перекрестная ссылка О работе со старыми системами см. раз- дел 24.5. Перекрестная ссылка Об осо- бенно полезном типе стратифи- кации — применении шаблонов проектирования — см. подраз- дел «Старайтесь использовать популярные шаблоны проекти- рования» раздела 5.3. ГЛАВА 5 Проектирование при конструировании 79 Уровни проектирования Проектирование программной системы требует нескольких уровней детальности. Некоторые методы проектирования используются на всех уровнях, а другие только на одном-двух (рис. 5-2). Рис. 5-2. Уровни проектирования программы. Систему (1) следует разделить на подсистемы (2), подсистемы — на классы (3), а классы — на методы и данные (4); методы также необходимо спроектировать (5) Уровень 1: программная система Первому уровню проектирования соответствует вся система. Некоторые программисты с системного уровня сразу пере- ходят к проектированию классов, но обычно целесообразно обдумать более высокоуровневые комбинации классов, такие как подсистемы или пакеты. Уровень 2: разделение системы на подсистемы или пакеты Главный результат проектирования на этом уровне — определение основных подсистем. Подсистемы могут быть Иными словами — и это неиз- менный принцип, на котором основан всегалактический успех всей корпорации, — фундамен- тальные изъяны конструкции ее товаров камуфлируются их внешними изъянами. Дуглас Адамс (Douglas Adams) 80 ЧАСТЬ II Высококачественный код довольно крупными, такими как модуль работы с базами данных, модули GUI, бизнес-правил или создания отчетов, интерпретатор команд и т. д. Суть проек- тирования на данном уровне заключается в разделении программы на основные подсистемы и определении взаимодействий между подсистемами. Обычно этот уровень нужен при работе над любыми проектами, требующими более нескольких недель. При проектировании отдельных подсистем можно применять разные под- ходы: выбирайте тот, который кажется вам оптимальным в каждом конкретном случае. На рис. 5-2 данный уровень проектирования обозначен цифрой 2. Особенно важный аспект этого уровня — определение правил взаимодействия под- систем. Если все подсистемы могут взаимодействовать, выгода их разделения исчезает. Подчеркивайте суть подсистем, ограничивая их взаимодействие между собой. Допустим, вы определили систему из шести подсистем (рис. 5-3). При отсутствии каких-либо ограничений в силу второго закона термодинамики энтропия системы должна увеличиться. Один из способов увеличения энтропии является абсолютно свободное взаимодействие между подсистемами (рис. 5-4). Рис. 5-3. Пример системы, включающей шесть подсистем Рис. 5-4. Возможный результат отсутствия правил, ограничивающих взаимодей- ствие подсистем Как видите, в итоге все подсистемы начинают напрямую взаимодействовать, что поднимает несколько важных вопросов: 쐽 в скольких разных частях системы нужно хоть немного разбираться разработчи- ку, желающему изменить какой-то аспект подсистемы графических операций? ГЛАВА 5 Проектирование при конструировании 81 쐽 что будет, если вы попытаетесь задействовать данный модуль бизнес-правил в другой системе? 쐽 что будет, если вы захотите включить в систему новый пользовательский ин- терфейс (например, интерфейс командной строки, удобный для проведения тестирования)? 쐽 что произойдет, если вы захотите перенести модуль хранения данных на уда- ленный компьютер? Стрелки между подсистемами можно рассматривать как шланги с водой. Если вам захочется «выдернуть» одну из подсистем, к ней наверняка будут подключены не- сколько шлангов. Чем больше шлангов вам нужно будет отсоединить и подключить заново, тем сильнее вы промокнете. Архитектура системы должна быть такой, чтобы замена подсистем требовала как можно меньше возни со шлангами. При должной предусмотрительности все эти вопросы можно решить, проделав немного дополнительной работы. Реализуйте коммуникацию между подсистемами на основе принципа «необходимого знания», и пусть оно будет действительно не- обходимым. Помните: проще сначала ограничить взаимодействие, а затем сделать его более свободным, чем пытаться изолировать подсистемы после написания не- скольких сотен вызовов между ними. На рис. 5-5 показано, как несколько правил коммуникации могут изменить систему, изображенную на рис. 5-4. Рис. 5-5. Определив несколько правил коммуникации, можно существенно упро- стить взаимодействие подсистем Чтобы соединения подсистем были понятными и легкими в сопровождении, ста- райтесь поддерживать простоту отношений между подсистемами. Самым простым отношением является то, при котором одна подсистема вызывает методы другой. Более сложное отношение имеет место, когда одна подсистема содержит классы другой. Самое сложное отношение — наследование классов одной подсистемы от классов другой. Придерживайтесь одного разумного правила: диаграмма системного уровня вроде той, что показана на рис. 5-5, должна быть ациклическим графом. Иначе говоря, программа не должна содержать циклических отношений, при которых класс A использует класс B, класс B использует класс C, а класс C — класс A. При работе над крупными программами и программными комплексами проек- тирование на уровне подсистем просто необходимо. Если вам кажется, что ваша 82 ЧАСТЬ II Высококачественный код программа достаточно мала, чтобы проектирование на уровне подсистем можно было пропустить, хотя бы примите это решение осознанно. Часто используемые подсистемы Некоторые типы подсистем снова и снова используются в разных системах. Ниже приведены те, что встречаются чаще всего. Подсистема бизнес-правил Бизнес-правилами называют законы, директивы, политики и процедуры, реализуемые в компьютерной системе. Например, в случае системы расчета заработной платы бизнес-правилами могли бы быть дирек- тивы налогового управления, определяющие разнообразные виды налогов. Дополнительным источником правил могло бы быть соглашение с профсоюзом, регламентирующее оплату сверхурочной работы, отпуска и т. д. При создании программы для агентства по страхованию автомобилей правила могут быть основаны на соответствующих государственных законах. Подсистема пользовательского интерфейса Изоляция компонентов пользо- вательского интерфейса в отдельной подсистеме позволяет изменять его, не влияя на остальную программу. Как правило, подсистема пользовательского интерфейса включает несколько подчиненных подсистем или классов, отвечающих за GUI, интерфейс командной строки, работу с меню, управление окнами, справочную систему и т. д. Подсистема доступа к БД Вы может скрыть детали реализации доступа к БД, чтобы большая часть программы не нуждалась в знании «грязных» подроб- ностей операций над низкоуровневыми структурами и могла работать с данными в терминах бизнес-проблемы. Подсистемы, скрывающие детали реализации, обе- спечивают важный уровень абстракции, снижающий сложность программы. Они концентрируют операции над БД в одном месте и снижают вероятность ошибок при работе с данными, а также позволяют легко изменять структуру БД без из- менения большей части программы. Подсистема изоляции зависимостей от ОС Зависимости от ОС следует изолировать в подсистеме по той же причине, что и зависимости от оборудова- ния. Если, например, вы разрабатываете программу для Microsoft Windows, зачем ограничивать себя средой Windows? Изолируйте вызовы Windows в специализи- рованной интерфейсной подсистеме, и если вам позднее захочется перенести программу на платформу Mac OS или Linux, то придется изменить только эту подсистему. Интерфейсная подсистема может быть слишком крупной, чтобы вы могли реализовать ее самостоятельно, однако такие подсистемы уже разработаны и включены в несколько коммерческих библиотек. |