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

         

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


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

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

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

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

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

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



Рис. 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. Непосредственными вдохновителями нашего обозначения были предложения Гамма, Хелпа, Джонсона и Влиссидеса [10]].


        Содержание раздела