Глава     2

Объектно-ориентированное проектирование


Разделы

Содержание

Когда программисты спрашивают друг друга: ╚Чем же, в конце концов, является объектно-ориентированное программирование?╩, ответ чаще всего подчеркивает синтаксические свойства таких языков, как C++ или Object Pascal, по сравнению с их более ранними, не объектно-ориентированными версиями, то есть C или Pascal. Тем самым обсуждение обычно переходит на такие предметы, как классы и наследование, пересылка сообщений, виртуальные и статические методы. Но при этом опускают наиболее важный момент в объектно-ориентированном программировании, который не имеет ничего общего с вопросами синтаксиса.

Работа на объектно-ориентированном языке (то есть на языке, который поддерживает наследование, пересылку сообщений и классы) не является ни необходимым, ни достаточным условием для того, чтобы заниматься объектно-ориентированным программированием. Как мы подчеркнули в главе═1, наиболее важный аспект в ООП═≈ техника проектирования, основанная на выделении и распределении обязанностей. Она была названа проектированием на основе обязанностей или проектированием на основе ответственности (responsibility-driven design) [Wirfs-Brock═1989b, Wirfs-Brock═1990].

2.1. Ответственность подразумевает невмешательство

Разделы

Как может констатировать любой, кто помнит себя ребенком (или кто воспитывает детей), ответственность═≈ обоюдоострый меч. Когда вы заставляете какой-либо объект (является ли он ребенком, или программной системой) быть ответственным за конкретные действия, вы ожидаете с его стороны определенного поведения, по крайней мере пока не нарушены правила. Но, в равной степени важно, что ответственность подразумевает определенный уровень независимости или невмешательства. Если вы скажете своей дочке, что она отвечает за уборку своей комнаты, вы, как правило, не стоите рядом с ней и не наблюдаете за выполнением работы═≈ это противоречило бы понятию ответственности. Вместо этого вы рассчитываете, что после выдачи распоряжения будет получен желаемый результат.

Аналогично в случае примера с цветами из главы═1, когда я передаю запрос хозяйке цветочного магазина с просьбой переслать цветы, я не задумываюсь о том, как мой запрос будет обслужен. Хозяйка цветочного магазина, раз уж она взяла на себя ответственность, действует без вмешательства с моей стороны.

Разница между традиционным и объектно-ориентированным программированием во многих отношениях напоминает различие между активным наблюдением за тем, как ребенок выполняет работу, и передачей (делегированием) ребенку ответственности за эту деятельность. Традиционное программирование основывается в основном на приказаниях чему-либо сделать что-то═≈ к примеру, модифицировать запись или обновить массив данных. Тем самым фрагмент кода привязан посредством передачи управления и соглашений о структуре данных ко многим другим разделам программной системы. Такие зависимости могут возникать через использование глобальных переменных, значений указателей или попросту из-за неправильного применения или зависимой реализации других фрагментов кода. Проектирование, основанное на ответственности, старается отсекать эти связи или по крайней мере сделать их настолько слабыми, насколько это возможно.

С первого взгляда идея кажется не более сложной, чем понятия маскировки информации и модульности, которые важны при программировании в целом, в том числе и при использовании традиционных языков. Но проектирование, основанное на распределении ответственности, поднимает маскировку данных с уровня техники до уровня искусства. Принцип маскировки информации становится жизненно важным при переходе от программирования ╚в малом╩ к программированию ╚в большом╩.

Одно из основных преимуществ ООП наблюдается, когда программные подсистемы многократно используются в разных проектах. Например, программа, управляющая моделированием (подобно той, которую мы будем разрабатывать в главе═6), может имитировать как движение бильярдных шаров по столу, так и перемещение рыбы в цистернах. Эта способность кода к многократному использованию неявным образом подразумевает, что в программном обеспечении почти не должно быть проблемно-зависимых компонентов═≈ оно должно полностью делегировать ответственность за специфичное поведение к фрагментам конкретной системы. Умению создавать подобный многократно используемый код не так просто научиться═≈ оно требует опыта, тщательного исследования учебных примеров (парадигм, в исходном значении этого слова) и использования языков программирования, в которых такое делегирование является естественным и легко выражаемым. В последующих главах мы приведем несколько примеров.

2.2. Программирование ╚в малом╩ и═╚в═большом╩

Разделы

О разработке индивидуального проекта часто говорят как о программировании ╚в малом╩, а о реализации большого проекта как о программировании ╚в большом╩.

Для программирования ╚в малом╩ характерны следующие признаки:

С другой стороны, программирование ╚в большом╩ наделяет программный проект следующими свойствами:

В то время как начинающий студент обычно знакомится с программированием ╚в малом╩, особенности многих объектно-ориентированных языков наилучшим образом понимаются при встрече с проблемами, типичными для программирования ╚в большом╩. Тем самым некоторое представление о трудностях, возникающих при разработке больших систем, является полезным для понимания ООП.

2.3. Почему надо начинать с═функционирования?

Разделы

Из-за чего процесс проектирования начинают с анализа функционирования или поведения системы? Простой ответ состоит в том, что поведение системы обычно известно задолго до остальных ее свойств.

Предшествовавшие методы разработки программного обеспечения концентрировались на таких идеях, как характеристики основных данных или же общая структура вызова функций. Но структурные элементы приложения могут быть определены только после интенсивного анализа задачи. Соответственно процесс формальной спецификации часто заканчивался созданием документа, который не понимали ни программисты, ни клиенты. Но поведение═≈ это нечто, что может быть описано в момент возникновения идеи программы и (в отличие от формальной спецификации системы) выражено в терминах, имеющих значение как для программиста, так и для клиента.

Мы проиллюстрируем проектирование на основе обязанностей (или RDD-проектирование═≈ Responsibility-Driven-Design) на учебном примере.

2.4. Учебный пример: проектирование на═основе обязанностей

Разделы

Представьте себе, что вы являетесь главным архитектором программных систем в ведущей компьютерной фирме. В один прекрасный день ваш начальник появляется в офисе с идеей, которая, как он надеется, будет очередным успехом компании. Вам поручают разработать систему под названием Interactive Intelligent Kitchen Helper (Интерактивный разумный кухонный помощник).

Задача, поставленная перед вашей командой программистов, сформулирована в нескольких скупых словах (написанных на чем-то, что оказывается использованной обеденной салфеткой, причем почерком, принадлежащим вашему начальнику).

2.4.1. Интерактивный разумный кухонный помощник

Учебный пример: проектирование на═основе обязанностей

Программа ╚Интерактивный разумный кухонный помощник╩ (Interactive Intelligent Kitchen Helper, IIKH) предназначена для персональных компьютеров. Ее цель═≈ заменить собой набор карточек с рецептами, который можно встретить почти в каждой кухне. Но помимо ведения базы данных рецептов, IIKH помогает в планировании питания на длительный период═≈ например, на неделю вперед. Пользователь программы IIKH садится за компьютер, просматривает базу данных рецептов и в диалоговом режиме определяет меню на весь требуемый период.

Как это обычно бывает при первоначальном описании многих программных систем, спецификация для IIKH в значительной степени двусмысленна в отношении ряда важных пунктов. Кроме того, проект и разработка программной системы IIKH потребует совместных усилий нескольких программистов. Тем самым первоначальная цель команды разработчиков состоит в том, чтобы сделать ясными двусмысленные места и наметить разбиение проекта на компоненты, с тем чтобы распределить их между отдельными членами команды.

Краеугольным камнем в ООП является характеристика программного обеспечения в терминах поведения, то есть в терминах действий, которые должны быть выполнены. Мы увидим воплощение в жизнь этого принципа на многих уровнях процесса разработки IIKH. Первоначально команда попытается охарактеризовать на очень высоком уровне абстрагирования поведение приложения в целом. Затем она займется описанием поведения различных программных подсистем. И только тогда, когда все аспекты поведения будут выделены и описаны, программисты-разработчики приступят к этапу кодирования. В следующих разделах мы будем отслеживать этапы работы команды программистов при создании данного приложения.

2.4.2. Работа по сценарию

Учебный пример: проектирование на═основе обязанностей

Первой задачей является уточнение спецификации. Как мы уже заметили, исходные спецификации почти всегда двусмысленны и непонятны во всем, кроме наиболее общих положений. На этом этапе ставится несколько целей. Одной из них является лучшее понимание и ощущение того, чем будет конечный продукт (принцип ╚посмотри и почувствуй╩ для проектирования системы). Затем эта информация может быть возвращена назад клиенту (в данном случае вашему начальнику), чтобы увидеть, находится ли она в соответствии с исходной концепцией. Вероятно и, возможно, неизбежно то, что спецификации для конечного продукта будут изменяться во время разработки программной системы, и поэтому важно, чтобы проект мог легко включать в себя новые идеи, а также чтобы потенциально возможные исправления были выявлены как можно раньше═≈ см. раздел═2.6.2. ╚Готовность к изменениям╩. На этом же этапе проводится обсуждение структуры будущей программной системы. В частности, действия, осуществляемые программной системой, разбиваются на компоненты.

2.4.3. Идентификация компонент

Учебный пример: проектирование на═основе обязанностей

Создание сложной физической системы, подобной зданию или двигателю автомобиля, упрощается с помощью разбиения проекта на структурные единицы. Точно так же разработка программного обеспечения облегчается после выделения отдельных компонент программы. Компонента═≈ это просто абстрактная единица, которая может выполнять определенную работу (то есть иметь определенные обязанности). На этом этапе нет необходимости знать в точности то, как задается компонента или как она будет выполнять свою работу. Компонента может в конечном итоге быть преобразована в отдельную функцию, структуру или класс, или же в совокупность других компонент (шаблон). На этом уровне разработки имеются две важные особенности:

Позднее мы поговорим о второй особенности подробнее. Сейчас мы просто занимаемся определением обязанностей компонент.

2.5. CRC-карточка═≈ способ записи обязанностей

Разделы

Чтобы выявить отдельные компоненты и определить их обязанности, команда программистов прорабатывает сценарий системы. То есть воспроизводится запуск приложения, как если бы оно было уже готово. Любое действие, которое может произойти, приписывается некоторой компоненте в качестве ее обязанности.


Рис.═2.1.5. CRC-карточка

В качестве составной части этого процесса полезно изображать компоненты с помощью небольших индексных карточек. На лицевой стороне карточки написаны имя компоненты, ее обязанности и имена других компонент, с которыми она должна взаимодействовать. Такие карточки иногда называются CRC-карточками от слов Component, Responsibility, Collaborator (компонента, обязанность, сотрудники) [Beck═1989]. По мере того как для компонент выявляются обязанности, они записываются на лицевой стороне CRC-карточки.

2.5.1. Дайте компонентам физический образ

CRC-карточка═≈ способ записи обязанностей

При проработке сценария полезно распределить CRC-карточки между различными членами проектной группы. Человек, имеющий карточку, которая представляет определенную компоненту, записывает ее обязанности и исполняет функции заменителя программы во время моделирования сценария. Он описывает действия программной системы, передавая ╚управление╩ следующему члену команды, когда программная система нуждается в услугах других компонент.

Преимущество CRC-карточек в том, что они широко доступны, недороги и с них можно стирать информацию. Это стимулирует экспериментирование, поскольку альтернативные проекты могут быть испробованы, изучены и отброшены с минимальными затратами. Физическое разделение карточек стимулирует интуитивное понимание важности логического разделения компонент, что помогает сделать упор на связности внутри модулей и зацеплении между модулями (которые вкратце будут описаны ниже). Небольшой размер индексной карточки служит хорошей оценкой примерной сложности отдельного фрагмента═≈ компоненты, которой приписывается больше задач, чем может поместиться на ее карточке, вероятно, является излишне сложной, и должно быть найдено более простое решение. Может быть, следует пересмотреть разделение обязанностей или разбить компоненту на две.

2.5.2. Цикл ╚что/кто╩

CRC-карточка═≈ способ записи обязанностей

Как мы заметили в начале нашего обсуждения, выделение компонент производится во время процесса мысленного представления работы системы. Часто это происходит как цикл вопросов ╚что/кто╩. Во-первых, команда программистов определяет: что требуется делать? Это немедленно приводит к вопросу: кто будет выполнять действие? Теперь программная система в значительной мере становится похожа на некую организацию, скажем, карточный клуб. Действия, которые должны быть выполнены, приписываются некоторой компоненте в качестве ее обязанностей.

Популярная наклейка от жевательной резинки утверждает, что время от времени может и должно спонтанно происходить необъяснимое. (Наклейка от жевательной резинки использует чуть более короткую фразу.) Мы знаем, однако, что в реальной жизни это вряд ли справедливо. Если происходит некоторое действие, должен быть и агент, которому предписано выполнять это действие. Точно так же как в карточном клубе каждое действие приписано определенным индивидуумам, при организации объектно-ориентированной программы каждое действие является обязанностью некоторой компоненты. Секрет хорошего объектно-ориентированного проекта состоит в том, чтобы установить агента для каждого действия.

2.5.3. Документирование

CRC-карточка═≈ способ записи обязанностей

На этом этапе следует начать разработку документации. Два документа должны являться существенными составными частями любой программной системы: руководство пользователя и проектная документация системы. Работа над каждым из них может начинаться до того, как написана первая строчка программного кода.

Руководство пользователя описывает взаимодействие с системой с точки зрения пользователя. Это═≈ отличное средство проверки того, что концепция команды программистов-разработчиков соответствует мнению клиента. Поскольку решения, принятые в процессе проработки сценария, соответствуют действиям, которые потребуются от пользователя при использовании программы, то написание руководства пользователя естественным образом увязывается с процессом проработки сценария.

Перед тем как написан какой-либо кусок кода, мышление команды программистов во многом похоже на сознание конечных пользователей. То есть именно на этом этапе разработчики могут наиболее легко предугадать те вопросы, на которые новичку-пользователю понадобятся ответы.

Второй существенный документ═≈ проектная документация. Она протоколирует основные решения, принятые при планировании программы, и, следовательно, должна создаваться в тот момент, когда эти решения еще свежи в памяти создателей, а не годом позже. Зачастую много проще написать общее глобальное описание программной системы в начале разработки. Затем, естественно, совершается переход к уровню отдельных компонент или модулей.

Хотя в равной мере важно документировать программу на уровне модулей, слишком большое внимание к деталям организации каждого фрагмента сделает сложным для последующих программистов, осуществляющих сопровождение программной системы, формирование общей картины приложения.

CRC-карточки являются одним из видов проектной документации, но многие другие важные решения не отражены в них. Аргументы за и против каждой важной альтернативы при проектировании должны записываться, равно как и факторы, которые повлияли на конечное решение. Должен вестись протокол или дневник хода проекта. Как руководство пользователя, так и проектная документация уточняются и изменяются в процессе работы в точном соответствии с тем, как модифицируется собственно программа.

2.6. Компоненты и поведение

Разделы

Вернемся к программе IIKH. Команда разработчиков решает, что когда система начинает работу, пользователь видит привлекательное информационное окно (см. рис.═2.1). Ответственность за его отображение приписана компоненте, названной Greeter. Некоторым, пока еще не определенным образом (с помощью всплывающих меню, кнопок, нажатия на клавиши клавиатуры или использования сенсорного экрана) пользователь выбирает одно из нескольких действий.

Первоначально планируется только пять действий:

  1. Просмотреть базы данных с рецептами, но без ссылок на какой-то конкретный план питания.
  2. Добавить новый рецепт в базу данных.
  3. Редактировать или добавить комментарии к существующему рецепту.
  4. Пересмотреть существующий план в отношении некоторых продуктов.
  5. Создать новый план питания.

Эти действия естественным образом разбиваются на две группы. Первые три действия связаны с базой данных рецептов, последние два═≈ с планированием питания. В результате команда принимает следующее решение: создать компоненты, соответствующие этим двум обязанностям. Продолжая прорабатывать сценарий, планирование питания на время игнорируем и переходим к уточнению действий, связанных с компонентой Recipe Database. На рис.═2.2 показан


Рис.═2.2. CRC-карточка для класса заставки Greeter

первоначальный вид CRC-карточки для компоненты Greeter.

В широком смысле обязанность компоненты, работающей с базой данных,═≈ просто поддерживать записи с рецептами.

Мы уже выделили три аспекта этой компоненты: Recipe Database должна обеспечивать просмотр библиотеки существующих рецептов, редактирование рецептов, включение новых рецептов в базу данных.

2.6.1. Отложенные решения

Компоненты и поведение

В конце концов придется решить, как пользователь станет просматривать базу данных. Например, должен ли он сначала входить в список категорий таких, как ╚супы╩, ╚салаты╩, ╚горячие блюда╩, ╚десерты╩?

С другой стороны, может ли пользователь задавать ключевые слова для ограничения области поиска, включая список ингредиентов (╚миндаль╩, ╚клубника╩, ╚сыр╩)? Или же использовать список предварительно заданных ключевых слов (╚любимые пирожные Боба╩)? Следует ли применять полосы прокрутки (scroll bars) или имитировать закладки в виртуальной книжке? Размышлять об этих предметах доставляет удовольствие, но важно то, что нет необходимости принимать конкретные решения на данном этапе проектирования (см. раздел═2.6.2. ╚Готовность к изменениям╩). Поскольку они влияют только на отдельную компоненту и не затрагивают функционирование остальных частей системы, то все, что надо для продолжения работы над сценарием,═≈ это информация о том, что пользователь может выбрать конкретный рецепт.

2.6.2. Готовность к изменениям

Компоненты и поведение

Как было сказано, единственное, что является постоянным в жизни,═≈ неизбежность изменений. То же самое справедливо для программного обеспечения. Независимо от того как тщательно вы пытаетесь разработать исходные спецификации и проект программной системы, почти наверняка изменения в желаниях или потребностях пользователя будут вызывать соответствующие исправления в программе (зачастую в течение всего жизненного цикла системы). Разработчики должны предвидеть это и планировать свои действия соответствующим образом:

2.6.3. Продолжение работы со сценарием

Компоненты и поведение

Каждый кулинарный рецепт будет идентифицироваться с конкретной программной компонентой. Если рецепт выбран пользователем, управление передается объекту, ассоциированному с рецептом. Рецепт должен содержать определенную информацию. В основном она состоит из списка ингредиентов и действий, необходимых для трансформации составляющих в конечный продукт. Согласно нашему сценарию компонента-рецепт должна также выполнять и другие действия. Например, она будет отображать рецепт на экране. Пользователь получит возможность снабжать рецепт аннотацией, а также менять список ингредиентов или набор инструкций. С другой стороны, пользователь может потребовать распечатать рецепт. Все эти действия являются обязанностью компоненты Recipe. (Временно мы продолжим описание Recipe как отдельно взятого объекта. На этапе проектирования мы можем рассматривать его как прототип многочисленных объектов-рецептов. Позднее мы вернемся к обсуждению альтернативы ╚одиночная компонента═≈ множество компонент╩.)

Определив вчерне, как осуществить просмотр базы данных, вернемся к ее блоку управления и предположим, что пользователь хочет добавить новый рецепт. В блоке управления базой данных неким образом определяется, в какой раздел поместить новый рецепт (в настоящее время детали нас не интересуют), запрашивается имя рецепта и выводится окно для набора текста. Таким образом, эту задачу естественно отнести к той компоненте, которая отвечает за редактирование рецептов.

Вернемся к блоку Greeter. Планирование меню, как вы помните, было поручено программной компоненте Plan Manager. Пользователь должен иметь возможность сохранить существующий план. Следовательно, компонента Plan Manager может запускаться либо в результате открытия уже существующего плана, либо при создании нового. В последнем случае пользователя необходимо попросить ввести временные интервалы (список дат) для нового плана. Каждая дата ассоциируется с отдельной компонентой типа Date. Пользователь может выбрать конкретную дату для детального исследования═≈ в этом случае управление передается соответствующей компоненте Date. Компонента Plan Manager должна уметь распечатывать меню питания на планируемый период. Наконец, пользователь может попросить компоненту Plan Manager сгенерировать список продуктов на указанный период.

В компоненте Date хранятся следующие данные: список блюд на соответствующий день и (необязательные) текстовые комментарии, добавленные пользователем (информация о днях рождения, юбилейные даты, напоминания и═т.═д.). Что должна уметь компонента? Прежде всего выводить на экран вышеперечисленные данные. Кроме того, в ней должна быть предусмотрена функция печати. В случае желания пользователя более детально ознакомиться с тем или иным блюдом, следует передать управление компоненте Meal.

В компоненте Meal хранится подробная информация о блюде. Не исключено, что у пользователя окажется несколько рецептов одного блюда. Поэтому необходимо добавлять и удалять рецепты. Кроме того, желательно иметь возможность распечатать информацию о том или ином блюде. Разумеется, должен быть обеспечен вывод на экран. Пользователю, вероятнее всего, захочется обратиться к еще каким-нибудь рецептам═≈ следовательно, необходимо наладить контакт с базой данных рецептов. Раз так, компоненты Meal и база данных должны взаимодействовать между собой.

Далее команда разработчиков продолжает исследовать все возможные сценарии. Необходимо предусмотреть обработку исключительных ситуаций. Например, что происходит, если пользователь задает ключевое слово для поиска рецепта, а подходящий рецепт не найден? Как пользователь сможет прервать действие (например, ввод нового рецепта), если он не хочет продолжать дальше? Все это должно быть изучено. Ответственность за обработку подобных ситуаций следует распределить между компонентами.

Изучив различные сценарии, команда разработчиков в конечном счете решает, что все действия могут быть надлежащим образом распределены между шестью компонентами (рис.═2.3). Компонента Greeter взаимодействует только с Plan Manager и Recipe Database. Компонента Plan Manager ╚зацепляется╩ только с Date, а та в свою очередь═≈ с Meal. Компонента Meal обращается к Recipe Manager и через посредство этого объекта к конкретным рецептам.


Рис.═2.3. Взаимосвязь между компонентами программы IIKH

2.6.4. Диаграммы взаимодействия

Компоненты и поведение

Схема, изображенная на рис.═2.3, хорошо подходит для отображения статических связей между компонентами. Но она не годится для описания динамического взаимодействия во время выполнения программы. Для этих целей используются диаграммы взаимодействия. На рис.═2.4 показана часть диаграммы взаимодействия для программы IIKH. Время движется сверху вниз. Каждая компонента представлена вертикальной линией. Сообщение от одной компоненты к другой изображается горизонтальной стрелкой между вертикальными линиями. Возврат управления (и, возможно, результата) в компоненту представлен аналогичной стрелкой. Некоторые авторы используют для этой цели пунктирную стрелку. Комментарии справа от рисунка более подробно объясняют взаимодействие.

Благодаря наличию оси времени диаграмма взаимодействия лучше описывает последовательность событий в процессе работы программы. Поэтому диаграммы взаимодействия являются полезным средством документирования для сложных программных систем.


Рис. 2.4. Пример диаграммы взаимодействия

2.7. Компоненты программы

Разделы

В этом разделе мы исследуем компоненты программы более подробно. За этим внешне простым понятием прячется много нетривиальных аспектов (что, впрочем, справедливо почти для всех понятий за исключением совсем элементарных).

2.7.1. Поведение и состояние

Компоненты программы

Мы уже видели, что компоненты характеризуются своим поведением, то есть тем, что они должны делать. Но компоненты также хранят определенную информацию. Возьмем, к примеру, компоненту-прототип Recipe из программы IIKH. Можно представить ее себе как пару ╚поведение≈состояние╩.

Не все компоненты обязаны иметь состояние. Например, у компоненты Greeter, вероятно, не будет внутренних данных, поскольку ей ни к чему помнить какую-либо информацию. Однако большинство компонент характеризуется и поведением, и состоянием.

2.7.2. Экземпляры и классы

Компоненты программы

Разделив понятия о состоянии и поведении, мы можем теперь затронуть тему, которую ранее избегали. Вероятно, в реальном приложении будет много рецептов. Однако все они будут вести себя одинаково. Отличается только состояние: список ингредиентов и инструкций по приготовлению. На ранних стадиях разработки нас должно интересовать поведение, общее для всех рецептов. Детали, специфические для отдельного рецепта, не важны.

Термин класс используется для описания множества объектов с похожим поведением. Мы увидим в последующих главах, что класс применяется как синтаксический механизм почти во всех объектно-ориентированных языках. Конкретные представители класса называются экземплярами. Заметим, что поведение ассоциировано с классом, а не с индивидуальными представителями. То есть все экземпляры класса воспринимают одни и те же команды и выполняют их сходным способом. С другой стороны, состояние является индивидуальным. Мы видим это на примере различных экземпляров класса Recipe. Все они могут выполнять одни и те же действия (редактирование, вывод на экран, печать), но используют различные данные. Мы рассмотрим концепцию класса более подробно в главе═3.

2.7.3. Зацепление и связность

Компоненты программы

Двумя важными понятиями при разработке программ являются зацепление (coupling) и связность (cohesion). Связность═≈ это мера того, насколько отдельная компонента образует логически законченную, осмысленную единицу. Высокая связность достигается объединением в одной компоненте соотносящихся (в том или ином смысле) друг с другом функций. Наиболее часто функции оказываются связанными друг с другом при необходимости иметь доступ к общим данным. Именно это объединяет разные части компоненты Recipe.

С другой стороны, зацепление характеризует взаимозависимость между компонентами программы. В общем случае желательно уменьшить степень зацепления как только возможно, поскольку связи между компонентами программы препятствуют их модификации и мешают дальнейшей разработке или повторному использованию в других программах.

В частности, зацепление возникает, если одна программная компонента должна иметь доступ к данным (состоянию) другой компоненты. Следует избегать подобных ситуаций. Возложите обязанность осуществлять доступ к данным на компоненту, которая ими владеет. Например, в случае с нашим проектом кажется, что ответственность за редактирование рецептов должна лежать на компоненте Recipe Database, поскольку именно в ней впервые возникает в этом необходимость. Но тогда объект Recipe Database должен напрямую манипулировать состоянием отдельных рецептов (их внутренними данными: списком ингредиентов и инструкциями по приготовлению). Лучше избежать столь тесного зацепления, передав обязанность редактирования непосредственно рецепту.

Более подробно о связности и зацеплении, а также о соответствующей технике программирования рассказывается в главе═17.

2.7.4. Интерфейс и реализация модуля═≈ принципы Парнаса

Компоненты программы

Идея характеризации компонент программы через их поведение имеет одно чрезвычайно важное следствие. Программист знает, как использовать компоненту, разработанную другим программистом, и при этом ему нет необходимости знать, как она реализована. Например предположим, что шесть компонент приложения IIKH создаются шестью программистами. Программист, разрабатывающий компоненту Meal, должен обеспечить просмотр базы данных с рецептами и выбор отдельного рецепта при составлении блюда. Для этого компонента Meal просто вызывает функцию browse, привязанную к компоненте Recipe Database. Функция browse возвращает отдельный рецепт Recipe из базы данных.

Все это справедливо вне зависимости от того, как конкретно реализован внутри Recipe Database просмотр базы данных.

Мы прячем подробности реализации за фасадом интерфейса. Происходит маскировка информации. Говорят, что компонента инкапсулирует поведение, если она умеет выполнять некоторые действия, но подробности, как именно это делается, остаются скрытыми. Это естественным образом приводит к двум различным представлениям о программной системе. Вид со стороны интерфейса═≈ это лицевая сторона; ее видят другие программисты. В интерфейсной части описывается, что умеет делать компонента. Вид со стороны реализации═≈ это ╚изнанка╩, видимая только тем, кто работает над конкретной компонентой. Здесь определяется, как компонента выполняет задание.

Разделение интерфейса и реализации является, возможно, наиболее важной идеей в программировании. Ее непросто объяснить студентам. Маскировка информации имеет значение в основном только в контексте программных проектов, в которых занято много людей. При таких работах лимитирующим фактором часто является не количество привлеченных людей, а частота обмена информацией и данными как между программистами, так и между разрабатываемыми ими программными системами. Как будет показано ниже, компоненты часто разрабатываются параллельно разными программистами в изоляции друг от друга.

Интерес к многократному использованию программных компонент общего назначения в разных проектах возрастает. Для осуществления подобного, связи между различными частями системы должны быть минимальны и прозрачны.

Как мы уже отмечали в предыдущей главе, эти идеи были сформулированы специалистом по информатике Дэвидом Парнасом в виде правил, часто называемых принципами Парнаса:

Следствие принципа отделения интерфейса от реализации состоит в том, что программист может экспериментировать с различными алгоритмами, не затрагивая остальные компоненты программы.

2.8. Формализация интерфейса

Разделы

Продолжим разработку программы IIKH. На следующих нескольких этапах уточняется описание компонент. Сначала формализуются способы взаимодействия.

Следует определить, как будет реализована каждая из компонент. Компонента, характеризуемая только поведением (не имеющая внутреннего состояния), может быть оформлена в виде функции. Например, компоненту, заменяющую все заглавные буквы в текстовой строке на строчные, разумнее всего сделать именно так. Компоненты с многими функциями лучше реализовать в виде классов. Каждой обязанности, перечисленной на CRC-карточке компоненты, присваивается имя. Эти имена станут затем названиями функций или процедур. Вместе с именами определяются типы аргументов, передаваемых функциям. Затем описывается (вся) информация, содержащаяся внутри компоненты. Если компоненте требуются некие данные для выполнения конкретного задания, их источник (аргумент функции, глобальная или внутренняя переменная) должен быть явно описан.

2.8.1. Выбор имен

Формализация интерфейса

Имена, связанные с различными действиями, должны тщательно подбираться. Шекспир сказал, что переименование не меняет сути объекта 3 , но определенно не все имена будут вызывать в воображении слушателя одинаковые мысленные образы.

Комментарий
3 ╚Что значит имя? Роза пахнет розой, хоть розой назови ее, хоть нет. Ромео под любым названьем был бы тем верхом совершенств, какой он есть╩.═≈ Вильям Шекспир, ╚Ромео и Джульетта╩, действие II, сцена 2 (пер. Бориса Пастернака).

Как давно известно правительственным чиновникам, неясные и используемые в переносном смысле имена придают отпугивающий вид даже простейшим действиям. Выбор удобных имен необычайно важен. Они должны быть внутренне совместимы, значимы, коротки, содержательны. Часто значительное время тратится на нахождение правильного набора имен для выполняемых заданий и объектов. Являясь далеко не бесплодным и не бесполезным процессом, надлежащий выбор имен на ранней стадии проектирования значительно упрощает и облегчает дальнейшую разработку.

Были предложены следующие положения общего характера, регулирующие этот процесс [Keller═1990]:

Как только для всех действий выбраны имена, CRC-карточка для каждой компоненты переписывается заново с указанием имен функций и списка формальных аргументов. Пример CRC-карточки для компоненты Date приведен на рис.═2.5. Что осталось не установленным, так это то, как именно каждая компонента будет выполнять указанные действия.

Необходимо еще раз ╚прокрутить╩ сценарий более детально, чтобы гарантировать, что все действия учтены и вся необходимая информация имеется и доступна для соответствующих компонент.


Рис.═2.5. Обновленная CRC-карточка для компоненты Date

2.9. Выбор представления данных

Разделы

На данном этапе, если только это не было сделано раньше, происходит разделение команды разработчиков на группы, каждая из которых отвечает за конкретные компоненты программы. Задача теперь состоит в переходе от описания компоненты к конкретному коду. Главное здесь═≈ проектирование структур данных, которые будут использоваться каждой из подсистем для хранения внутренней информации, необходимой для выполнения предписанных обязанностей.

Именно на этом этапе в игру вступают классические структуры данных, используемые в информатике. Выбор структуры данных является важным, центральным моментом с точки зрения проектирования. Если представление данных выбрано правильно, то код, используемый компонентой при выполнении ее обязанностей, становится почти самоочевидным.

Структуры данных должны точно соответствовать рассматриваемой задаче. Неправильный выбор структуры может привести к сложным и неэффективным программам.

На этом же этапе описание поведения компонент должно быть преобразовано в алгоритмы. Реализованные функции затем сопоставляются с потребностями компоненты, являющейся клиентом данного фрагмента, чтобы гарантировать, что все ее запросы оказываются выполненными и что все необходимые для ее работы данные являются доступными.

2.10. Реализация компонент

Разделы

Когда проект в целом определен и разбит на подсистемы, следующим шагом является реализация компонент. Если предыдущие этапы были выполнены корректно, каждая обязанность или поведение будут кратко охарактеризованы. Задачей данного этапа является воплощение желаемых действий на компьютерном языке. В следующем разделе мы опишем некоторые из наиболее типичных эвристических подходов, используемых с этой целью.

Если это не было сделано ранее (например, как часть этапа спецификации всей системы), то теперь можно решить, как будут устроены внутренние детали отдельных компонент. В случае нашего примера на данном этапе следует подумать, как пользователь будет просматривать базу данных рецептов.

По мере того как программные проекты с большим числом разработчиков становятся нормой, все реже встречается ситуация, когда один√единственный программист отвечает за всю систему. Наиболее важные для программиста качества═≈ это способность понимать, как отдельный фрагмент кода подключается к более высокому программному уровню, и желание работать совместно с остальными членами команды.

Часто в процессе реализации одной компоненты становится ясно, что некоторая информация или действия должны быть присвоены совсем другой компоненте, которая работала бы ╚за сценой╩, не видимо для пользователя. Такие компоненты иногда называют суфлерами. Мы встретим соответствующие примеры в некоторых последующих главах.

Важной частью анализа и кодирования на этом этапе является полная характеризация и документирование необходимых предварительных условий, которые требуются программной компоненте для выполнения задания. Также следует проверить, правильно ли работает программная компонента, если вызвать ее с правильными входными значениями. Это подтвердит корректность алгоритмов, использованных при реализации компоненты.

2.11. Интеграция компонент

Разделы

Когда индивидуальные подсистемы разработаны и протестированы, они должны быть интегрированы в конечный продукт. Это делается поэтапно. Начиная с элементарной основы, к системе постепенно добавляются (и тестируются) новые элементы. При этом для еще не реализованных частей используются так называемые заглушки (stubs)═≈ подпрограммы без какого-либо поведения или с ограниченной функциональностью.

Например, при разработке программы IIKH было бы разумным начать интегрирование с компоненты Greeter. Чтобы протестировать ее в изоляции от остальных блоков программы, потребуются заглушки для управляющего кода базы данных с рецептами Recipe Database и блока управления планированием питания Meal Plan. Заглушки просто должны выдавать информационные сообщения и возвращать управление. Таким образом, команда разработчиков компоненты Greeter сможет протестировать различные аспекты данной компоненты (например, проверить, вызывает ли нажатие клавиши нужную реакцию). Отладку отдельных компонент часто называют тестированием блоков.

Затем заглушки заменяются более серьезным кодом. Например, вместо заглушки для компоненты Recipe Database можно вставить реальную подсистему, сохранив заглушки для остальных фрагментов. Тестирование продолжается до тех пор, пока не станет ясно, что система работает правильно. Этот процесс называют тестированием системы в целом.

Когда все заглушки заменены работающими компонентами, приложение завершено. Процесс тестирования заметно облегчается, если число связей между компонентами невелико═≈ в этом случае не придется писать множество программ-заглушек.

Во время интеграции системы вполне возможно, что ошибка, проявляющаяся в одной из программных систем, вызвана некорректным кодом в другом фрагменте. Тем самым ошибки, выявляемые в процессе интеграции, приводят к необходимости исправлять некоторые компоненты. Вслед за этим измененные компоненты должны вновь тестироваться изолированно перед очередной попыткой интеграции. Повторная прогонка разработанных ранее тестовых примеров, выполняемая после изменений в компоненте программы, иногда называется регрессионным тестированием (regression testing).

2.12. Сопровождение и развитие

Разделы

Хотелось бы, чтобы с передачей пользователю функционирующего приложения, работа команды разработчиков завершалась. К сожалению, такого почти никогда не происходит. Необходимо дополнительное сопровождение программного обеспечения. Вот некоторые причины, вызывающие его неизбежность:

Хороший проект предусматривает неизбежность изменений и подготавливает их с самого начала.

Упражнения

Разделы

  1. Опишите распределение обязанностей в организации, которая включает по крайней мере шесть членов. Рассмотрите учебное заведение (студенты, преподаватели, директор, гардеробщик), фирму (совет директоров, президент, рабочий) и клуб (президент, вице-президент, рядовой член). Опишите обязанности каждого члена организации и его сотрудников (если они есть).
  2. Создайте с помощью диаграммы взаимодействия сценарий для организации из упражнения═1.
  3. Для типичной карточной игры опишите программную систему, которая будет взаимодействовать с пользователем в качестве противоположного партнера. Типичные компоненты должны включать игровой стол и колоду карт.
  4. Опишите программную систему для управления ATM (Automatic Teller Machine). Поскольку слово Teller достаточно многозначно (рассказчик, счетчик голосов при выборах, кассир в банке, диктор радиолокационной станции ПВО и т.═д.), то у вас имеется большая свобода в выборе предназначения этой машины. Приведите диаграммы взаимодействия для различных сценариев использования этой машины.

Объектно-ориентированное проектирование