Объектно-ориентированный анализ и проектирование с примерами приложений на С++

Буч Гради

ЧАСТЬ ВТОРАЯ Метод

 

 

Глава 5 Обозначения

 

Составление диаграмм - это еще не анализ и не проектирование. Диаграммы позволяют описать поведение системы (для анализа) или показать детали архитектуры (для проектирования). Если вы понаблюдаете за работой инженера (программиста, технолога, химика, архитектора), вы быстро убедитесь, что будущая система формируется в сознании разработчика и только в нем. Когда, спустя некоторое время, система будет понятна в общих чертах, она скорее всего будет представлена на таких высокотехнологичных носителях, как тетрадные листы, салфетки или старые конверты [].

Однако, хорошо продуманная и выразительная система обозначений очень важна для разработки. Во-первых, общепринятая система позволяет разработчику описать сценарий или сформулировать архитектуру и доходчиво изложить свои решения коллегам. Символ транзистора понятен всем электронщикам мира. Аналогично, над проектом жилого дома, разработанным архитекторами в Нью-Йорке, строителям из Сан-Франциско скорее всего не придется долго ломать голову, решая, как надо расположить двери, окна или электрическую проводку. Во-вторых, как подметил Уайтхед в своей основополагающей работе по математике, "Освобождая мозг от лишней работы, хорошая система обозначений позволяет ему сосредоточиться на задачах более высокого порядка" []. В-третьих, четкая система обозначений позволяет автоматизировать большую часть утомительной проверки на полноту и правильность. Как говорится в отчете управления оборонных исследований:

"Разработка программного обеспечения всегда будет кропотливой работой... Хотя наши машины могут выполнять рутинную работу и помогать нам держать нить рассуждений, разработка концепций останется прерогативой человека... Что всегда будет в цене - это искусство построения концептуальной структуры, а заботы об ее описании уйдут в прошлое" [].

 

5.1. Элементы обозначений

Необходимость разных точек зрения

Невозможно охватить все тонкие детали сложной программной системы одним взглядом. Как отмечают Клейн и Джингрич: "Необходимо понять как функциональные, так и структурные свойства объектов. Следует уяснить также таксономическую структуру классов объектов, используемые механизмы наследования, индивидуальное поведение объектов и динамическое поведение системы в целом. Эта задача в чем-то аналогична показу футбольного или теннисного матча, когда для вразумительной передачи происходящего действия требуется несколько камер, расположенных в разных углах спортивной площадки. Каждая камера передает свой аспект игры, недоступный другим камерам" [].

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

Наша система обозначений богата деталями, но это не означает, что в каждом проекте необходимо использовать все ее аспекты. На практике для описания большей части итогов анализа и проектирования достаточно некоторого малого подмножества этой системы; один наш коллега называет такие облегченные обозначения "нотацией Booch Lite" (сокращенная нотация Буча). В процессе знакомства с нашей системой обозначений мы четко выделим это подмножество. Зачем же тогда беспокоиться о деталях системы обозначений, не вошедших в минимальное подмножество? В основном они нужны, чтобы выражать некоторые важные тактические решения; а некоторые из них предназначены для создания программных инструментов CASE. Мы будем называть их дополнениями.

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

 Рис. 5-1. Объектные модели.

Различные языки программирования описывают одни и те же понятия различно. Наша система обозначений не зависит от конкретного языка. Конечно, некоторые ее элементы могут не иметь аналогов в языке, на котором будет написана проектируемая система. В этом случае просто не надо ими пользоваться. Например, в Smalltalk не бывает свободных подпрограмм, следовательно, в проект нет смысла закладывать утилиты классов. Аналогично, C++ не поддерживает метаклассы, следовательно, этот элемент должен быть исключен. Но нет ничего предосудительного в подгонке обозначений под конструкции выбранного языка. Например, в CLOS операции можно снабдить специальными пометками, чтобы определить методы :before, :after и :around. Подобным же образом, в системе автоматического проектирования для C++ вместо спецификаций класса можно пользоваться прямо заголовочными файлами.

Цель этой главы - определить синтаксис и семантику наших обозначений для объектно-ориентированного анализа и проектирования. В качестве примера мы будем использовать гидропонную теплицу, которая рассматривалась в главе 2. В настоящей главе не обсуждается, как, собственно, были получены представленные в примерах решения: это задача главы 6. В главе 7 обсуждаются практические аспекты этого процесса, а в главах 8-12 система обозначений демонстрируется в деле на серии примеров разработки приложений.

Модели и ракурсы

В главе 3 мы объяснили, что такое классы и объекты, а также какова связь между ними. Как показано на рис. 5-1, при принятии решений в анализе и проектировании полезно рассмотреть взаимодействие классов и объектов в двух измерениях: логическом/физическом и статическом/динамическом. Оба этих аспекта необходимы для определения структуры и поведения объектной системы.

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

Рассмотрим для примера систему, включающую в себя несколько сотен классов; эти классы образуют часть модели. Невозможно, а на самом деле и не нужно представлять все классы и их связи на единственной диаграмме. Вместо этого мы можем описать модель в нескольких диаграммах классов, каждая из которых представляет только один ее ракурс. Одна диаграмма может показывать структуру наследования некоторых ключевых классов; другая - транзитивное замыкание множества всех классов, используемых конкретным классом. Когда модель "устоится" (придет в устойчивое состояние), все такие диаграммы становятся семантически согласованными друг с другом и с моделью. Например, если по данному сценарию (который мы описываем на диаграмме объектов), объект A посылает сообщение M объекту B, то M должно быть прямо или косвенно определено в классе B. В соответствующей диаграмме классов должна быть надлежащая связь между классами A и B, так, чтобы экземпляры класса A действительно могли посылать сообщение M.

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

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

Логическая и физическая модели

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

При анализе мы должны задать следующие вопросы:

• Каково требуемое поведение системы?

• Каковы роли и обязанности объектов по поддержанию этого поведения?

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

При проектировании мы должны задать следующие вопросы относительно архитектуры системы:

• Какие существуют классы и какие есть между ними связи?

• Какие механизмы регулируют взаимодействие классов?

• Где должен быть объявлен каждый класс?

• Как распределить процессы по процессорам и как организовать работу каждого процессора, если требуется обработка нескольких процессов?

Чтобы ответить на эти вопросы, мы используем следующие диаграммы:

• диаграммы классов

• диаграммы объектов

• диаграммы модулей

• диаграммы процессов.

Статическая и динамическая модели

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

• диаграммами переходов из одного состояния в другое

• диаграммами взаимодействия.

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

Инструменты проектирования

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

 

5.2. Диаграммы классов

Существенное: классы и отношения между ними

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

Два главных элемента диаграммы классов - это классы и их основные отношения.

Классы. На рис. 5-2 показано обозначение для представления класса на диаграмме. Класс обычно представляют аморфным объектом, вроде облака [Выбор графических обозначении - это трудная задача. Требуется осторожно балансировать между выразительностью и простотой, так что проектирование значков - это в большой степени искусство, а не наука. Мы взяли облачко из материалов корпорации Intel, документировавшей свою оригинальную объектно-ориентированную архитектуру iAPX432 []. Форма этого образа намекает на расплывчатость границ абстракции, от которых не ожидается гладкости и простоты. Пунктирный контур символизирует то, что клиенты оперируют обычно с экземплярами этого класса, а не с самим классом. Можно заменить эту форму прямоугольником, как сделал Румбах []:  

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

Каждый класс должен иметь имя; если имя слишком длинно, его можно сократить или увеличить сам значок на диаграмме. Имя каждого класса должно быть уникально в содержащей его категории. Для некоторых языков, в особенности - для C++ и Smalltalk, мы должны требовать, чтобы каждый класс имел имя, уникальное в системе.  

Рис. 5-2. Значок класса.

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

Как мы описывали в главе 3, атрибут обозначает часть составного объекта, или агрегата. Атрибуты используются в анализе и проектировании для выражения отдельных свойств класса [Точнее, атрибут эквивалентен отношению агрегации с физическим включением, метка которого совпадает с именем атрибута, а мощность равна в точности единице]. Мы используем следующий не зависящий от языка синтаксис, в котором атрибут может обозначаться именем или классом, или и тем и другим, и, возможно, иметь значение по умолчанию:

• A - только имя атрибута;

• :C - только класс;

• A:C - имя и класс;

• A:C=E - имя, класс и значение по умолчанию.

Имя атрибута должно быть недвусмысленно в контексте класса. В главе 3 говорилось, что операция - это услуга, предоставляемая классом. Операции обычно изображаются внутри значка класса только своим именем. Чтобы отличать их от атрибутов, к их именам добавляются скобки. Иногда полезно указать полную сигнатуру операции:

• N() - только имя операции;

• RN(Аргументы) - класс возвращаемого значения (R), имя и формальные параметры (если есть).

Имена операций должны пониматься в контексте класса однозначно в соответствии с правилами перегрузки операций выбранного языка реализации.

Общий принцип системы обозначений: синтаксис элементов, таких, как атрибуты и операции, может быть приспособлен к синтаксису выбранного языка программирования. Например, на C++ мы можем объявить некоторые атрибуты как статические, или некоторые операции как виртуальные или чисто виртуальные [В C++ члены, общие для всех объектов класса, объявляются статическими; виртуальной называют полиморфную операцию; чисто виртуальной называют операцию, за реализацию которой отвечает подкласс]; в CLOS мы можем пометить операцию как метод :around. В любом случае мы пользуемся спецификой синтаксиса данного языка, чтобы обозначить детали. Как описывалось в главе 3, абстрактный класс - это класс, который не может иметь экземпляров. Так как абстрактные классы очень важны для проектирования хорошей структуры классов, мы вводим для них специальный значок треугольной формы с буквой А в середине, помещаемый внутрь значка класса (рис. 5-3). Общий принцип: украшения представляют вторичную информацию о некой сущности в системе. Все подобные типы украшений имеют такой же вид вложенного треугольника.

Отношения между классами. Классы редко бывают изолированы; напротив, как объяснялось в главе 3, они вступают в отношения друг с другом. Виды отношений показаны на рис. 5-4: ассоциация, наследование, агрегация (has) и использование. При изображении конкретной связи ей можно сопоставить текстовую пометку, документирующую имя этой связи или подсказывающую ее роль. Имя связи не обязано быть глобальным, но должно быть уникально в своем контексте.

Значок ассоциации соединяет два класса и означает наличие семантической связи между ними. Ассоциации часто отмечаются существительными, например Employment (место работы), описывающими природу связи. Класс может иметь ассоциацию с самим собой (так называемая рефлексивная ассоциация). Одна пара классов может иметь более одной ассоциативной связи. Возле значка ассоциации вы можете указать ее мощность (см. главу 3), используя синтаксис следующих примеров:

• 1 - В точности одна связь

• N - Неограниченное число (0 или больше)

• 0..N - Ноль или больше

• 1..N - Одна или больше

 

Рис. 5-3. Значок абстрактного класса.

 

Рис. 5-4. Значки отношений между классами.

• 0..1 - Ноль или одна

• 3..7 - Указанный интервал

• 1..3, 7 - Указанный интервал или точное число

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

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

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

Значок агрегации обозначает отношение "целое/часть" (связь "has") и получается из значка ассоциации добавлением закрашенного кружка на конце, обозначающем агрегат. Экземпляры класса на другом конце стрелки будут в каком-то смысле частями экземпляров класса-агрегата. Разрешается рефлексивная и циклическая агрегация. Агрегация не требует обязательного физического включения части в целое.

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

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

Рис. 5-5 показывает, как описывается в этих обозначениях задача обслуживания тепличной гидропонной системы. Эта диаграмма представляет только малую часть структуры классов системы. Мы видим здесь класс GardeningPlan (план выращивания), который имеет атрибут, названный crop (посев), одну операцию-модификатор execute (выполнить) и одну операцию-селектор canHarvest (можно собирать?). Имеется ассоциация между этим классом и классом EnvironmentalController (контроллер среды выращивания): экземпляры плана задают климат, который должны поддерживать экземпляры контроллера.  

Рис. 5-5. Диаграмма классов гидропонной системы.

Эта диаграмма также показывает, что класс EnvironmentalController является агрегатом: его экземпляры содержат в точности по одному экземпляру классов Heater (нагреватель) и Cooler (охлаждающее устройство), и любое число экземпляров класса Light (лампочка). Оба класса Heater и Cooler являются подклассами абстрактного запускающего процесс класса Actuator, который предоставляет протоколы startUp и shutDown (начать и прекратить соответственно), и который использует класс Temperature.

Существенное: категории классов

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

Многие объектно-ориентированные языки не поддерживают это понятие. Следовательно, выделение обозначений для категорий классов позволяет выразить важные архитектурные элементы, которые не могли быть непосредственно записаны на языке реализации [Среда программирования Smalltalk поддерживает концепцию категорий классов. Собственно это и подвигло нас на включение категорий в систему обозначений. Однако, в Smalltalk категории классов не имеют семантического содержания: они существуют только для более удобной организации библиотеки классов. В C++ категории классов связаны с концепцией компонент (Страуструп), они еще не являются чертой языка, хотя включение в него семантики пространства имен рассматривается []. (В настоящее время пространства имен включены в стандарт. - Примеч. ред.)].  

Рис. 5-6. Значок категории классов.

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

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

На рис. 5-6 показан значок, обозначающий категорию классов. Как и для класса, для категории требуется имя, которое должно быть уникально в данной модели и отлично от имен классов.

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

Категория классов представляет собой инкапсулированное пространство имен. По аналогии с квалификацией имен в C++, имя категории можно использовать для однозначной квалификации имен содержащихся в ней классов и категорий. Например, если дан класс A из категории B, то его полным именем будет A::B. Таким образом, как будет обсуждаться далее, для вложенных категорий квалификация имен простирается на произвольную глубину.

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

Категория может использовать невложенные категории и классы. С другой стороны, и классы могут использовать категории. Для единообразия мы обозначаем эти экспортно-импортные отношения так же, как отношение использования между классами (см. рис. 5-4). Например, если категория A использует категорию B, это означает, что классы из A могут быть наследниками, или содержать экземпляры, использовать или быть еще как-то ассоциированы с классами из B.  

Рис. 5-7. Диаграмма классов верхнего уровня для гидропонной системы.

  Когда в категории слишком много общих классов, вроде базовых классов-контейнеров или других базовых классов, подобных Object в Smalltalk, возникают практические затруднения. Такие классы будут использоваться чуть ли не всеми другими категориями, загромождая корневой уровень диаграммы. Чтобы выйти из положения, такие категории помечаются ключевым словом global в левом нижнем углу значка, показывающим, что категория по умолчанию может быть использована всеми остальными.

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

Пример. На рис. 5-7 приведен пример диаграммы классов верхнего уровня для тепличного хозяйства. Это типичная многослойная система. Здесь абстракции, которые ближе к реальности (а именно активаторы и датчики климата и удобрений), располагаются на самых нижних уровнях, а абстракции, отражающие понятия пользователя, - ближе к вершине. Категория классов ТипыПосевов - глобальна, то есть ее услуги доступны всем другим категориям. На значке категории классов Планирование показаны два ее важных класса: GardeningPlan (план выращивания) с рис. 5-5 и PlanAnalyst (анализатор планов). При увеличении любой из восьми категорий классов, показанных на рисунке, обнаружатся составляющие их классы.  

Рис. 5-8. Значок параметризованного класса.

Дополнительные обозначения

До сих пор мы занимались существенной частью нашей системы обозначений [Все существенные элементы в совокупности как раз и образуют нотацию Booch Lite]. Однако, чтобы передать некоторые часто встречающиеся стратегические и тактические решения, нам потребуется расширить ее. Общее правило: держаться существенных понятий и обозначений, а дополнительные применять только тогда, когда они действительно необходимы.

Параметризованные классы. В некоторых объектно-ориентированных языках программирования, в частности, C++, Eiffel и Ada можно создавать параметризованные классы. Как было сказано в главе 3, параметризованным классом называется семейство классов с общей структурой и поведением. Чтобы создать конкретный класс этого семейства, нужно подставить вместо формальных параметров фактические (процесс инстанцирования). Конкретный класс может порождать экземпляры.

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

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

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

Метаклассы. В некоторых языках, таких как Smalltalk и CLOS, есть метаклассы. Метакласс (см. главу 3) - это класс класса. В Smalltalk, например, метаклассы - это механизм поддержки переменных и операций класса (подобных статическим членам класса в C++), особенно фабрик класса (производящих операций), создающих экземпляры объектов данного класса. В CLOS метаклассы играют важную роль в возможности уточнения семантики языка [].  

Рис. 5-9. Значок метакласса.

  Метаклассы принципиально отличаются от обычных классов, и, чтобы подчеркнуть это, их значок закрашивается серым цветом, как это сделано на рис. 5-9. Связь между классом и его метаклассом (метасвязь) имеет вид жирной стрелки, направленной от класса к его метаклассу. Метакласс GardeningPlan обеспечивает методы-фабрики new() и default(), которые создают новые экземпляры класса GardeningPlan.

Метакласс не имеет экземпляров, но может любым образом быть ассоциирован с другими классами.

Метасвязь имеет еще одно применение. На некоторых диаграммах классов бывает полезно указать объект, который является статическим членом некоторого класса. Чтобы показать класс этого объекта, мы можем провести метасвязь "объект/ класс". Это согласуется с предыдущим употреблением: связь между некоторой сущностью (объектом или классом) и ее классом.

Утилиты классов. Благодаря своему происхождению, гибридные языки, такие как C++, Object Pascal и CLOS, позволяют разработчику применять как процедурный, так и объектно-ориентированный стиль программирования. Это контрастирует со Smalltalk, который целиком организован вокруг классов. В гибридном языке есть возможность описать функцию-не-член, называемую также свободной подпрограммой. Свободные подпрограммы часто возникают во время анализа и проектирования на границе объектно-ориентированной системы и ее процедурного интерфейса с внешним миром.

Утилиты классов употребляются одним из двух способов. Во-первых, утилиты класса могут содержать одну или несколько свободных подпрограмм, и тогда следует просто перечислить логические группы таких функций-не-членов. Во-вторых, утилиты класса могут обозначать класс, имеющий только переменные (и операции) класса (в C++ это означало бы класс только со статическими элементами [Программирующие на Smalltalk часто используют идиому утилит, чтобы достичь того же эффекта]). Таким классам нет смысла иметь экземпляры, потому что все экземпляры будут находиться в одном и том же состоянии. Такой класс сам выступает в роли своего единственного экземпляра.  

Рис. 5-10. Значок утилиты классов.

Как показано на рис. 5-10, утилита классов обозначается обычным значком класса с украшением в виде тени. В этом примере утилита классов PlanMetrics (параметры плана) предоставляет две важные операции: expectedYield (ожидаемый урожай) и timeToHarvest (время сбора урожая). Утилита обеспечивает эти две операции на основе услуг, предоставляемых классами нижнего уровня - GardeningPlan (план) и CropDatabase (база данных об урожае). Как показывает диаграмма, PlanMetrics зависит от CropDatabase: получает от нее информацию об истории посевов. В свою очередь, класс PlanAnalyst использует услуги PlanHetrics.

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

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

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

Вложенность. Классы могут быть физически вложены в другие классы, а категории классов - в другие категории и т.д. Обычно это нужно для того, чтобы ограничить видимость имен. Вложение соответствует объявлению вложенной сущности в окружающем ее контексте. Мы изображаем вложенность физически вложенным значком; на рис. 5-11 полное имя вложенного класса - Nutritionist::NutrientProfile.  

Рис. 5-11. Значок вложенности.

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

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

Управление экспортом. Все основные языки объектно-ориентированного программирования позволяют четко разделить интерфейс класса и его реализацию. Кроме того, как описано в главе 3, большинство из них позволяет разработчику определить более детально доступ к интерфейсу класса.

Например, в C++ элементы класса бывают открытыми (доступны всем клиентам), защищенными (доступны только подклассам, друзьям и самому классу) и закрытыми (доступны только самому классу и его друзьям). Кроме того, некоторые элементы могут быть частью реализации класса и тем самым быть недоступными даже друзьям этого класса [Например, объект или класс, описанный в .срр-файле, доступен только функциям-членам, реализованным в том же файле]. В Ada элементы класса могут быть открытыми или закрытыми. В Smalltalk все переменные экземпляров по умолчанию закрытые, а все операции - открытые. Доступ предоставляется самим классом и только явно: клиент ничего не может получить насильно.

Мы изображаем способ доступа следующими украшениями связи:

• <нет украшения> - открытый (по умолчанию)

• | - защищенный

• || - закрытый

• ||| - реализация

Мы ставим их как "засечки" на линии связи у источника. Например, на рис. 5-12 показано, что класс GrainCrop множественно наследует от классов Crop (посев) (открытый суперкласс) и FoodItem (пища) (защищенный суперкласс).  

Рис. 5-12. Значок управления доступом.

FoodItem в свою очередь содержит от одного до двадцати трех закрытых экземпляров класса VitaminContent (содержание витаминов) и один открытый экземпляр класса CaloricEquivalent (калорийность). Заметим, что CaloricEquivalent мог бы быть записан как атрибут класса FoodItem, так как атрибуты эквивалентны агрегации, мощность которой равна 1:1. Кроме того, мы видим, что класс GrainCrop (посев зерновых) использует класс GrainYieldPredictor (предсказатель урожая зерновых) как часть своей реализации. Это обычно означает, что некоторый метод класса GrainCrop использует услуги, предоставляемые классом GrainYieldPredictor.

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

Символы ограничения доступа можно применять к вложенности во всех ее формах. На обозначении класса мы можем указать доступ к атрибутам, операциям или вложенным классам, добавив символ ограничения доступа в качестве префикса к имени. Например, на рис. 5-12 показано, что класс Crop имеет один открытый атрибут scientificName (ботаническое название), один защищенный - yield (урожай), и один закрытый - nutrientValue (количество удобрения). Такие же обозначения используются для вложенных классов или категорий классов. По умолчанию все вложенные классы и категории являются открытыми, но мы можем указать ограниченный доступ соответствующей меткой.

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

• static - переменная (или функция) класса;

• virtual - совместно используемый базовый класс в ромбовидной структуре наследования;

• friend - класс, которому даны права доступа к закрытым и защищенным элементам другого класса.

 

Рис. 5-13. Значки отношений.

Логично использовать для них такое же украшение в виде треугольного значка, как и для абстрактного класса, но с символами S, V или F соответственно.

Рассмотрим пример на рис. 5-13, который представляет другой ракурс классов, показанных на предыдущем рисунке. Мы видим, что базовый класс OrganicItem (органический компонент) содержит один экземпляр класса ItemDictionary (словарь компонентов) и что этот экземпляр содержится самим классом, а не его экземплярами (то есть он является общим для всех экземпляров). В общем случае мы указываем обозначение static на одном из концов ассоциации или на конце связи агрегации.

Рассматривая класс GrainCrop, мы видим, что структура наследования приобретает ромбовидную форму (связи наследования, разветвившись, сходятся). По умолчанию, в C++ ромбовидная форма структуры наследования ведет к тому, что в классах-листьях дублируются структуры базового, дважды унаследованного класса. Чтобы класс GrainCrop получил единственную копию дважды унаследованных структур класса OrganicItem, мы должны применить виртуальное наследование, как показано на рисунке. Мы можем добавлять украшение виртуальной связи только к наследованию.

Значок дружественности можно присоединить к любому типу связи, расположив значок ближе к серверу, подразумевая, что сервер считает клиента своим другом. Например, на рис. 5-13 класс PlanAnalyst дружит с классом Crop, а, следовательно, имеет доступ к его закрытыми и защищенным элементам, включая оба атрибута yield и scientificName.

Физическое содержание. Как показано в главе 3, отношение агрегации является специальным случаем ассоциации. Агрегация обозначает иерархию "целое/часть" и предполагает, что по агрегату можно найти его части. Иерархия "целое/часть" не означает обязательного физического содержания: профсоюз имеет членов, но это не означает, что он владеет ими. С другой стороны, отдельная запись о посеве именно физически содержит в себе соответствующую информацию, такую, как имя посева, урожай и график подкормки.  

Рис. 5-14. Физическое содержание.

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

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

• по значению целое физически содержит часть

• по ссылке целое физически содержит указатель или ссылку на часть.

В чисто объектно-ориентированных языках, в особенности в Smalltalk, физическое содержание бывает только по ссылке.

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

Рассмотрим пример, приведенный на рис. 5-14. Мы видим, что экземпляры класса CropHistory (история посева) физически содержат несколько экземпляров классов NutrientSchedule (график внесения удобрений) и ClimateEvent (климатическое событие). Физическое содержание частей агрегации по значению означает, что их создание или уничтожение происходит при создании или уничтожении самого агрегата. Таким образом, агрегация по значению гарантирует, что время жизни агрегата совпадает с временем жизни его частей. В противоположность этому, каждый экземпляр класса CropHistory обладает только ссылкой или указателем на один экземпляр класса Crop. Это означает, что времена жизни этих двух объектов независимы, хотя и здесь один является физической частью другого. Еще один случай - отношение агрегации между классами CropEncyclopedia (энциклопедия посевов) и CropHistory. В данном случае мы вообще не упоминаем физическое содержание. Диаграмма говорит о том, что эти два класса состоят в отношении "целое/часть", и что по экземпляру CropEncyclopedia можно найти соответствующий экземпляр CropHistory, но физическое содержание тут ни при чем. Вместо этого может быть разработан другой механизм, реализующий эту ассоциацию. Например, объект класса CropEncyclopedia запрашивает базу данных, и получает ссылку на подходящий экземпляр CropHistory.

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

Коротко говоря, роль абстракции - это то, чем она является для внешнего мира в данный момент. Роль обозначает потребность или способность, в силу которых один класс ассоциируется с другим. Текстовое украшение, описывающее роль класса, ставится рядом с любой ассоциацией, ближе к выполняющему роль классу, как это видно на рис. 5-15. На этом рисунке классы PlanAnalyst (анализатор планов) и Nutritionist (агрохимик) оба являются поставщиками информации для объекта класса CropEncyclopedia (они оба добавляют информацию в энциклопедию), а объекты класса PlanAnalyst являются также и пользователями (они просматривают материал из энциклопедии). В любом случае, роль клиента определяет индивидуальное поведение и протокол, который он использует. Обратим внимание также на рефлексивную ассоциацию класса PlanAnalyst: мы видим, что несколько экземпляров этого класса могут сотрудничать друг с другом и при этом они используют особый протокол, отличающийся от их поведения в ассоциации, например, с классом CropEncyclopedia.  

Рис. 5-15. Роли и ключи.

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

Рис. 5-16. Значок ограничения.

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

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

На рис. 5-16 мы видим, что для класса EnviromentalController наложено ограничение на мощность, постулирующее, что в системе имеется не более 7 экземпляров этого класса. При отсутствии ограничения на мощность класс может иметь сколько угодно экземпляров. Обозначение для абстрактного класса, введенное ранее, является специальным случаем ограничения (нуль экземпляров), но так как это явление очень часто встречается в иерархиях классов, оно получило собственный тип украшения (треугольник с буквой А).

Класс Heater (нагреватель) имеет ограничение другого типа. В рисунок включено требование гистерезиса в работе нагревателя: он не может быть включен, если с момента его последнего выключения прошло меньше пяти минут. Мы прилагаем это ограничение к классу Heater, считая, что контроль за его соблюдением возложен на экземпляры класса.

На этой диаграмме изображены еще два типа ограничений: ограничение на ассоциации. В ассоциации между классами EnvironmentalController и Light требуется, чтобы отдельные источники света были уникально индексированы относительно друг друга в контексте данной ассоциации. Имеется еще ограничение, наложенное на ассоциации EnvironmentalController с классами Heater и Cooler, состоящее в том, что диспетчер не может включить нагреватель и охладитель одновременно. Это ограничение прикладывается к ассоциации, а не к классам Heater и Cooler, потому что его соблюдение не может быть поручено самим нагревателям и охладителям.

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

Ограничения бывают также полезны для выражения вторичных классов, атрибутов и ассоциаций [В терминологии Румбаха это называется производные сущности: для них он использует специальный значок. Нашего общего подхода к ограничениям достаточно, чтобы выразить семантику производных классов, атрибутов и ассоциации; этот подход облегчает повторное использование существующих значков и однозначное определение сущностей, от которых взяты производные]. Например, рассмотрим классы Adult (взрослые) и Child (дети), являющиеся подклассами абстрактного класса Person (Люди). Мы можем снабдить класс Person атрибутом dateofbirth (дата рождения) и добавить атрибут, называемый age (возраст), например, потому что возраст играет особую роль в нашей модели реального мира. Однако, age - атрибут вторичный: он может быть определен через dateofbirth. Таким образом, в нашей модели мы можем иметь оба атрибута, но должны указать ограничение, определяющее вывод одного из другого. Вопрос о том, какие атрибуты из каких выводятся, относится к тактике, но ограничение пригодится независимо от принятого нами решения.

Аналогично, мы могли бы иметь ассоциацию между классами Adult и Child, которая называлась бы Parent (родитель), а могли бы включить и ассоциацию, именуемую Caretaker (попечитель), если это нужно в модели (например, если моделируются официальные отношения родительства в системе социального обеспечения). Ассоциация Caretaker вторична: ее можно получить как следствие ассоциации Parent; мы можем указать этот инвариант как ограничение, наложенное на ассоциацию Caretaker.

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

Рассмотрим пример на рис. 5-17. На нем показана ассоциация многие-ко-многим между классами Crop и Nutrient. Эта ассоциация означает, что к каждому посеву применяется N (любое число) удобрений, а каждое удобрение применяется к N (любому числу) посевов. Класс NutrientSchedule является как бы свойством этого отношения многие-ко-многим: каждый его экземпляр соответствует паре из посева и удобрения. Чтобы выразить этот семантический факт, мы рисуем на диаграмме пунктирную линию от ассоциации Crop/Nutrient (ассоциация с атрибутом) к ее свойству - классу NutrientSchedule (атрибут ассоциации). Каждая уникальная ассоциация может иметь не больше одного такого атрибута и ее имя должно соответствовать имени класса-атрибута.

Идея атрибутирования ассоциаций имеет обобщение: при анализе и проектировании появляется множество временных предположений и решений; их смысл и назначение часто теряются, потому что нет подходящего места для их хранения, а хранить все в голове - дело немыслимое. Поэтому полезно ввести обозначение, позволяющее добавлять произвольные текстовые примечания к любому элементу диаграммы. На рис. 5-17 имеется два таких примечания. Одно из них, приложенное к классу NutrientSchedule, сообщает нечто об ожидаемой уникальности его экземпляров (Выбирает из общего набора расписаний); другое (Получаем из базы данных удобрений) приложено к конкретной операции класса Nutrient и выражает наши пожелания к ее реализации.  

Рис. 5-17. Ассоциация с атрибутом и примечание.

  Для таких примечаний мы используем значки, похожие на бумажки, и соединяем их с элементом, к которому они относятся, пунктирной линией. Примечания могут содержать любую информацию: обычный текст, фрагменты программ или ссылки на другую документацию (все это может пригодиться при разработке инструментов проектирования). Примечания могут быть не связаны ни с каким элементом, это значит, что они относятся к самой диаграмме [Значок, который мы используем, похож на обозначение примечаний во многих windows-системах, особенно следующих традициям Macintosh. Непосредственными вдохновителями нашего обозначения были предложения Гамма, Хелпа, Джонсона и Влиссидеса []].

Спецификации

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

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

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

Общие элементы. Все спецификации имеют как минимум следующие компоненты:

Имя: идентификатор Определение: текст

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

Определение - это текст, идентифицирующий представленное элементом понятие или функцию и пригодный для включения в словарь проекта (который обсуждается в следующей главе).

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

Спецификации класса. Каждый класс в модели имеет ровно одну спецификацию, в которой содержатся как минимум следующие пункты:

Обязанности: текст Атрибуты: список атрибутов Операции: список операций Ограничения: список ограничений

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

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

Перечисленные основные понятия могут быть представлены в терминах выбранного языка реализации. В частности, все эти сведения, как правило, однозначно фиксируются объявлением класса на C++ или спецификацией пакета в Ada.

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

Автомат: ссылка на автомат

Использование дополнительных элементов системы обозначений требует ввести в спецификацию класса следующие пункты:

Управление экспортом: открытый | реализация Мощность: выражение

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

Параметры: список формальных или фактических параметров

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

Устойчивость: мгновенный | постоянный Параллельность: последовательный | охраняемый | синхронный | активный Место в памяти: выражение

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

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

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

Спецификации операций. Для всех операций-членов классов и свободных подпрограмм наши спецификации включают следующие основные пункты:

Класс возвращаемого значения: ссылка на класс Аргументы: список формальных аргументов

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

Квалификация: текст

В C++, например, этот пункт может содержать утверждение о том, является ли операция статической, виртуальной, чисто виртуальной или константой.

Использование дополнительных элементов обозначений требует введения дополнительной графы:

Доступ: открытый | защищенный | закрытый | реализация

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

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

Протокол: текст

Эта графа происходит из практики языка Smalltalk: протокол операции не имеет семантического значения, а служит просто для именования логической совокупности операций, вроде таких, как initialize-release (инициализация-освобождение) или model access (доступ к модели).

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

Предусловия: текст | ссылка на текст программы | ссылка на диаграмму объектов Семантика: текст | ссылка на текст программы | ссылка на диаграмму объектов Постусловия: текст | ссылка на текст программы | ссылка на диаграмму объектов Исключения: список исключительных ситуаций

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

Последняя серия необязательных граф служит для описания некоторых функциональных аспектов операции:

Параллельность: последовательный | охраняемый | синхронный Память: выражение Время: выражение

Первые две аналогичны одноименным графам в спецификации класса. Третья - относительные или абсолютные оценки времени выполнения операции.

 

5.3. Диаграммы состояний и переходов

Существенное: состояния и переходы

Диаграмма состоянии и переходов показывает: пространство состояний данного класса; события, которые влекут переход из одного состояния в другое; действия, которые происходят при изменении состояния. Мы приспособили обозначения, использованные Харелом []: его работа предоставляет простой, но очень выразительный подход, который гораздо эффективнее традиционных автоматов с конечным числом состояний [Мы дополнили его работу применительно к объектно-ориентированному программированию, следуя предложениям Румбаха [] и Беара и др. []]. Отдельная диаграмма состояний и переходов представляет определенный ракурс динамической модели отдельного класса или целой системы. Мы строим диаграммы состояний и переходов только для классов, поведение которых (управляемое событиями) для нас существенно. Мы можем также представить диаграмму состояний и переходов для управляемого событиями поведения системы в целом. Эти диаграммы используются в ходе анализа, чтобы показать динамику поведения системы, а в ходе проектирования - для выражения поведения отдельных классов или их взаимодействия.  

Рис. 5-18. Значок состояния.

  Два основных элемента диаграммы состояний и переходов - это, естественно, состояния и переходы между ними.

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

В любой момент времени состояние объекта определяет набор свойств (обычно статический) объекта и текущие (обычно динамические) значения этих свойств. Под "свойствами" подразумевается совокупность всех связей и атрибутов объекта. Мы можем обобщить понятие состояния так, чтобы оно было применимо и к объекту, и к классу, так как все объекты одного класса "живут" в одном пространстве состояний. Это пространство может представлять собой неопределенное, хотя конечное множество возможных (но не всегда ожидаемых или желаемых) состояний. На рис. 5-18 показано обозначение, которое мы используем для отдельного состояния.

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

На значках некоторых состояний полезно указать ассоциированные с ними действия. Как показано на рис. 5-18, действия обозначаются так же, как атрибуты и операции в значке класса. Мы можем увеличить значок, чтобы увидеть весь список действий, или, если нет необходимости указывать действия, можно удалить разделяющую линию и оставить только имя [Для совместимости с обозначениями Харела разделяющую линию можно вообще убрать]. Ассоциацию действий с состояниями мы обсудим позднее.  

Рис. 5-19. Значок перехода из состояния в состояние.

Переходы. Событием мы называем любое происшествие, которое может быть причиной изменения состояния системы. Изменение состояний называется переходом. На диаграмме переходов и состояний он изображается значком, показанным на рис. 5-19. Каждый переход соединяет два состояния. Состояние может иметь переход само в себя; обычно есть несколько различных переходов в одно и тоже состояние, но все переходы должны быть уникальны в том смысле, что ни при каких обстоятельствах не может произойти одновременно два перехода из одного состояния.

Например, в поведении гидропонной теплицы играют роль следующие события:

• Посажена новая партия семян.

• Урожай созрел и готов к сбору.

• Из-за плохой погоды упала температура в теплице.

• Отказало охлаждающее устройство.

• Наступил заданный момент времени.

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

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

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

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

Событие может быть представлено символическим именем (или именованным объектом), классом или именем некоторой операции. Например, событие CoolerFailure (неисправность охлаждающего устройства) может обозначать либо литерал, либо имя объекта. Мы можем придерживаться той стратегии, что все события являются символическими именами и каждый класс с поведением, управляемым событиями, имеет операцию, которая распознает эти имена и выполняет соответствующие действия. Такая стратегия часто используется в архитектурах типа модель-представление-котроллер (model-view-controller), которая пришла из языка Smalltalk. Для большей общности можно считать события объектами и определить иерархию классов, которые представляют собой абстракции этих событий. Например, можно определить общий класс событий DeviceFailure (неисправность устройства) и его специализированные подклассы, такие как CoolerFailure (неисправность охлаждающего устройства) и HeaterFailure (неисправность нагревателя). Теперь извещение о событии можно связать с экземпляром класса-листа (например, CoolerFailure) или более общего суперкласса (DeviceFailure). И если выполнение некоторого действия назначено только при возникновении события класса CoolerFailure, то это означает, что все другие случаи отказа устройств должны намеренно игнорироваться. С другой стороны, если выполнение действия связано с событием DeviceFailure, то действие должно выполняться независимо от того, на каком устройстве произошел сбой. Продолжая в том же духе, мы можем сделать так, чтобы переходы из состояния в состояние были полиморфны относительно классов событий. Наконец, можно определить событие просто как операцию, такую как GardeningPlan::execute(). Это похоже на подход, который трактует события как имена, но в отличие от него здесь не требуется явного диспетчера событий.

Для нашего метода несущественно, какая из этих стратегий выбрана для разработки, если она последовательно проводится во всей системе. Обычно в замечаниях указывается, какая стратегия использована для данного конкретного автомата.

Действие можно записывать, используя синтаксис, показанный в следующих примерах:

• heater.startUp() - действие

• DeviceFailure - произошло событие

• start Heating - начать некоторую деятельность

• stop Heating - прекратить деятельность.

Имена операций или событий должны быть уникальны в области видимости диаграммы; там, где необходимо, они могут быть квалифицированы соответствующими именами классов или объектов. В случае начала или прекращения некоторой деятельности, она может быть представлена операцией (такой, как Actuator::shutDown()) или символическим именем (для событий). Когда деятельность соответствует некоторой функции системы, такой, как harvest crop (сбор урожая), мы обычно пользуемся символическими именами.

На каждой диаграмме состояний и переходов должно присутствовать ровно одно стартовое состояние; оно обозначается немаркированным переходом в него из специального значка, изображаемого в виде закрашенного кружка. Иногда бывает нужно указать также конечное состояние (обычно автомат, ассоциированный с классом или системой в целом, никогда не достигает конечного состояния; этот автомат просто перестает существовать после того, как содержащий его объект уничтожается). Мы обозначаем конечное состояние, рисуя немаркированный переход от него к специальному значку, изображаемому как кружок с закрашенной серединой.  

Рис. 5-20. Диаграмма состояний и переходов для контроллера тепличной среды ( EnvironmentalController ).

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

На рис. 5-20 показан пример использования существенных обозначений. Пример опять описывает гидропонную систему. Мы видим диаграмму состояний и переходов для класса EnvironmentalController, впервые введенного на рис. 5-5.

На этой диаграмме все события представляются символическими именами. Мы видим, что все объекты этого класса начинают свою жизнь в начальном состоянии Idle (ожидание); затем они изменяют свое состояние по событию Define climate, для которого не предполагается явных действий (считается, что это событие, то есть ввод климатического задания, происходит только в дневное время). Дальше динамическое поведение этого класса состоит в переключении между со-стояниями Daytime и Nighttime (день и ночь); оно определяется событиями Sunrise и Sunset (восход и закат) соответственно; с этими событиями связаны действия по изменению освещения. В обоих состояниях событие понижения или повышения температуры в теплице вызывает обратную реакцию (операция adjustTemperature(), которая является локальной в этом классе). Мы возвращаемся в состояние Idle, когда поступит событие Terminate climate, то есть будет отменено климатическое задание.

Дополнительные понятия

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

Рис. 5-21. Действия, условные переходы и вложенные состояния.

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

• entry start Alarm - запуск процедуры при входе в состояние

• exit shutDown() - вызов операции при выходе из состояния.

Как и для переходов, можно назначить любое действие после ключевых слов entry и exit(вход и выход).

Деятельность можно ассоциировать с состоянием, используя синтаксис следующего примера:

• do Cooling - в данном состоянии заниматься этой деятельностью.

Этот синтаксис служит сокращенной записью явных указаний: "Начать деятельность при входе в состояние и окончить при выходе из него".

На рис. 5-21 мы видим пример использования этих обозначений. При входе в состояние Heating (нагревание) вызывается операция Heater::startUp(), а при выходе - операция Heater::shutDown(), то есть происходит запуск и остановка нагревания. При входе и выходе из состояния Failure (сбой), соответственно вызывается и прекращается сигнал тревоги (Alarm).

Рассмотрим также переход из состояния Idle в состояние Heating. Он совершается, если температура понизилась, но только в случае, если прошло больше пяти минут после того, как последний раз был выключен нагреватель. Это пример условного (или защищенного) перехода; условие представляется логическим выражением в скобках.

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

Имеет значение порядок выполнения условного перехода. Пусть имеется состояние S, из которого при событии E совершается переход T с условием C и действием A. Переход T осуществляется в такой последовательности:

• Происходит событие E.

• Проверяется условие C.

• Если C удовлетворено, то выполняется переход T и действие A.

Это означает, что если условие C не выполнено, то переход не может быть осуществлен до тех пор, пока событие E не произойдет еще раз и условие C не будет проверено еще раз. Побочные эффекты при вычислении условия или выполнении действия, назначенного на выход, не могут отменить переход. Например, предположим, что произошло событие E, условие C выполнилось, но действие A, выполняемое при выходе из состояния S, изменило ситуацию так, что условие C перестало выполняться: переход T все равно состоялся.

Мы можем использовать еще и следующий синтаксис:

• in Cooling - выражение для текущего состояния.

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

Можно использовать в условии и выражение, налагающее ограничения по времени:

• timeout (Heating, 30) - выражение ограничения по времени.

Это условие выполняется, если система более 30 секунд находилась в состоянии Heating и остается в нем в момент проверки. Этот тип условия употребляется в системах реального времени для "переходов без события", так как защищает систему от зависания на долгое время в одном состоянии. Это выражение можно использовать для указания нижней границы времени нахождения в данном состоянии. Если приложить временное ограничение к каждому переходу с событием, выводящим из данного состояния, это будет равнозначно требованию, что система находится в каждом состоянии как минимум время, указанное в ограничении [Харел предложил "обобщенную завитушку" для обозначения двухсторонних границ по времени, но мы не будем обсуждать здесь его обобщения, так как условия исчерпания времени достаточно выразительны].

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

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

На рис. 5-21 показаны внутренние детали состояния Cooling, то есть вложенные в него состояния; для простоты мы опустили все его действия, включая действия при входе и выходе.

Объемлющие состояния, такие, как Cooling, называются суперсостояниями, а вложенные, такие, как Running, - подсостояниями. Вложенность может достигать любой глубины, то есть подсостояние может быть суперсостоянием для вложенных состояний более низкого уровня. Данное суперсостояние Cooling содержит три подсостояния. Семантика вложенности подразумевает отношение xor (исключающее или) для вложенных состояний: если система находится в состоянии Cooling (охлаждение), то она находится ровно в одном из подсостояний Startup (начальное), Ready (готовность) или Running (выполнение).

Чтобы проще ориентироваться в диаграмме переходов с вложенными состоя-ниями мы можем увеличить или уменьшить ее масштаб относительно выбранного состояния. При уменьшении вложенные состояния исчезают, а при увеличении проявляются. Переходы в скрытые на диаграмме подсостояния и выходы из них показываются стрелкой с черточкой, как переход в состояние Ready на рисунке [Если быть точными, то переходы Too hot и Ok относительно состояния Cooling также должны быть показаны на рис. 5-21 с черточкой, так как это переходы между подсостояниями].

Переходам между состояниями разрешено начинаться и кончаться на любом уровне. Рассмотрим различные формы переходов:

• Переход между одноуровневыми состояниями (такой, как из Failure в Idle или из Ready в Running) - простейшая форма перехода; его семантика описана в предыдущем разделе.

• Можно совершить переход непосредственно в подсостояние (как из Idle в Startup), или непосредственно из подсостояния (как из Running в Idle), или одновременно и то, и другое.

• Указание перехода из суперсостояния (как из Cooling в Failure через событие Failure) означает, что он осуществляется из каждого подсостояния этого суперсостояния. Такой переход пронизывает все уровни до переопределения. Это упрощает диаграмму за счет удаления банальных переходов, общих для всех подсостояний.

• Указание перехода в состояние с вложенными подсостояниями (например, предыдущий переход в состояние Failure) подразумевает переход к его начальному подсостоянию (по умолчанию).

История. Иногда, возвращаясь к суперсостоянию, мы хотели бы попасть в то его подсостояние, где мы были последний раз. Эту семантику мы будем изображать значком истории (буква H (History) внутри кружка, размещенного где-нибудь внутри значка суперсостояния). Например, на рис. 5-22 мы видим развернутое изображение состояния Failure. В самый первый раз, когда наша система переходит в него, она принимает начальное состояние по умолчанию Create log (создать журнал); что обозначено непомеченным переходом из закрашенного кружка внутри объемлющего состояния; когда журнал (log) создан, система переходит в состояние Log ready. После того, как сообщение о сбое занесено в журнал, мы возвращаемся обратно. Когда мы попадем в состояние Failure в следующий раз, нам не нужно будет опять создавать журнал, и мы перейдем прямо к Log ready, так как когда мы в последний раз выходили из состояния Failure, система находилась именно в этом подсостоянии.  

Рис. 5-22. История событий.

  Действие "истории" распространяется только на тот уровень, на котором она указана. Если мы хотим распространить ее действие на все нижние подуровни, то мы обозначим это, пририсовав к ее значку звездочку. Можно получить промежуточный результат, пририсовав значок истории только к отдельным подсостояниям.

Спецификации

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

 

5.4. Диаграммы объектов

Существенное: объекты и их отношения

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

Рис. 5-23. Значок объекта.

Объекты. На рис. 5-23 показан значок, который изображает объект на диаграмме объектов. Как и в диаграммах классов, можно провести горизонтальную линию, разделяющую текст внутри значка объекта на две части: имя объекта и его атрибуты.

Имя объекта следует синтаксису для атрибутов, и может быть либо записано в одной из трех следующих форм:

• A - только имя объекта

• :C - только класс объектов

• A:C - имя объекта и класса

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

Смысл неквалифицированных имен зависит от контекста диаграммы объектов. Более точно: диаграммы объектов, определенные на самом верхнем уровне системы, имеют глобальную область видимости; другие диаграммы объектов могут быть определены для категорий классов, отдельных классов или отдельных методов, а, значит, иметь соответствующие области видимости. Квалифицированное имя может быть использовано при необходимости явной ссылки на глобальные объекты, переменные классов (статические элементы в C++), параметры методов, атрибуты или локально определенные объекты в той же области видимости.

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

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

Рис. 5-24. Значок связи между объектами.

На значках объектов бывает полезно указать несколько их атрибутов. "Несколько" - так как значок объекта представляет только один какой-то ракурс его структуры. Синтаксис атрибутов совпадает с описанным ранее синтаксисом атрибутов класса и позволяет указать выражение, используемое по умолчанию. Имена атрибутов объектов должны соответствовать атрибутам, определенным в классе объекта, или в любом из его суперклассов. Синтаксис имен атрибутов может быть приспособлен к синтаксису языка реализации.

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

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

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

Пусть имеются объекты A и B и связь L между ними. Тогда A может вызвать любую операцию, имеющуюся в классе B и доступную A. То же верно для операций над A, вызываемых B. Объект, вызывающий операцию, называется объект-клиент, а объект, который предоставляет операцию, - объект-сервер. Обычно отправитель сообщения знает получателя, но обратное необязательно.

В установившемся состоянии структуры классов и объектов системы должны быть согласованы. Если мы показываем на диаграмме операцию M на классе B, вызванную по связи L, то м должна быть объявлена в спецификации B, или в спецификациях его суперклассов.

Как показано на рис. 5-24, рядом с соответствующей связью на диаграмме можно записать набор сообщений. Каждое сообщение состоит из следующих трех элементов:

• D - символ синхронизации, обозначающий направление вызова

• M - вызов операции или извещение о событии

• S - необязательный порядковый номер.

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

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

• N() - только имя операции

• RN(arguments) - возвращаемое значение, имя и фактические параметры операции.

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

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

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

Пример. На рис. 5-25 показана диаграмма объектов для нашего тепличного хозяйства в контексте категории классов Planning (планирование; описана на рис. 5-7). Цель этой диаграммы - проиллюстрировать сценарий выполнения обычной функции системы, а именно, прогнозирование затрат на сбор урожая некоторого посева.

Выполнение этой функции требует сотрудничества нескольких различных объектов. Сценарий начинается с вызова объектом PlanAnalyst (анализатор планов) операции timeToHarvest() (время собирать урожай) над утилитой класса PlanMetrics (параметры планов). При этом объект с передается как фактический параметр операции. Затем утилита PlanMetrics вызывает операцию status() (состояние) на некотором неименованном объекте класса GardeningPlan (план выращивания). В пояснении говорится: "Надо проверить, что этот план действительно выполняется". В свою очередь, объект GardeningPlan вызывает операцию maturationTime() (время созревания) на выбранном объекте класса GrainCrop (посев зерновых), запрашивающую ожидаемое время созревания посева. Когда эта операция-селектор будет выполнена, управление возвращается объекту класса PlanAnalyst, который затем непосредственно вызывает операцию C.yield(), унаследованную от суперкласса (операция Crop::yield()). Управление снова возвращается объекту класса PlanAnalyst, который продолжает сценарий, выполняя над собой операцию netCost().  

Рис. 5-25. Диаграмма объектов гидропонной системы.

На диаграмме показана связь между объектами классов PlanAnalyst и GardeningPlan. Хотя сообщения между ними не посылаются, связь отражает существование семантической зависимости между этими объектами.

Дополнительные понятия

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

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

Рис. 5-26 дает пример использования этого дополнительного обозначения. Здесь мы видим, что некоторый объект класса PlanAnalyst заносит информацию об определенном посеве (Crop) в анонимный объект CropEncyclopedia (энциклопедия посевов) и делает это, пока находится в роли Автор.

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

Поток данных. Как было описано в главе 3, данные могут передаваться по или против направления посылки сообщения. Иногда явное указание направления передачи данных помогает объяснить семантику конкретного сценария. Мы используем для этого значок, заимствованный из обозначений структурного проектирования. На рис. 5-26 дан пример его использования: здесь показано, что после завершения сообщения insert (вставить) возвращается значение succeeded (успех). Передаваться и возвращаться может либо объект, либо значение.  

Рис. 5-26. Роли.

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

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

Например, канал связи от объекта PlanAnalyst к утилите классов PlanMetrics помечен буквой G; это значит, что утилита класса глобальна. Объект C по-разному виден объекту PlanAnalyst и объекту GardeningPlan: с точки зрения первого объект с класса GrainCrop виден как параметр некоторой операции (обозначается буквой P); с точки зрения второго C виден как атрибут или поле, то есть как часть агрегированного объекта (обозначен буквой F (field)).

Вообще, для указания видимости могут быть использованы следующие обозначения:

• G - сервер глобален для клиента

• P - сервер является параметром некоторой операции клиента

• F - сервер является частью клиента

• L - сервер локально определен в области видимости клиента.

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

Рис. 5-27. Значки видимости.

Активные объекты и синхронизация. Как отмечалось в главе 3, некоторые объекты могут быть активными, то есть им отводится отдельный поток управления. Другие объекты могут существовать только в однопоточной среде. Третьи, будучи по природе однозадачными, гарантированно переносятся в многопоточную среду.

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

Ранее, говоря о дополнительных элементах спецификаций класса, мы заметили, что есть четыре типа семантики: последовательная, защищенная, синхронизированная и активная. По существу, все объекты класса наследуют соответствующую семантику класса; все объекты считаются последовательными, если явно не указано обратное. Мы можем явно показать многозадачную семантику объекта на диаграмме объектов, указав в левом нижнем углу значка объекта одно из слов sequential, guarded, synchronous или active. Например, на рис. 5-28 мы видим, что объекты H, C и некий экземпляр класса EnvironmentalController - активные. Немаркированные объекты, такие как L, считаются последовательными.

Символ синхронизации сообщений, введенный ранее (простая стрелка), представляет обычную последовательную передачу сообщения. Однако, при наличии нескольких потоков управления мы должны указывать и другие формы синхронизации. Пример на рис. 5-28, возможно, несколько надуманный, иллюстрирует различные типы синхронизации сообщений, которые могут появиться на диаграмме объектов. Сообщение turnOn() (включить) - пример простой посылки сообщения; оно изображается простой стрелкой. Семантика простой посылки сообщения гарантирована только в однопоточной среде. Остальные сообщения из этого примера используют некоторые формы синхронизации процессов. Все такие дополнительные виды синхронизации применяются только к серверам, которые не являются последовательными.  

Рис. 5-28. Активные объекты и синхронизация.

Например, сообщение startUp() - синхронизированное, то есть клиент будет ждать до тех пор, пока сервер не примет сообщение. Посылка синхронизированного сообщения эквивалентна механизму свиданий задач в языке Ada (rendezvous). В случае сообщения isReady() клиент отложит сообщение, если сервер не сможет его немедленно обработать. Сообщение restart() будет отложено клиентом, если сервер не может его обработать за указанный промежуток времени.

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

Расписание. В программах, имеющих ограничения по времени, важно отслеживать чистое время с момента начала каждого сценария. Для обозначения относи тельного времени (в секундах) мы ставим знак плюс. Например, на рис. 5-29 сообщение startUp() вызывается в первый раз спустя 5 секунд после начала сценария далее, через 6.5 секунд после начала сценария следует сообщение ready() и затем, спустя 7 секунд после начала сценария, - сообщение turnOn().

Спецификации

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

Context: глобальный | категория | класс | операция

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

Рис. 5-29. Расписание.

 

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

Существенное: объекты и их взаимодействия

Диаграмма взаимодействии используется, чтобы проследить выполнение сценария в том же контексте, что и диаграмма объектов [Эти диаграммы обобщают диаграммы трассировки событий Румбаха и диаграммы взаимодействий Джекобсона []]. В известной степени диаграмма взаимодействия есть просто другой способ представления диаграммы объектов. Например, на рис. 5-30 мы видим диаграмму взаимодействия, которая дублирует большую часть семантики диаграммы объектов, показанной на рис. 5-25. Преимущество диаграммы взаимодействий в том, что на ней легче читается порядок посылки сообщений, а преимущество диаграммы объектов в том, что она лучше подходит для многих объектов со сложными вызовами и допускает включение другой информации: связи, значения атрибутов, роли, блок-схемы и видимость. Так как оба типа диаграмм имеют неоспоримые достоинства, мы пользуемся в нашем методе обоими [Диаграммы объектов и диаграммы взаимодействий настолько близки по семантике, что инструментальные средства могут генерировать одну диаграмму из другой с минимальной потерей информации].

Диаграммы взаимодействия не вводят новых понятий или обозначений. Скорее, они берут существенные элементы диаграммы объектов и перестраивают их. Как показывает рис. 5-30, диаграмма взаимодействий внешне напоминает таблицу. Имена объектов диаграммы взаимодействий (те же, что и на диаграмме объектов) записываются горизонтально в верхней ее строке. Под каждым из них рисуется вертикальная пунктирная линия. Отправления сообщений (которые могут обозначать события или вызовы операций) показываются горизонтальными стрелками, с тем же синтаксисом и обозначениями синхронизации, что и на диаграмме объектов. Линия, обозначающая посылку сообщения, проводится от вертикали клиента к вертикали сервера. Первое сообщение показывается на самом высоком уровне, второе ниже и т.д., таким образом отпадает надобность в их порядковых номерах.

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

Рис. 5-31. Пояснения и переход управления.

Дополнительные понятия

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

Пояснения. Для сложных сценариев, использующих условия или итерации, диаграмма объектов может быть дополнена пояснениями. Как показано на примере (рис. 5-31), пояснения могут быть подписаны к любому сообщению слева от диаграммы на соответствующем уровне простым текстом, с элементами структуризации, или с использованием синтаксиса языка реализации.

Передача управления. Ни простейшие диаграммы объектов, ни диаграммы взаимодействий не показывают передач управления. Например, если мы показали, что объект A посылает сообщения X и Y другим объектам, то остается неясным, являются ли сообщения X и Y независимыми сообщениями из A или они были вызваны как части некоторого объемлющего сообщения Z. Как показано на рис. 5-31, мы можем нарисовать на вертикальной линии каждого объекта полоски, показывающие периоды, когда управление находится в этом объекте. На этом примере мы видим, что всем руководит анонимный экземпляр класса GardeningPlan, который, выполняя климатический план, вызывает другие методы, которые, в свою очередь,вызывают следующие методы, и, в конце концов, управление возвращается обратно к нему же.

 

5.6. Диаграммы модулей

Существенное: модули и их зависимость

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

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

Основными элементами диаграммы модулей являются модули и их зависимости.

Модули. На рис. 5-32 сведены обозначения различных типов модулей. Первые три значка - это файлы, различающиеся своими функциями. Значок главной программы обозначает файл, содержащий корневую программу. В C++, например, это соответствовало бы некоторому файлу с расширением .cpp содержащему привилегированную функцию-неэлемент, называемую main. Обычно существует ровно один такой модуль на программу. Значок описания и значок тела обозначают файлы, которые содержат, соответственно, описания и реализации. В C++, например, модуль описаний соответствует заголовочному файлу с расширением .h, а модуль тела - файлу с текстом программы с расширением .cpp.  

Рис. 5-32. Значки модулей и подсистем.

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

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

Рис. 5-33. Диаграмма модулей гидропонной системы.

Зависимости. Единственная связь, которая может существовать между двумя модулями, - компиляционная зависимость - представляется стрелкой, выходящей из зависимого модуля. В C++, например, мы указываем такую зависимость директивой #include. Аналогично, в Ada компиляционная зависимость указывается фразой with. В множестве компиляционных зависимостей не могут встречаться циклы. Чтобы определить частичную упорядоченность компиляций, достаточно выполнить частичное упорядочение структуры модулей системы.

Пример. На рис. 5-33 показан пример обозначений модулей, в архитектуре системы тепличного гидропонного хозяйства. Мы видим здесь шесть модулей. Два из них, climatedefs и cropdefs, являются только описаниями и служат для предоставления общих типов и констант. Остальные четыре включают в себя и тела, и описания: это типичный стиль построения диаграмм модулей, так как описания и тела очень тесно связаны. На рисунке эти две части совмещены, и зависимость тела от описания получилась скрытой, хотя реально она существует. Также оказалось скрытым имя тела, но, по нашему соглашению, имена тела и описания различаются лишь суффиксами (.cpp и .h).

Зависимости на этой диаграмме предполагают частичное упорядочение компиляции. Например, тело модуля climate зависит от описания heater, которое, в свою очередь, зависит от описания climatedefs.Существенное: подсистемы. Как объяснялось в главе 2, большие системы могут быть разложены на несколько сотен, если не тысяч, модулей. Пытаться разобраться в физической архитектуре такой системы без ее дополнительного структурирования почти безнадежно. На практике разработчики стремятся следовать неформальному соглашению собирать связанные между собой модули в структуры типа каталогов. По этим соображениям мы введем понятие подсистемы на диаграмме модулей. Подсистемы представляют собой совокупности логически связанных модулей, примерно как категория классов представляет совокупность классов.

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

На рис. 5-32 показано обозначение подсистемы. Как и модуль, подсистема должна быть именованной. Имена подсистем подчиняются тем же правилам, что и имена модулей, хотя полное имя подсистемы обычно не содержит суффиксов.

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

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

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

Рис. 5-34. Диаграмма модулей верхнего уровня для гидропонной системы.

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

Дополнительные понятия

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

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

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

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

 

5.7. Диаграммы процессов.

Существенное: процессоры, устройства и соединения

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

Основные элементы диаграммы процессов - процессоры, устройства и их соединения.

Процессоры. На рис. 5-35 показано обозначение процессора. Процессор - часть аппаратуры, способная выполнять программы. Каждый процессор должен иметь имя; никаких особых ограничений на имена процессоров нет, так как они обозначают "железо", а не программы.

Мы можем дополнить значок процессора списком процессов. Каждый процесс в таком списке обозначает или главную программу (функцию main из диаграммы модулей) или имя активного объекта (из диаграммы объектов).

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

Рис. 5-35. Значки процессора и устройства нашей логической модели.

Соединения. Процессоры и устройства должны сообщаться друг с другом. На диаграмме процессов мы изображаем соединения между ними ненаправлеными линиями. Соединение обычно представляет непосредственную связь между аппаратурой, например, RS232, Ethernet, или даже доступ к разделяемой памяти, но эта связь может быть и не прямой, например, "Земля-спутник". Соединения обычно считаются двунаправленными, но при необходимости к их обозначению можно добавить стрелку, чтобы указать направление. Любое соединение может иметь необязательную именующую его метку.

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

Дополнительные понятия

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

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

Рис. 5-36. Диаграмма процессов гидропонной системыми с закругленными углами.

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

∙ Вытесняющее   Процесс с более высоким приоритетом, может отнимать процессор у исполняемого процесса с более низким приоритетом; обычно процессы с одинаковым приоритетом получают равные промежутки времени для выполнения, так что вычислительные ресурсы распределены справедливо. 

 ∙ Невытесняющее   Текущий процесс продолжает выполняться на процессоре до тех пор, пока сам не уступит контроль над ним. 

 ∙ Циклическое   Процессам по очереди выделяется равное количество процессорного времени, обычно называемое квантом времени, по истечении которого управление передается другому процессу. Процесс может получать время в квантах и подквантах. 

 ∙ Алгоритмическое   Переключением процессов управляет некоторый алгоритм. 

 ∙ Ручное   Пользователь извне системы управляет переключением процессов. 

  Для более подробного описания диспетчеризации процессов на конкретном процессоре бывает полезно привести диаграмму объектов или взаимодействий, особенно если используется алгоритмическое переключение.

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

 

5.8. Применение системы обозначений

Результат объектно-ориентированного проектирования

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

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

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

Увеличение и уменьшение масштаба

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

Мы также считаем, что эта система обозначений годится для реализации на разных языках объектно-ориентированного программирования.

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

 

Выводы

• Проектирование - это не рисование диаграмм; диаграммы просто отражают результаты проектирования.

• При проектировании сложной системы важно рассмотреть ее в различных ракурсах - как с точки зрения логической/физической структуры, так и статической/динамической семантики.

• Система обозначений объектно-ориентированного проектирования включает четыре основных диаграммы (классов, объектов, модулей, процессов) и две дополнительные (состояний и переходов, взаимодействий).

• Диаграмма классов показывает, какие существуют классы и связи между ними в логической структуре системы. Конкретная диаграмма классов - один из ракурсов полной структуры классов системы.

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

• Диаграмма модулей показывает распределение классов и объектов по модулям в физической структуре системы. Диаграмма модулей - один из ракурсов модульной архитектуры системы.

• Диаграмма процессов показывает распределение процессов по процессорам в физической структуре системы. Каждая диаграмма процессов - один из ракурсов архитектуры процессов системы.

• Диаграмма состояний и переходов показывает: (1) пространство состояний экземпляров данного класса; (2) события, которые влекут переход из одного состояния в другое; (3) действия, которые происходят при изменении состояния.

• Диаграмма взаимодействий позволяет следить за выполнением сценария в контексте диаграммы объектов.

 

Дополнительная литература

Со времени выхода первого издания этой книги я без устали старался ввести в метод Буча лучшие элементы обозначении, принадлежащие другим методологам, особенно Румбаху (Rumbaugh) и Джекобсону (Jacobson), удалял и упрощал элементы, неудачные или имеющие сомнительную пользу. В то же время, концептуальное единство системы обозначений береглось как зеница ока. Данная глава - кульминация этих усилий.

Об обозначениях в разработке программного обеспечения написано чрезвычайно много; книга Мартина и МакКлюра (Martin and McClure) [H 1988] служит хорошим общим справочником по многим традиционным подходам. Грэхам (Graham) [F 1991] дал обзор ряда нотаций, специфичных для объектно-ориентированных методов.

Ранняя форма описанной в этой главе системы обозначений была впервые документирована в работе Буча (Booch) [F 1981]. Эта система в дальнейшем развилась и включила выразительные средства: семантических сетей (Стилингс и др. (Stillings et al.) [A 1987] и Барри Фейгенбаум (Barrand Feigenbaum) [J 1981]),диаграммы "сущность-отношение" (Чэн (Chen) [Е 1976]), модели сущности (Росс (Ross) [F 1987]), сети Петри (Petri) (Пе-терсон (Peterson) [J 1977], Сахару (Sahraoui) [F 1987] и Бруон и Балзамо (Bruon and Balsamo) [F 1986]), ассоциации (Румбах (Rumbaugh) [F 1991]) и карты состояний (Харел (Harel) [F 1987]). Особенно интересна работа Румбаха, поскольку, как он заметил, в наших подходах больше сходства, чем различий.

Значки для объектов и классов были инспирированы iAPX 432 [D 1981]. За основу изображения для объектных диаграмм были взяты обозначения Сейдвица (Seidewitz) [F 1985]. Для семантики параллельности были приспособлены обозначения Бура (Buhr) [F1988,1989].

Чэн (Chang) [G 1990] дал хороший обзор более общих аспектов визуальных языков.

 

Глава 6 Процесс

 

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

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

 

6.1. Основные принципы

Характерные черты удачных проектов

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

• Ясное представление об архитектуре создаваемой системы;

• Хорошо организованный итеративно развивающийся процесс работы над проектом.

Архитектура. Признак добротности архитектуры - ее концептуальное единство и целостность. По утверждению Брукса, "концептуальная целостность в проектировании важнее всего" []. Как показано в главах 1 и 5, архитектура объектно-ориентированной программной системы содержит структуры классов и объектов, имеющие горизонтальное и вертикальное слоение. Обычно конечному пользователю нет дела до архитектуры системы. Однако, как указывает Страуструп, "ясная внутренняя структура" играет важную роль в построении системы, которая будет понятна, тестируема, устойчива и сможет развиваться и перестраиваться []. Более того, именно ясность архитектуры дает возможность выявить общие абстракции и механизмы, которые можно свести воедино, тем самым делая систему проще, меньше и надежнее.

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

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

Хорошей архитектуре присущи следующие свойства:

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

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

• Архитектура проста, то есть не содержит ничего лишнего: общее поведение достигается общими абстракциями и механизмами.

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

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

Цикл итеративного развития. Рассмотрим две крайности - полное отсутствие формализованного жизненного цикла разработки и очень жесткие, строго соблюдаемые правила разработки. В первом случае мы имеем анархию; тяжким трудом (преимущественно нескольких своих членов) команда разработчиков в конце концов может родить что-то стоящее, но состояние проекта всегда будет неизмеримо и непредсказуемо. Следует ожидать, что команда отработает весьма неэффективно, а, может быть, и вообще не создаст ничего пригодного для передачи заказчику. Это - пример проекта в свободном падении [Есть шанс, что проект в свободном падении приземлится благополучно, но вам не нужно ставить в связи с этим на кон будущее своей компании]. Во втором случае мы имеем диктатуру, в которой инициативы наказуемы, экспериментирование, которое могло бы привнести больше элегантности в архитектуру, не поощряется, и действительные требования заказчика никогда корректно не доходят до разработчиков нижнего уровня, скрытых за настоящей бумажной стеной, воздвигнутой бюрократией.

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

Итеративно развивающийся процесс является антитезой традиционного "водопада" и не сводится к одностороннему движению сверху-вниз или снизу-вверх. Обнадеживающие прецеденты этого стиля есть в опыте создания как аппаратуры, так и программ [, ]. Например, пусть надо сформировать штат фирмы, занимающейся проектированием и изготовлением сложной уникальной аппаратуры. Можно использовать горизонтальный подход, когда проект катится водопадом, так, что архитекторы передают его конструкторам, а те электронщикам. Это - пример проектирования сверху-вниз, когда мы приглашаем узких (хотя и глубоких) специалистов в своей области []. Можно пойти по другому пути, наняв мастеров на все руки, каждому из которых можно поручить вертикальный сегмент проекта от начала до конца. Это уже гораздо больше похоже на итеративно развивающийся процесс.

По нашему мнению, процесс объектно-ориентированного проектирования не сводится к одностороннему движению сверху-вниз или снизу-вверх. Друк считает, что хорошо структурированные сложные системы можно создать методом "возвратного проектирования" (round-trip gestalt design). В этом методе основное внимание уделяется процессу поступательного итеративного развития путем совершенствования различных, но, тем не менее, совместимых между собой логических и физических моделей системы. Мы считаем, что возвратное проектирование составляет необходимую основу процесса объектно-ориентированного проектирования.

В отдельных случаях решаемая задача может быть уже хорошо изучена и много раз запрограммирована. Процесс разработки можно привести в идеальный порядок: проектировщики новой системы уже понимают, какие абстракции являются главными; они уже знают, какие механизмы нужно использовать и каким, в общих чертах, будет поведение системы. Творчество все еще важно в таком процессе, но здесь проблема достаточно сужена и большинство стратегических решений предопределены. Тогда, поскольку риск исключен, можно достичь очень высоких показателей производительности []. Чем больше мы знаем о задаче, тем легче ее решить.

Большинство промышленных задач не таковы: они связаны с балансированием уникальных требований к функциональности и эффективности и требуют полной творческой отдачи всего коллектива разработчиков. Более того, любая человеческая деятельность, которая требует творчества и инноваций, идет путем проб и ошибок, итеративно развивающегося процесса, который опирается на опыт, компетентность и талант каждого члена коллектива [Эксперименты Кертиса и его коллег подкрепляют эти наблюдения. Они изучали работу профессиональных разработчиков программного обеспечения, записывая видеокамерой их действия и затем анализируя их содержание (анализ, проектирование, реализация и т.п.) и время на выполнение. В результате исследований был сделан вывод, что "создание программ представляется набором итеративных, плохо упорядоченных и взаимно перекрывающихся процессов под приспосабливающимся управлением... Развитие по сбалансированной схеме сверху-вниз проявляется как особый случай, когда схема проектирования оказалась вполне подходящей или задача мала по размеру... Хорошие проектировщики работают одновременно на нескольких уровнях абстракции и детализации" []]. Так что нет и не будет стандартных рецептов для проектирования программных систем.

Рациональный процесс проектирования

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

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

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

 ∙ Воспроизводимый   Организация в разумной степени управляет своими планами и обязательствами. 

 ∙ Определенный   Процесс разработки в разумной степени определен, понятен и применяется на практике; он позволяет выбирать команду и предсказывать ход разработки. Следующая цель - оформить выработанную практику разработки как инструментальную среду. 

 ∙ Управляемый   Организация выработала количественные показатели процесса. Цель состоит в снижении затрат на сбор данных и налаживание механизмов обратной связи, позволяющих данным влиять на процесс. 

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

  К сожалению, как отмечают Парнас и Клеменс: "Мы никогда не отыщем процесс, который дал бы нам возможность проектировать программы строго рациональным образом", поскольку дело это творческое и новаторское по определению. Однако, продолжают они, "хорошей новостью является, то, что мы можем его имитировать... (Поскольку) разработчики нуждаются в руководстве, мы приблизимся к рациональной разработке, если будем следовать процессу, а не действовать, как попало. Когда организация занята многими программными продуктами, есть смысл иметь стандартную процедуру... Если мы держим в голове идеальный процесс, становится легче измерять успехи проекта" [].

С приобретением опыта у организации встает вопрос: "Как примирить творчество и новации с возрастающей управляемостью?". Ответ состоит в разграничении макро- и микроэлементов процесса проектирования. Микропроцесс родственен спиральной модели развития, предложенной Боемом, и служит каркасом для итеративного подхода к развитию []. Макропроцесс близок к традиционному "водопаду" и задает направляющие рамки для микропроцесса. Примиряя эти два в корне различных процесса, мы имитируем полностью рациональный процесс разработки и обретаем основу для определенного уровня зрелости в деле создания программного обеспечения.

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

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

 

6.2. Микропроцесс проектирования

Обзор

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

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

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

Как показано на рис. 6-1, микропроцесс обычно состоит из следующих видов деятельности:

• выявление классов и объектов на данном уровне абстракции;

• выяснение семантики этих классов и объектов;

• выявление связей между этими классами и объектами;

• спецификация интерфейса и реализация этих классов и объектов.

Теперь рассмотрим каждый из этих видов деятельности подробно.

Выявление классов и объектов

Цель. Цель выявления классов и объектов состоит в том, чтобы найти границы предметной области. Кроме того, эта деятельность является первым шагом в продумывании объектно-ориентированной декомпозиции разрабатываемой системы.

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

Результаты. Главным результатом этого шага является обновляющийся по мере развития проекта словарь данных. Вначале достаточно составить список действующих лиц, состоящий из всех заметных классов и объектов, названых именами, отражающими их смысл []. Когда словарь разрастется, можно сделать простейшую базу данных, или более специальный инструмент проектирования, непосредственно поддерживающий выбранный метод разработки [Формально, словарь данных объектно-ориентированной разработки должен содержать спецификации каждого элемента архитектуры]. В своих более формальных разновидностях словарь данных служит предметным указателем для всех остальных компонентов проекта, включая диаграммы и спецификации обозначений объектно-ориентированного проектирования.  

Рис. 6-1. Микропроцесс.

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

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

Виды деятельности. Как мы описывали в главе 4, выявление классов и объектов связано с двумя видами творческих актов: открытием и изобретением.

Не каждый член команды должен быть равно искусен во всем. Аналитики, особенно работающие с экспертами в предметной области, должны уметь хорошо обнаруживать абстракции, то есть находить осмысленные классы и объекты в предметной области. Тем временем архитекторы и старшие разработчики придумывают классы и объекты, решающие чисто программистские проблемы. Мы обсудим природу этих творческих актов в следующей главе.

В любом случае основой для выявления классов и объектов служат методы классификации, описанные в главе 4. Обычный порядок действий таков:

• Применить классический подход к классификации (см. раздел 4.2, "Объектно-ориентированный анализ"), чтобы получить множество кандидатов в классы и объекты. В начале жизненного цикла хорошими стартовыми точками являются материальные элементы и их роли. Затем исследовать последовательности событий, что даст другие абстракции первого и второго порядка: в конце концов, для каждого события мы должны иметь объект, который отвечает за его обнаружение и/или обработку.

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

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

В каждом из этих подходов CRC-карточки являются эффективным катализатором "мозгового штурма" и помогают теснее сплотить коллектив, подталкивая его членов к общению [Это ужасно банально, но некоторые проектировщики программ и в самом деле не очень общительны].

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

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

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

Выяснение семантики классов и объектов

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

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

Результаты. На этом шаге получаются несколько результатов. Первым является уточнение словаря данных, с помощью которого мы изначально присвоили обязанности абстракциям. В ходе проектирования мы можем выработать спецификации к каждой абстракции (как описано в главе 5), перечисляя имена операций в протоколе каждого класса. Затем, как можно скорее, мы выразим интерфейсы этих классов на языке реализации. Для C++ это означает создание .h-файлов, в Ada - спецификаций пакетов, в CLOS - обобщенных функций для каждого класса, в Smalltalk - это объявление, но не реализация методов каждого класса. Если проект связан с базой данных, особенно с объектно-ориентированной, на этом шаге мы получаем общий каркас нашей схемы данных.

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

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

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

Виды деятельности. С этим шагом связано три вида деятельности: раскадровка, проектирование изолированных классов и поиск шаблонов.

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

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

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

• По ходу раскадровки перераспределить обязанности так, чтобы сбалансировать поведение. Где возможно, использовать или адаптировать уже существующие обязанности. Очень распространенным приемом является деление больших обязанностей на малые; иногда тривиальные обязанности объединяются в более сложные.

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

В начале разработки проекта мы можем задавать семантику классов и объектов в свободной форме, просто описывая обязанности каждой абстракции. Обычно достаточно фразы или предложения; если этого мало - мы встречаем верный признак того, что данная обязанность является чрезмерно сложной и должна разделиться на меньшие. На более поздних стадиях разработки, когда мы будем заниматься доводкой протоколов отдельных абстракций, можно указать имена специфических операций, не определяя их полные сигнатуры, которые мы выясним потом. Таким образом, мы получим соответствие: каждая обязанность выполняется набором операций, а каждая операция как-либо участвует в выполнении обязанностей соответствующей абстракции. После этого, чтобы отразить динамическую семантику протоколов классов [Как мы описывали в главе 3, протокол определяет, что некоторые операции должны вызываться в определенном порядке. Для всех случаев кроме самых тривиальных операции редко встречаются в одиночестве; выполнение каждой из них имеет свои предусловия, проверка которых часто требует вызова других операции], имеющих управляемое событиями или зависящее от состояния поведение, мы можем построить конечные автоматы для них.

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

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

• Выбрать одну абстракцию и перечислить ее роли и обязанности.

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

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

• Учесть конструирование, копирование и уничтожение объектов []. Если не имеется причин поступить иначе, лучше иметь общие стратегические принципы для таких операций, чем позволить отдельным классам вводить свои собственные решения.

• Придать завершенность: добавить другие примитивные действия, которые не нужны существующим клиентам, но "округляют" абстракцию, что повышает вероятность использования ее новыми клиентами. Помня, что невозможно иметь полную завершенность, стремиться к простоте.

Важно избегать преждевременного определения отношения наследования - это часто ведет к потере целостности типа.

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

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

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

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

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

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

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

Качественные показатели включают все эвристики классов, описанные в главе 3. Сложные и туманные обязанности и операции говорят о том, что абстракции еще недостаточно определены. Невозможность написать конкретный файл заголовков или как-либо по другому формализовать интерфейс классов также говорит о том, что абстракции плохо сформулированы, или что основные понятия определяли не те люди [Остерегайтесь аналитиков и архитекторов, если они не хотят или не могут выразить конкретно семантику своих абстракции; это признак надменности или беспомощности].

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

Выявление связей между классами и объектами

Цель. Цель выявления связей между классами и объектами - уточнить границы каждой обнаруженной ранее в микропроцессе абстракции и опознать все сущности, с которыми она взаимодействует. Это действие формализует концептуальное и физическое размежевание между абстракциями, начатое на предыдущем шаге.

Мы применяем этот шаг в анализе для спецификации связей между классами и объектами (включая некоторые важные отношения наследования и агрегации).

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

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

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

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

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

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

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

На этом же шаге также обновляется словарь данных. В нем отражаются распределения классов и объектов по категориям и модулей по подсистемам.

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

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

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

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

• Для каждой ассоциации определить роль каждого участника, если необходимо уточнить кардинальность и выявить другие ограничения.

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

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

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

• Если между классами наблюдается общность, необходимо поместить эти классы в иерархию "общее/частное". Как говорилось в главе 3, обычно лучше создать "лес" классов, чем единое дерево. На предыдущем шаге мы уже определили кандидатов на базовые, абстрактные классы и классы-примеси; теперь нужно разместить их в структуре наследования. Для существенных классов следует рассмотреть диаграммы классов и оценить их качество, согласно эвристикам главы 3. В частности, требует особого внимания иерархическая структура: она не должна быть слишком высокой или слишком короткой, чересчур широкой или узкой. Там, где встречаются шаблоны в структуре или поведении, нужно реорганизовать иерархию так, чтобы максимизировать общность (но не в ущерб простоте).

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

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

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

Отношения наследования, агрегации, инстанцирования и использования - важнейшие типы ассоциаций, представляющие для нас интерес вместе с такими свойствами, как метки, роли, кардинальность и т.д. Типичный порядок уточнения ассоциаций таков:

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

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

• Найти классы с похожим поведением, которые либо находятся на одном уровне, либо еще не входят в структуру наследования и рассмотреть возможность введения общих параметризованных классов.

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

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

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

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

Реализация классов и объектов

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

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

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

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

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

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

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

Типичный порядок действий таков:

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

• Рассмотреть объекты, которым можно делегировать обязанности. Для достижения эффективности может потребоваться незначительная реорганизация обязанностей и/или протокола абстракции нижнего уровня.

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

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

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

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

 

6.3. Макропроцесс проектирования

Обзор

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

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

Макропроцесс заботит в первую очередь технического руководителя команды разработчиков, цели которого несколько отличаются от задач отдельного разработчика. Они оба заинтересованы в качестве конечного программного продукта, удовлетворяющем требованиям заказчика [Ну, конечно, не все, а большинство. К сожалению, некоторые менеджеры больше заинтересованы в развитии своей империи, чем в развитии программного продукта. Прибавьте к этому предыдущее примечание относительно аналитиков и проектировщиков. Я думаю, Данте мог бы найти для них подходящее место]. Однако, конечного пользователя мало волнует, правильно ли использованы в проекте параметризованные классы или полиморфизм; заказчик гораздо более обеспокоен сроками, качеством, полнотой и правильностью работы программы. Поэтому макропроцесс сконцентрирован на управлении риском и выявлении общей архитектуры - двух управляемых компонентах, имеющих решающее значение для сроков, полноты и качества проекта.

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

• Выявление сущности требований к программному продукту (концептуализация).

• Разработка модели требуемого поведения системы (анализ).

• Создание архитектуры для реализации (проектирование).

• Итеративное выполнение реализации (эволюция).

• Управление эволюцией продукта в ходе эксплуатации (сопровождение).

 

Рис. 6-2. Макропроцесс.

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

Основная философия макропроцесса состоит в постепенном развитии. Как его определяет Вонк, "при разработке методом последовательного развития, система выстраивается шаг за шагом, причем каждая новая версия содержит функциональность предыдущей, плюс новые функции" []. Этот подход чрезвычайно хорошо сочетается с объектно-ориентированной парадигмой и дает много возможностей для управления риском. Как утверждает Гилб: "Постепенная передача программ заказчику изобретена для того, чтобы заранее предупредить нас о надвигающихся неприятностях" [].

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

Концептуализация

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

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

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

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

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

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

• Решить, какие цели преследуются при опробовании концепции и каковы критерии того, что считать благополучным исходом.

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

• Оценить готовый прототип и принять ясное решение о проектировании конечного продукта или о дальнейшем исследовании. Решение приступить к разработке конечного продукта нужно принимать с разумным учетом потенциального риска, выявленного при опробовании концепции.

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

Довольно часто концепции опробуются на одном языке (например, на Smalltalk), а разработка конечного продукта ведется на другом (скажем, C++).

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

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

Анализ

Цель. Как утверждает Меллор, "цель анализа - дать описание задачи. Описание должно быть полным, непротиворечивым, пригодным для чтения и обозрения всеми заинтересованными сторонами, реально проверяемым" []. Говоря нашим языком, цель анализа - представить модель поведения системы.

Надо подчеркнуть, что анализ сосредоточен не на форме, а на поведении. На этой фазе неуместно заниматься проектированием классов, представлением или другими тактическими решениями. Анализ должен объяснить, что делает система, а не то, как она это делает. Любое, сделанное на стадии анализа (вопреки этому правилу) утверждение о том "как", может считаться полезным только для демонстрации поведения системы, а не как проверяемое требование к ее проектированию.

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

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

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

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

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

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

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

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

Виды деятельности. С анализом связаны два основных вида деятельности: анализ предметной области и планирование сценариев.

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

Планирование сценариев является центральным действием анализа. Интересно, что по этому вопросу, кажется, имеется совпадение мнений среди других методологов, особенно у Рубина и Голдберга (Rubin adn Goldberg), Адамса (Adams), Вирфс-Брока (Wirfs-Brock), Коада (Coad) и Джекобсона (Jacobson). Типичный порядок его выполнения следующий:

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

• Для каждого представляющего интерес набора функциональных точек сделать раскадровку сценария, используя технику анализа поведения и примеров использования, описанную в главе 4 [Всесторонний анализ этого предмета можно найти в работах Джекобсона [] и Рубина и Голдберга []]. В мозговом штурме каждого сценария эффективна техника CRC-карточек. Когда прояснится семантика сценариев, следует документировать их, используя диаграммы объектов, которые иллюстрируют объекты, инициирующие и обеспечивающие поведение, и их взаимодействие при выполнении действий сценария. Приложить описание событий, происходящих при выполнении сценария, и порядок выполняемых в результате действий. Кроме того, необходимо перечислить все предположения, ограничения и показатели эффективности для каждого сценария [].

• Если необходимо, сделать вторичные сценарии, иллюстрирующие поведение системы в исключительных ситуациях.

• Для объектов с особо важным жизненным циклом описать диаграммы состояний (построить конечный автомат).

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

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

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

Путевые вехи и характеристики. Мы благополучно завершим эту фазу, когда мы будем иметь уточненные и подписанные сценарии для всех фундаментальных типов поведения системы. Говоря подписанные, мы предполагаем, что конечные результаты анализа проверялись экспертами, конечными пользователями, аналитиками и архитекторами; говоря фундаментальные, мы имеем в виду типы поведения, основные для данного приложения. Повторим, мы не ожидаем полного анализа, - достаточно рассмотреть только основные и несколько второстепенных видов поведения.

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

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

Проектирование

Цель. Цель проектирования - создать архитектуру развивающейся реализации и выработать единые тактические приемы, которыми должны пользоваться различные элементы системы. Мы начинаем процесс проектирования сразу после появления некоторой приемлемой модели поведения системы. Важно не начинать проектирование до завершения анализа. Равным образом важно избегать затягивания проектирования, пытаясь получить идеальную, а следовательно, недостижимую аналитическую модель [Такая ситуация обычно классифицируется как паралич анализа].

Результаты. Имеется два основных результата проектирования: описание архитектуры и выработка общих тактических приемов.

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

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

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

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

Виды деятельности. С проектированием связано три действия: архитектурное планирование, тактическое проектирование и планирование релизов.

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

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

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

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

Архитектурное планирование сконцентрировано на том, чтобы создать в самом начале жизненного цикла каркас системы, а потом постепенно развивать его.

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

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

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

• Документировать каждый принцип и распространить полученные документы, чтобы обеспечить единое архитектурное видение.

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

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

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

• Определить цели и расписание релизов так, чтобы дать время на разработку и синхронизировать релизы с другими действиями, например, с разработкой документации и полевыми испытаниями.

• Начать планирование задач, учитывая критические места проекта и ресурсы, отведенные на выпуск каждого релиза.

Естественным побочным результатом планирования релизов является план, в котором определены расписание работ, задачи коллектива и оценка риска.

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

Основным признаком совершенства является простота. Хорошая архитектура имеет характеристики организованной сложной системы (см. главу 1).

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

Эволюция

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

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

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

Пейдж-Джонс называет ряд преимуществ такой поступательной разработки:

• "Обеспечивается обратная связь с пользователями, когда это больше всего необходимо, полезно и значимо.

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

• Менее вероятно, что проект будет снят с финансирования, если он вдруг выбился из графика.

• Главные интерфейсы системы тестируются в первую очередь и наиболее часто.

• Более равномерно распределяются ресурсы на тестирование.

• Реализаторы могут быстрее увидеть первые результаты работы системы, что их морально поддерживает.

• Если сроки исполнения сжатые, то можно приступить к написанию и отладке программ до завершения проектирования".

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

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

Для кого делается действующий релиз программы? В начале процесса разработки основные действующие релизы передаются разработчиками контролерам качества, которые тестируют их по сценариям, составленным при анализе, и накапливают информацию о полноте, корректности и устойчивости работы релиза. Это раннее накопление данных помогает при выявлении проблем качества, которые будут учтены в следующих релизах. Позднее действующие релизы передаются конечным (альфа и бета) пользователям некоторым управляемым способом. "Управляемым" означает, что разработчики тщательно выверяют требования к каждому релизу и определяют аспекты, которые желательно проверить и оценить.

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

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

Виды деятельности. Эволюция связана с двумя видами деятельности: микропроцесс и управление изменениями.

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

• Определить функциональные точки, которые попадут в новый релиз, и области наивысшего риска, особенно те, которые были выявлены еще при эволюции предыдущего релиза.

• Распределить задачи по релизам среди членов команды и начать новый микропроцесс. Контролировать микропроцесс, просматривая проект, и проверять состояние дел в важных промежуточных этапах с интервалами от нескольких дней до двух недель.

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

• Завершить микропроцесс интеграцией и очередным действующим релизом.

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

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

При эволюции системы на практике ожидаются следующие типы изменений:

• Добавление нового класса или нового взаимодействия между классами.

• Изменение реализации класса.

• Изменение представления класса.

• Реорганизация структуры классов.

• Изменение интерфейса класса.

Каждый тип изменений имеет свою причину и стоимость.

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

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

Подобным образом можно было бы изменить представление класса (в C++ - защищенные и закрытые члены класса). Обычно это делается, чтобы получить более эффективные (с точки зрения памяти или скорости) экземпляры класса. Если представление класса инкапсулировано, что возможно в Smalltalk, C++, CLOS и Ada, то изменение в представлении не будет разрушать логику взаимодействия объектов-пользователей с экземплярами класса (если, конечно, новое представление обеспечивает ожидаемое поведение класса). С другой стороны, если представление класса не инкапсулировано, что также возможно в любом языке, то изменение в представлении класса чрезвычайно опасно, так как клиенты могут от него зависеть. Это особенно верно в случае подклассов: изменение представления суперкласса вызовет изменения представления всех его подклассов. Во всяком случае, изменение представления класса имеет цену: нужно произвести перекомпиляцию интерфейса и реализации класса, сделать то же для всех его клиентов, для клиентов тех клиентов и т.д.

Реорганизация структуры классов системы встречается довольно часто, хотя и реже, чем другие упомянутые виды изменений. Как отмечают Стефик и Бобров, "Программисты часто создают новые классы и реорганизуют имеющиеся, когда они видят удобную возможность разбить свои программы на части" [26]. Изменение структуры классов обычно происходит в форме изменения наследственных связей, добавления новых абстрактных классов и перемещения обязанностей и реализации общих методов в классы более верхнего уровня в иерархии классов. На практике структура классов системы особенно часто реорганизуется вначале, а потом, когда разработчики лучше поймут взаимодействие ключевых абстракций, стабилизируется. Реорганизация структуры классов поощряется на ранних стадиях проектирования, потому что в результате может получиться более лаконичная программа. Однако реорганизация структуры классов не обходится даром. Обычно изменение положения верхнего класса в иерархии делает устаревшими определения всех классов под ним и требует их перекомпиляции (а, значит, и перекомпиляции всех зависимых от них классов и т.д.).

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

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

Почему перекомпиляция так неприятна? Для маленьких систем здесь нет проблем: перекомпиляция всей системы занимает несколько минут. Однако для больших систем это совсем другое дело. Перекомпиляция программы в сотни тысяч строк может занимать до половины суток машинного времени. Представьте себе, что вам понадобилось внести изменение в программное обеспечение компьютерной системы корабля. Как вы сообщите капитану, что он не может выйти в море, потому что вы все еще компилируете? В некоторых случаях цена перекомпиляции бывает так высока, что разработчикам приходится отказаться от внесения некоторых, представляющих разумные усовершенствования, изменений. Перекомпиляция представляет особую проблему для объектно-ориентированных языков, так как наследование вводит дополнительные компиляционные зависимости []. Для строго типизированных объектно-ориентированных языков программирования цена перекомпиляции может быть даже выше; в этих языках время компиляции принесено в жертву безопасности.

Все изменения, обсуждавшиеся до настоящего времени, сравнительно легкие: самый большой риск несут существенные изменения в архитектуре, которые могут погубить весь проект. Часто такие изменения производят чересчур блестящие инженеры, у которых слишком много хороших идей [].

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

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

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

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

Сопровождение

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

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

• "Эксплуатируемая программа должна непрерывно изменяться; в противном случае она будет становиться все менее и менее полезной (закон непрерывного изменения).

• Когда эволюционирующая программа изменяется, ее структура становится более сложной, если не прилагаются активные усилия, чтобы этого избежать (закон возрастающей сложности)" [].

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

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

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

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

• Упорядочить по приоритетам предложения о крупных изменениях и сообщения об ошибках, связанных с системными проблемами, и оценить стоимость переработки.

• Составить список этих изменений и принять их за функциональные точки в дальнейшей эволюции.

• Если позволяют ресурсы, запланировать в следующем релизе менее интенсивные, более локализованные улучшения.

• Приступить к разработке следующего эволюционного релиза программы.

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

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

 

Выводы

• Удачные проекты обычно характеризуются ясным представлением об архитектуре и хорошо управляемым итеративным жизненным циклом.

• Идеально рациональный процесс проектирования невозможен, но его можно имитировать, сочетая микро- и макропроцесс разработки.

• Микропроцесс объектно-ориентированной разработки приводится в движение потоком сценариев и продуктов архитектурного анализа (макропроцесс); микропроцесс представляет ежедневную деятельность команды разработчиков.

• Первый шаг в микропроцессе связан с идентификацией классов и объектов на данном уровне абстракции; основными видами деятельности являются открытие и изобретение.

• Второй шаг микропроцесса состоит в выявлении семантики классов и объектов; основными видами деятельности здесь являются раскадровка сценариев, проектирование изолированных классов и поиск шаблонов.

• Третий шаг микропроцесса - выявление связей между классами и объектами; основными действиями являются спецификация ассоциаций, выявление взаимодействий и уточнение ассоциаций.

• Четвертый шаг микропроцесса связан с реализацией классов и объектов; основное действие - выбор структур данных и алгоритмов.

• Макропроцесс объектно-ориентированной разработки управляет микропроцессом, определяет измеримые характеристики проекта и помогает контролировать риск.

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

• Второй шаг макропроцесса - анализ. Его цель - получить модель поведения системы. Основными действиями на этом этапе являются анализ предметной области и планирование сценариев.

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

• Четвертый шаг макропроцесса - эволюция, последовательно приближающая систему к желаемому результату. Основные действия - применение микропроцесса и управление изменениями.

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

 

Дополнительная литература

Ранняя форма процесса, описанного в этой главе, была впервые опубликована Бучем (Booch) [F 1982]. Берард (Berard) позднее развил эту работу в статье [F 1986]. Среди родственных подходов можно назвать GOOD (General Object-Oriented Design) Сейдвица и Старка (Seidewitz and Stark) [F 1985,1986,1987], SOOD (Structured Object-oriented Design) корпорация Локхид (Lockheed) [С 1988], MOOD (Multipleview Object-oriented Design) Керта (Kerth) [F 1988], и HOOD (Hierarchical Object-oriented Design), предложенный CISI Ingenierie и Matra для европейской космической станции [F 1987]. Более свежие ссылки: Страуструп (Stroustrup) [G 1991] и Microsoft [G 1992], где предложены сходные процессы.

В дополнение к работам, упомянутым в дополнительной литературе к главе 2, ряд других методологов предложил специфические процессы объектно-ориентированного развития. На эти работы есть много библиографических ссылок. Вот наиболее интересные из них: Алабио (Alabios) [F 1988], Бойд (Boyd) [F 1987], Бур (Buhr) [F 1984], Черри (Cherry) [F 1987,1990], деШампо (deChampeaux) [F 1992], Фелсингер (Felsinger) [F 1987], Файерсмит (Firesmith) [F 1986,1993], Хайнс и Юнгер (Hines and Unger) [G 1986], Дже-кобсон (Jacobson) [F 1985], Джамса (Jamsa) [F 1984], Кади (Kadie) [F 1986], Мазиеро и Германо (Masiero and Germano) [F 1988], Ниелсен (Nielsen) [F 1988], Ниес (Nies) [F 1986], Рэйлич и Сильва (Railich and Silva) [F 1987], Грэхем (Graham) [F 1987].

Сравнение различных процессов объектно-ориентированного развития можно найти в работах Арнольда (Arnold) [F 1991], Боем-Дэвиса и Росса (Boehm-Davis and Ross) [H 1984], деШампо (deChampeaux) [В 1991], Криббса, Муна и Ро (Cribbs, Moon, and Roe) [F 1992], Фоулер (Fowler) [F 1992], Келли (Kelly) [F 1986], Манино (Mannino) [F 1987], Сонга (Song) [F 1992], Вебстера (Webster) [F 1988]. Брукман (Brookman) [F 1991] и Фичмэн (Fichman) [F 1992] сравнили структурные и объектно-ориентированные методы.

Эмпирические исследования процессов создания программного обеспечения можно найти в работе Кертис (Curtis) [H 1992], а также в трудах Software Process Workshop [H 1988]. Еще одно интересное исследование принадлежит Гвиндону (Guindon) [H 1987], изучавшему процессы, которые разработчики использовали раньше. Речтин (Rechtin) [H 1992] предложил прагматическое руководство для системного архитектора, который должен управлять процессом развития.

Интересная ссылка по вопросу о "созревании" программного продукта - это работа Хэмфри (Hamphrey) [H 1989]. Классическая ссылка на то, как симитировать этот процесс, - статья Парнаса (Parnas) [H 1986].

 

Глава 7 Практические вопросы

 

Разработка программ пока остается чрезвычайно трудоемким делом, в значительной степени она по-прежнему больше напоминает строительство коттеджей, чем промышленное возведение зданий []. Доклад Кишиды и др. свидетельствует, что даже в Японии на начальной стадии проектов "все еще по большей части полагаются на неформальный подход - карандаш и бумагу" [].

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

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

 

7.1. Управление и планирование

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

Управление риском

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

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

Многие виды деятельности по управлению разработкой программного обеспечения, например, планирование задач и просмотры, предусмотрены не только в объектно-ориентированной технологии. Однако при управлении объектно-ориентированным проектом намечаемые задачи и рассматриваемые результаты не совсем такие, как в других системах.

Планирование задач

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

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

Планирование задач связано с построением графика представления результатов макропроцесса. В промежутках между очередными релизами менеджеры команды должны оценить трудности, угрожающие проекту [Гилб замечает: "если вы не идете в атаку на трудности, трудности идут в атаку на вас" []], сконцентрировать ресурсы, чтобы разрешить возникшие проблемы, и далее заниматься новой итерацией микропроцесса, в результате которой нужно получить стабильную систему, удовлетворяющую сценариям, запланированным для нового релиза. Планирование задач на этом уровне очень часто оказывается неудачным из-за чрезмерно оптимистических графиков []. Разработка, которая рассматривалась как "просто вопрос программирования", растягивается на многие дни работы; графики выбрасываются в корзину, когда разработчик, занимаясь частью системы, предполагает определенные протоколы для других частей системы, а потом получает неполно или неправильно изготовленные классы. Смертельную опасность могут представлять внезапно обнаружившиеся ошибки в компиляторе или то, что программа не укладывается в заданное время исполнения. И то и другое часто приходится преодолевать, жертвуя принятыми ранее тактическими решениями.

Ключ к тому, чтобы не поддаваться чрезмерно оптимистическому планированию, - "калибровка" команды и ее инструментов разработки. Типичное планирование задач протекает следующим образом. Вначале менеджер направляет энергию разработчика на специфические части системы, например на проектирование классов для интерфейса с реляционной базой данных. Разработчик анализирует необходимые усилия и оценивает время исполнения, которое менеджер учитывает при планировании других его действий. Проблема в том, что эти оценки не всегда реальны: они обычно делаются в расчете на самый благоприятный случай. Один разработчик может согласиться на решение задачи за неделю, а другой на эту же задачу попросит месяц. Когда работа будет реально выполнена, может оказаться, что она отняла три недели рабочего времени у обоих разработчиков: первый разработчик недооценил усилия (общая проблема многих программистов), а второй разработчик оценил реальные усилия более точно (например потому, что он понимал разницу между действительным рабочим временем и календарным, которое часто заполнено множеством нефункциональных действий). Таким образом, чтобы разработать графики, к которым коллектив может иметь доверие, менеджерам необходимо ввести своего рода "калибровочные коэффициенты" для пересчета оценок времени, заявленных разработчиками. Это не признак того, что менеджеры не доверяют разработчикам, но просто признание того факта, что большинство программистов сосредоточены на технических проблемах, а не на задачах планирования. Менеджер должен помогать разработчикам учиться планировать, - но это тот навык, который может быть приобретен только опытом.

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

Просмотр

Просмотр (walkthroughs) - общепринятая практика, которую нужно использовать каждой команде разработчиков. Как и планирование задач, просмотр программного обеспечения был введен независимо от объектно-ориентированной технологии. Однако при просмотре не объектно-ориентированных систем внимание обращается на другое.

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

Как описано в предыдущей главе, сценарии являются первичным результатом объектно-ориентированного анализа. Они должны выражать требуемое поведение системы в терминах ее функциональных точек. Формальные просмотры сценариев проводятся аналитиками команды, вместе с экспертами предметной области или конечными пользователями при возможном участии других разработчиков. Лучше проводить такие просмотры на протяжении всей стадии анализа, чем ожидать выполнения одного глобального просмотра по завершении анализа, когда будет уже слишком поздно сделать что-нибудь полезное, перенаправив усилия аналитиков. Эксперименты показывают, что даже непрограммисты могут понять сценарии, представленные в виде текста или диаграмм объектов [Мы встречались с использованием этой системы обозначении в работе таких непрограммистских групп как астрономы, биологи, метеорологи, физики и банкиры]. В конечном счете просмотр помогает выработать общий словарь для разработчиков и пользователей системы. Привлечение к участию в просмотре других членов команды способствует уяснению ими реальных требований к системе на ранних этапах разработки.

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

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

 

7.2. Кадры

Распределение ресурсов

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

На стадии анализа потребность в ресурсах с переходом на объектно-ориентированные методы обычно мало изменяется. Однако, поскольку объектно-ориентированный процесс уделяет больше внимания архитектуре, мы стремимся привлекать архитекторов и других разработчиков как можно раньше, иногда начиная архитектурные эксперименты еще на последней стадии анализа. Во время эволюции, как правило, потребуется меньше ресурсов, потому что работа облегчится общими абстракциями и механизмами, изобретенными ранее при проектировании архитектуры или выпуске предварительных версий. Тестирование может также потребовать меньше ресурсов, потому что новые функции обычно добавляются к уже корректно ведущей себя структуре класса или механизму. Таким образом, тестирование начинается раньше и является скорее постоянным и постепенным, чем разовым действием. Интеграция обычно требует значительно меньших ресурсов по сравнению с традиционными методами, главным образом потому, что она тоже происходит постепенно, от релиза к релизу, а не одним броском. Таким образом, в устойчивом состоянии трудозатраты оказываются гораздо меньше, чем при традиционных подходах. Более того, если учесть эксплуатационные затраты, то окажется, что весь жизненный цикл объектно-ориентированных программ часто стоит дешевле, так как конечный продукт, скорее всего, будет лучшего качества и окажется более приспособленным к изменениям.

Роли разработчиков

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

Эксперименты показывают, что объектно-ориентированная разработка требует несколько иного разделения труда по сравнению с традиционными методами. Мы считаем следующие три роли разработчиков важнейшими в объектно-ориентированном подходе:

• архитектор проекта;

• ответственные за подсистемы;

• прикладные программисты.

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

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

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

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

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

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

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

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

∙ Менеджер проекта   Отвечает за управление материалами проекта, заданиями, ресурсами и графиком работ. 

 ∙ Аналитик   Отвечает за развитие и интерпретацию требований конечных пользователей; должен быть экспертом в проблемной области, однако его не следует изолировать от остальной команды разработчиков. 

 ∙ Инженер по повторному использованию   Управляет хранилищем (репозитарием) материалов проекта; участвуя в просмотре и других действиях, активно ищет общее и добивается его использования; находит, разрабатывает или приспосабливает компоненты для общего использования в рамках конкретного проекта или целой организации. 

 ∙ Контролер качества   Измеряет результаты процесса разработки; задает общее направление (на системном уровне) тестирования всех прототипов и релизов. 

 ∙ Менеджер интеграции   Отвечает за сборку совместимых друг с другом версий категорий и подсистем в релизы; следит за их конфигурированием. 

 ∙ Ответственный за документацию   Готовит документацию по выпускаемому продукту и его архитектуре для конечного пользователя. 

 ∙ Инструментальщик   Отвечает за создание и адаптацию инструментов программирования, которые облегчают производство программ и (особенно) генерацию кода. 

 ∙ Системный администратор   Управляет физическими компьютерными ресурсами в проекте. 

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

Опыт показывает, что объектно-ориентированная разработка может обойтись меньшим числом занятых в ней людей по сравнению с традиционными методами. На самом деле, чтобы за один год произвести высококачественную программу объемом в несколько сот тысяч строк достаточно 30-40 разработчиков. Однако мы согласны с Боемом, который считает, что "лучшие результаты получаются, когда разработчиков занято меньше, а квалификация их выше" []. К сожалению, если при разработке проекта пытаться обойтись меньшим количеством людей, чем считается необходимым, можно встретить известное сопротивление. Как отмечалось в предыдущей главе, такое отношение, возможно, вызвано попытками некоторых менеджеров построить империю. Другие менеджеры любят скрываться за множеством служащих, потому что большее количество людей означает больше власти. Кроме того, в случае провала проекта есть на кого свалить вину. Применение самого изощренного метода проектирования или новейших инструментов не освобождает менеджера от ответственности за подбор проектировщиков, способных мыслить, и не является основанием для того, чтобы пустить проект на самотек [].

 

7.3. Управление релизами

Интеграция

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

В предыдущей главе объяснялось, что в процессе объектно-ориентированной разработки интеграция редко проводится за один раз. Обычно происходит множество мелких интеграции, каждая из которых соответствует созданию нового прототипа или архитектурного релиза. Каждый новый релиз поступательно развивает предыдущие стабильные релизы. "При итеративной разработке сначала строится программный продукт, удовлетворяющий нескольким конечным требованиям, но конструктивно выполненный так, чтобы облегчить затем удовлетворение всех требований и достичь таким образом большей адаптируемости" []. С точки зрения конечного пользователя, поток релизов проистекает из макропроцесса. Каждый следующий релиз поддерживает все больше функций, и в конечном счете они развиваются в готовую систему. С точки зрения человека, наблюдающего процесс изнутри, создается много больше релизов, но только некоторые из них будут "заморожены", чтобы стабилизировать важнейшие интерфейсы системы, и взяты за основу для дальнейшей работы. Такая стратегия дает возможность снизить риск разработки, ускоряет выявление проблем в архитектуре и узких мест уже на ранних стадиях.

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

Управление конфигурацией и версиями

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

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

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

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

Тестирование

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

• Тестирование модулей.

Предполагает тестирование отдельных классов и механизмов; является обязанностью инженера, который их реализовал.

• Тестирование подсистем.

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

• Тестирование системы

Предполагает тестирование системы как целого; является обязанностью контролеров качества; тестирование системы, как правило, тоже происходит регрессивно.

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

 

7.4. Повторное использование

Элементы повторного использования

Любой программный продукт (текст программы, архитектура, сценарий или документация) может быть использован повторно. Как сказано в главе 3, в объектно-ориентированных языках программирования первичным лингвистическим средством повторного использования являются классы: класс может порождать подклассы, специализирующие или дополняющие его. Далее, в главе 4 говорилось о повторном использовании шаблонов классов, объектов и элементов проектирования в форме идиом, механизмов и сред разработки. Повторное использование шаблонов находится на более высоком уровне абстракции по сравнению с использованием индивидуальных классов и дает больший выигрыш (хотя оно труднее достижимо).

Не следует доверять цифрам, характеризующим повторное использование []. В удачных проектах, с которыми мы сталкивались, количество повторно использованных элементов доходило до 70% (то есть почти три четверти программного обеспечения системы было взято без изменений из некоторого другого источника), но бывало и нулевым. Не следует думать, что повторное использование должно достичь некоторой обязательной величины; возможность повторного использования сильно зависит от предметной области и нетехнических факторов, таких, например, как степень напряженности рабочего графика, природа отношений с субподрядчиками и соображения безопасности.

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

Как осуществить повторное использование?

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

Лучше всего поручить повторное использование кому-то лично. Как описывалось в предыдущей главе, надо искать возможные общности, обычно выявляемые при просмотре архитектуры, реализовывать их, создавая новые или приспосабливая старые компоненты, а потом отстаивать их перед другими разработчиками. Даже простые формы поощрения, такие, как равное признание автора первоначального кода и первооткрывателя возможности заимствования, оказывают стимулирующее воздействие. Можно придумать что-нибудь посущественнее - обед в ресторане или путешествие на выходные для двоих - и присуждать эти поощрения тем разработчикам, чьи решения были заимствованы чаще всего, или тем, которые заимствовали наибольшую часть кода за заданное время [Близкие к разработчикам люди часто терпят некоторый моральный урон в заключительной горячке разработки, и такая компенсация им будет весьма кстати].

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

 

7.5. Качество и измерения

Качество программного продукта

Шульмейер и МакМанус определяют качество программного продукта как "пригодность к использованию" []. Качество программы не должно быть делом случая. Качество должно гарантироваться процессом разработки. На самом деле, объектно-ориентированной технология не порождает качества автоматически: можно написать сколь угодно плохие программы на любом объектно-ориентированном языке программирования.

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

Контроль качества программного продукта - это "систематические действия, подтверждающие пригодность к использованию программного продукта в целом" []. Цель контроля качества - дать нам количественные меры качества программной системы. Многие традиционные количественные меры непосредственно приложимы и к объектно-ориентированным системам.

Как описывалось выше, разбор и просмотр не теряют своей значимости в объектно-ориентированных системах, позволяя предсказать качество системы и влиять на него. Возможно, самым главным количественным критерием качества является количество обнаруженных ошибок. Во время эволюции системы мы учитываем программные ошибки в соответствии с их весом и расположением. График обнаружения ошибок отображает зависимость количества обнаруженных ошибок от времени. Как указывает Доббинс, "не так важно действительное число ошибок, как наклон этого графика" []. Для управляемого процесса этот график имеет форму горба, с вершиной примерно в середине периода тестирования, а дальше эта кривая падает до нуля. Неуправляемому процессу соответствует неубывающая со временем или медленно убывающая кривая.

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

Другая количественная мера - плотность ошибок. Количество обнаруженных ошибок на килостроку программного текста (KSLOC - Kilo Source Lines Of Code) является традиционным показателем, применимым, в частности, к объектно-ориентированным системам. В "здоровых" проектах плотность ошибок "имеет тенденцию достигать стабильного значения при просмотре примерно 10 KSLOC. Просматривая код далее, мы не должны наблюдать увеличения этого показателя" [].

Мы полагаем, что в объектно-ориентированных системах полезно также измерять число ошибок на категорию классов или на класс. При этом правило 80/20 считается приемлемым: 80% выявленных ошибок в программе сосредоточено в 20% классов системы [].

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

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

Наверное, самый скверный способ оценить сделанную работу, каким может воспользоваться управляющий, - сосчитать количество написанных строк текста программы. Число строк, попавших во фрагмент кода, абсолютно никак не связано с его завершенностью и сложностью. В дополнение к другим недостаткам этого неандертальского подхода, в нем слишком просто играть с цифрами, что приводит к оценкам производительности, отличающимся друг от друга более, чем на два порядка. Например, что такое строка программы (особенно на Smalltalk)? Считаются ли физические строки или точки с запятой? Как учесть несколько операторов на одной строке и операторы, которые занимают более одной строки? А как измерить количество затраченного труда? Считать код работой всего персонала или, может быть, только программистов, написавших реализацию? Рабочий день должен считаться восьмичасовым или время, проведенное за утренней раскачкой, тоже должно учитываться? Традиционные меры сложности более подходят для первых поколений языков программирования, они не являются показателями завершенности и сложности объектно-ориентированной системы.

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

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

Чидамбер и Кемерер предлагают несколько мер, которые непосредственно применимы к объектно-ориентированным системам []:

• взвешенная насыщенность класса методами;

• глубина дерева наследования;

• число потомков;

• зацепление объектов;

• отклик на класс;

• недостаток связности в методах.

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

Глубина дерева наследования и число потомков - количественные характеристики формы и размера структуры классов. Как обсуждалось в главе 3, хорошо структурированная объектно-ориентированная система чаще бывает организована как лес классов, чем как одно высоченное дерево. Мы советуем строить сбалансированные по глубине и ширине структуры наследования: обычно - не глубже, чем 7╠2 уровня, и не шире, чем 7╠2 ветви.

Зацепление объектов - мера их взаимозависимости. Так же, как и при традиционном программировании, мы стремимся спроектировать незацепленные (то есть слабо связанные) объекты, поскольку они имеют больше шансов на повторное использование.

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

 

7.6. Документация

Наследие разработки

Разработка программной системы включает в себя гораздо больше, чем просто написание кода. Некоторые аспекты проекта должны быть постоянно доступны менеджерам разработки и внешним пользователям. Мы хотим также сохранить сведения о решениях, принятых при анализе и проектировании для тех, кто будет заниматься сопровождением системы. Как отмечалось в главе 5, результаты объектно-ориентированной разработки обычно содержат диаграммы классов, объектов, модулей и процессов. В совокупности эти диаграммы предоставляют возможность проследить их появление непосредственно из начальных требований к системе. Диаграммы процессов соответствуют программам, которые являются корневыми модулями диаграммы модулей. Каждый модуль представляет реализацию некоторой комбинации классов и объектов, которые, в свою очередь, можно найти на диаграммах классов и объектов соответственно. Наконец, диаграммы объектов представляют сценарии, определяемые требованиями, а диаграммы классов демонстрируют ключевые абстракции, которые формируют словарь предметной области.

Содержание документации

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

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

Документация по архитектуре и реализации должна описывать:

• архитектуру системы верхнего уровня;

• ключевые абстракции и механизмы архитектуры;

• сценарии, иллюстрирующие важнейшие аспекты предусмотренного поведения системы.

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

 

7.7. Инструменты

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

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

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

Виды инструментов

Мы выделяем по крайней мере семь различных видов инструментов для объектно-ориентированной разработки. Первый инструмент - система с графическим интерфейсом, поддерживающая объектно-ориентированную систему обозначений, представленную в главе 5. Такой инструмент может быть использован при анализе, чтобы зафиксировать семантику сценариев, на ранних стадиях разработки, чтобы передать стратегические и тактические решения, принятые при проектировании, а также для координирования действий проектировщиков. Подобный инструмент будет полезен на протяжении всего жизненного цикла и при сопровождении системы. В частности, мы считаем возможным восстанавливать по тексту программы многие важные аспекты объектно-ориентированной системы. Такая способность инструментальной системы очень важна: в традиционных CASE-средствах разработчик может создавать замечательные картинки только для того, чтобы обнаружить, что они устарели, потому что программисты манипулируют реализацией, не отраженной в проекте. Восстановление проекта по коду снижает вероятность того, что документация будет идти не в ногу с реализацией.

Следующий важный для объектно-ориентированной разработки инструмент - броузер, который показывает структуру классов и архитектуру модулей системы. Иерархия классов может сделаться настолько сложной, что трудно даже отыскать все абстракции, которые были введены при проектировании или выявить кандидатов для повторного использования []. При изучении фрагмента программы разработчику может понадобиться посмотреть определение класса некоторого объекта. Найдя этот класс, ему вероятно придется заглянуть в описание какого-нибудь из его суперклассов. Рассматривая этот суперкласс, разработчик может захотеть внести изменения в интерфейс класса, но при этом придется изучить поведение всех его клиентов. Этот анализ будет особенно громоздким, если он применяется к файлам, которые представляют физические, а не логические аспекты проекта. По этой причине броузер оказывается очень важным инструментом объектно-ориентированного анализа и проектирования. Броузеры есть, например, в среде Smalltalk. Подобные средства, хотя и разного качества, имеются и для других объектно-ориентированных языков.

Другой вид инструментов, который очень важен, если не абсолютно необходим - инкрементный компилятор. Метод эволюционной разработки, который применяется в объектно-ориентированном программировании, нуждается в компиляторе, который мог бы компилировать отдельные объявления и операторы. Мейровиц замечает, что "UNIX, с ее ориентацией на пакетное компилирование больших программных файлов в библиотеки, которые потом компонуются с другими фрагментами кода, не предоставляет необходимой поддержки для объектно-ориентированного программирования. Совершенно недопустимо тратить десять минут на компиляцию и компоновку, чтобы изменить реализацию метода и тратить целый час на компиляцию и компоновку только для того, чтобы добавить новое поле в суперкласс верхнего уровня! Для быстрой отладки методы и определения полей должны компилироваться инкрементно" []. Такие компиляторы существуют для многих языков, описанных в приложении. К сожалению, большинство реализации содержит традиционные пакетно-ориентированные компиляторы.

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

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

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

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

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

Организационные выводы

Потребность в мощных инструментах приводит к появлению двух специальных должностей в штате организации, занимающейся разработкой программных систем: инженер по повторному использованию и инженер-инструментальщик. Среди прочего, обязанности первого состоят в сопровождении библиотеки классов проекта. Без активных усилий она может стать необозримым пустырем превратившихся в хлам классов, в который ни один из разработчиков не захочет заглядывать. Часто бывает, что разработчиков надо побуждать к заимствованию существующих компонентов или предотвращать изобретение велосипеда; это тоже входит в задачи инженера по повторному использованию. В обязанности инженера-инструментальщика входят создание новых предметно-зависимых инструментов и переделка существующих инструментов для текущих нужд. Например, может потребоваться целая система тестов для проверки некоторых аспектов пользовательского интерфейса или более специализированный броузер классов. Инструментальщик облегчает свою работу тем, что разрабатывает и развивает инструменты, используя компоненты, уже помещенные в библиотеку классов. Их можно задействовать и в новых разработках.

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

 

7.8. Специальные вопросы

Узко-специфические проблемы

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

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

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

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

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

Переход на объектные технологии

Как считает Кемпф, "Изучение объектно-ориентированного программирования может оказаться гораздо более трудной задачей, чем просто освоение очередного языка. В данном случае надо научиться другому стилю программирования, а не просто запомнить синтаксис. То есть учить приходится не язык, а способ мышления" []. Как развить объектно-ориентированное мировоззрение? Мы рекомендуем:

• Обеспечить формальное обучение разработчиков и менеджеров элементам объектной модели.

• Начать с проектов с низким уровнем риска и позволить команде делать ошибки; по мере приобретения опыта можно направлять участников в другие команды в качестве наставников.

• Продемонстрировать разработчикам и менеджерам примеры хорошо структурированных объектно-ориентированных систем.

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

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

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

 

7.9. Выгоды и опасности объектно-ориентированной разработки

Выгоды

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

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

• использует выразительную мощь объектно-ориентированных языков программирования;

• стимулирует повторное использование программных компонент;

• приводит к созданию более гибких, легко изменяемых систем;

• сокращает риск разработки;

• лучше воспринимается человеческим сознанием.

Изучение многочисленных случаев из практики подкрепляет эти выводы; особенно часто указывается на то, что объектный подход может сократить время разработки и размер кода [, , ].

Опасности

Говоря о теневой стороне объектно-ориентированной технологии, нужно рассмотреть два вопроса: производительность и начальные затраты. По сравнению с процедурными языками, объектно-ориентированные языки определенно вносят дополнительные накладные расходы на пересылку сообщения от одного объекта другому. Как указывалось в главе 3, при вызове методов, которые не найдены и не связаны статически во время компиляции, выполняемая программа должна динамически искать нужный метод по классу объекта-получателя. Исследования показывают, что, в худшем случае, на вызов метода тратится в 1.75-2.5 раза больше времени чем на обычный вызов процедуры [, ]. С другой стороны, наблюдения показывают, что при вызове методов динамический поиск действительно необходим примерно в 20% случаев. В строго типизированных языках компилятор часто может определять, какие вызовы могут быть связаны статически и сгенерировать для них вызов процедуры вместо динамического поиска.

Другая причина снижения производительности кроется не столько в природе объектно-ориентированных языков, сколько в способе их использования в процессе объектно-ориентированной разработки. Как говорилось уже много раз, объектно-ориентированная технология порождает многослойные системы абстракций. Одно из следствий этого расслоения в том, что каждый метод оказывается очень маленьким, так как он строится на методах нижнего уровня. Другое следствие расслоения: иногда методы служат лишь для того, чтобы получить доступ к защищенным атрибутам объекта. В результате происходит слишком много вызовов. Вызов метода на высшем уровне абстракции обычно влечет каскад других вызовов; методы верхних уровней вызывают методы нижних уровней и т.д. Для приложений, в которых время - ограниченный ресурс, недопустимо слишком большое количество вызовов методов. Опять же, с позитивной стороны такое слоение способствует пониманию системы; к некоторым сложным системам невозможно даже подступиться, если не начать с проектирования слоев. Мы рекомендуем сначала проектировать систему с желаемыми функциональными свойствами, а потом определять узкие места. Часто их можно "расшить", объявив соответствующие методы как подстановки (выигрывая тем самым время), "подчистив" иерархию классов или нарушив инкапсуляцию атрибутов класса.

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

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

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

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

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

 

Выводы

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

• Многие приемы традиционного менеджмента программных разработок, например, просмотр, применимы и в объектно-ориентированной технологии.

• В стабильном состоянии объектно-ориентированные проекты требуют меньших ресурсов, а роли, необходимые для управления этими ресурсам, несколько отличаются от традиционных.

• В процессе объектно-ориентированной разработки нельзя производить интеграцию всего сразу и за один раз; структурными единицами управления для релизов должны быть категории классов и подсистемы, а не отдельные файлы и классы.

• Повторным использованием надо заниматься специально.

• График числа обнаруженных ошибок за определенное время и плотность ошибок - полезные количественные меры качества объектно-ориентированных программ. Существует ряд полезных количественных характеристик, ориентированных на классы.

• Документация никогда не должна ставиться во главу угла при разработке.

• Объектно-ориентированная разработка требует иного инструментария по сравнению с традиционными методами.

• Переход организации на объектно-ориентированные технологии - это смена мировоззрения, а не просто изучение нового языка программирования.

• Объектно-ориентированные технологии связаны как с выгодами, так и с опасностями, но опыт показывает, что выгод много больше.

 

Дополнительная литература

Ван Генучтен (Van Genuchten) [H 1991] и Джоунс (Jones) [H 1992] изучали понятие риска в программном обеспечении. Об образе мышления отдельного разработчика см. работу Вейнберга (Weinberg) [J 1971, H 1988]. Абдель-Хамид и Мэдник (Abdel-Hamid and Madnick) [H 1991] изучали динамику программистских групп.

Глиб (Gilb) [H 1988] и Чаретте (Charette) [H 1989] - это основные ссылки по практике менеджмента разработки программного обеспечения. Работа Арона (Aron) [H 1974] предлагает сравнительный взгляд на управление индивидуальным программистом и командой программистов. Что практически происходит, когда прагматика выталкивает теорию в окно, см. в работах Гласса (Glass) [G 1982], Ламмерса (Lammers) [H 1986] и Хэмфри (Hamphrey) [H 1989].ДeMapко и Листер(DeMarco and Lister) [H 1987], Иордан (Yourdon) [H 1989], Реттиг (Rettig) [H 1990] и Томсет (Thomsett) [H 1990] предложили ряд рекомендаций, интересных для менеджера разработки.

Детали управления просмотром см. в работах Вейнберга и Фридмана (Weinberg and Freedman) [H 1990] и Иордана (Yourdon) [H 1989a].

Шулмейер и МакМанус (Schulmeyer and McManus) [H 1992] - прекрасная традиционная ссылка по вопросу гарантирования качества программного обеспечения. Чидамбер и Keмеpep(ChidamberandKemerer)[H 1991] и Вош(Walsh) [H 1992, 1993] изучают обеспечение качества и количественные характеристики в контексте объектно-ориентированных систем.

Советы по поводу перехода на объектную модель (как индивидуальные, так и для целых организаций) можно найти в работах Голдберга (Goldberg) [G 1978], Голдберга и Кэя (Goldberg and Kay) [G 1977] и Кемпфа (Kempf) [G 1987].