Санкт-Петербургский государственный технический университет Физико-механический факультет Кафедра «Прикладная математика...
21 downloads
287 Views
998KB Size
Report
This content was uploaded by our users and we assume good faith they have the permission to share this book. If you own the copyright to this book and it is wrongfully on our website, we offer a simple DMCA procedure to remove your content from our site. Start by pressing the button below!
Report copyright / DMCA form
Санкт-Петербургский государственный технический университет Физико-механический факультет Кафедра «Прикладная математика» Диссертация допущена к защите Зав. кафедрой _________________Л.В.Петухов "___"_________________2002 г.
ДИССЕРТАЦИЯ на соискание ученой степени МАГИСТРА
Тема: Анализ, проектирование и реализация эффективной объектной модели UML
Направление:
510200 – Прикладная математика
Магистерская программа:
510209 – Математическое и программное обеспечение вычислительных машин
Выполнил студент гр.6057/2
А.С. Феоктистов
Руководитель, к.ф.-м.н., доц.
Ф.А. Новиков
Консультант по охране труда, к.т.н., доц.
В.В. Монашков
Санкт-Петербург 2002
Реферат Стр.113, илл.59, табл.10. Диссертация
посвящена
вопросам
разработки
объектной
модели
UML.
Рассматриваются логическая и физическая метамодели UML с целью создания программного компонента, который поддерживает все сущности и отношения, используемые в UML 1.4. Для построения модели используется трансформационный подход. Выполняется преобразование модели стандарта с целью придания ей нужных свойств. Трансформации осуществляются
с
помощью
систематического
применения
образцов.
Подробно
описываются использованные образцы проектирования. Разработанная модель UML эффективна в следующем смысле – она строго соответствует
стандарту,
не
содержит
неэффективных
или
плохо
реализуемых
программных конструкций (например, множественного наследования реализации) и практична в использовании. Модель оснащена загрузчиком из XMI, а также механизмом оповещения об изменении состояний элементов.
Ключевые слова: UML,
МОДЕЛЬ,
ПРОЕКТИРОВАНИЯ, ГРАНЬ.
МЕТАМОДЕЛЬ,
ЭЛИМИНАЦИЯ
КЛАСС,
ОБЪЕКТ,
МНОЖЕСТВЕННОГО
XMI,
ОБРАЗЦЫ
НАСЛЕДОВАНИЯ,
Abstract Pages 113, Figures 59, Tables 10. The thesis is focused on the design and the implementation of the UML object model. The logical and physical metamodels are examined for the purpose of construction the software component which supports all the entities and relationships used in the UML 1.4. The transformation method is used to design the model. The transformation of the standard semantics model is performed to add several needed features. Transformations are applied by the methodical usage of design patterns. The design patterns are described in detail. The developed object model is efficient in the sense that it strictly corresponds to the standard, it doesn’t contain non-efficient or badly implementing programming constructs (for example, multiple implementation inheritance) and it is practical in usage. The model has XMI loader and the event notification mechanism support.
Keywords: UML, MODEL, METAMODEL, CLASS, OBJECT, XMI, DESIGN PATTERNS, AVOIDING MULTIPLE INHERITANCE, FACET.
Содержание Введение ........................................................................................................................................................7 1
Обзор литературы и постановка задачи .....................................................................................11 1.1
Объектно-ориентированный анализ и проектирование ............................................................11
1.2
CASE средства ..............................................................................................................................11
1.3
Унифицированный язык моделирования....................................................................................12
1.4
Объектная модель UML ...............................................................................................................13
1.5
Современные CASE средства, основанные на UML .................................................................17
1.6
Постановка задачи ........................................................................................................................21
2
Методы трансформации метамодели ..........................................................................................23 2.1
Образцы проектирования.............................................................................................................24
2.2
Элиминация множественного наследования (Avoiding Multiple Inheritance)..........................25
2.3
Грань (Facet) ..................................................................................................................................28
2.4
Коммуникационный концентратор (Communication Hub) ........................................................31
2.5
Компоновщик (Composite) ...........................................................................................................35
2.6
Одиночка (Singleton).....................................................................................................................37
3
4
Проектирование и реализация программного продукта .........................................................40 3.1
Физическая метамодель UML 1.4................................................................................................40
3.2
Проекции модели на диаграммы классов ...................................................................................44
3.3
Устранение множественного наследования ...............................................................................45
3.4
Типы данных .................................................................................................................................53
3.5
Генерация кода..............................................................................................................................56
3.6
Рефакторинг ..................................................................................................................................59
3.7
XMI Загрузчик модели .................................................................................................................62
3.8
Механизм оповещения о событиях .............................................................................................66 Тестирование и применение программы ....................................................................................71
5
5
4.1
Тестирование.................................................................................................................................71
4.2
Применение ...................................................................................................................................73 Вопросы охраны труда и эргономики .........................................................................................76
Заключение.................................................................................................................................................86 Литература .................................................................................................................................................87 Приложение 1 – Метамодель UML.........................................................................................................90 Приложение 2 – Пример XMI................................................................................................................111 Приложение 3 – Тест механизма оповещения....................................................................................113
6
Введение О чем эта работа? Ответ на этот вопрос в какой-то степени дает название темы диссертации. Разберем его подробнее, опуская общепонятные слова, остановимся на «эффективной объектной модели UML». UML является аббревиатурой полного названия Unified Modeling Language. Правильный перевод этого названия на русский – унифицированный язык моделирования1. Главным словом в этом словосочетании является слово «язык». Язык – это знаковая система для хранения и передачи информации. Различаются языки формальные, правила употребления которых строго и явно определены и неформальные, употребление которых основано
на
сложившейся
практике.
Различаются
также
языки
естественные,
появляющиеся как бы сами собой2 в результате неперсонифицированных усилий массы людей и языки искусственные, являющиеся плодом видимых усилий определенных лиц. Например, язык, на котором написана эта диссертация (мы полагаем, что это русский язык) является неформальным и естественным. С другой стороны, подавляющее большинство языков программирования являются формальными и искусственными. Встречаются и другие комбинации: например, язык алгебраических формул мы считаем формальным и естественным, а эсперанто – неформальным искусственным. Так вот, UML можно охарактеризовать как формальный искусственный язык, хотя и не в такой степени, как многие распространенные языки программирования. Признаком искусственности служит наличие трех общепризнанных авторов: Г. Буч, Д. Рамбо, А. Джекобсон. В то же время в формирование языка внесли вклад многие теоретики и разработчики, имя которым легион. Языкотворческая практика применительно к UML непрерывно продолжается, что дает основание считать UML до некоторой степени естественным языком. Описание UML по большей части формальное, но содержит и явно неформальные составляющие. Такие особенности UML как точки вариации семантики и стандартные механизмы расширения, заметно отличают UML от языков, которые, по общему мнению, являются образцами формализма.
1
К сожалению, есть и неправильные: в русскоязычной литературе по UML уже успели появиться
такие варианты перевода слова “unified” как «единый» и «универсальный». 2
Точнее говоря, природа этого явления до конца не изучена.
7
Считается, что формальный искусственный язык описан должным образом, если это описание содержит, по меньшей мере, следующие части. 1. Синтаксис, то есть определение правил конструирования выражений языка. 2. Семантика, то есть определение правил приписывания смысла выражениям языка. 3. Прагматика, то есть определение правил использования выражений языка для достижения определенных целей. Как формальный искусственный язык UML имеет синтаксис, семантику и прагматику, хотя эти части названы в некоторых случаях иначе и описаны по-другому, нежели это принято в языках программирования. Описывая историю создания UML, его авторы характеризуют эпоху до UML как период «войны методов». Пожалуй, «война» – это слишком сильно сказано, но, действительно, UML является отнюдь не первым языком моделирования. К моменту его появления насчитывались десятки других, различающихся системой обозначений, степенью универсальности, способами применения и т. д. Авторы языков и теоретики программирования препирались между собой, выясняя чей подход лучше, а практические программисты всю эту «войну методов» равнодушно игнорировали, поскольку ни один из методов не дотягивал до уровня индустриального стандарта. Толчком к изменению ситуации послужили следующие обстоятельства. Во-первых, массовое
распространение
получил
объектно-ориентированный
подход
к
программированию (ООП), в результате чего возникла потребность в соответствующих средствах. Другими словами, появления чего-то, подобного UML с нетерпением ждали практики. Во-вторых, три крупнейших специалиста в этой области, авторы наиболее популярных методов, решились объединить усилия именно с целью унификации своих (и не только своих) разработок. Приложив заслуживающие уважения усилия, авторам UML, при поддержке и содействии всей международной программистской общественности удалось свести воедино (унифицировать) большую часть того, что было известно им и до них. В результате унификации получилась теоретически изящная и практически полезная вещь – UML. То есть UML – это результат унификации существующих языков моделирования, отсюда и появилось слово «унифицированный» в названии. В процессе проектирования что-то делается и в результате нечто получается. Если эта деятельность и форма результата регламентированы определенным образом, то им уместно дать название. Так сложилось, что результат проектирования (и анализа), 8
оформленный средствами определенного языка принято называть моделью.3 Деятельность по составлению моделей естественно назвать моделированием. Именно в этом смысле UML является языком моделирования. Важно отметить, что хотя UML – графический язык и для составления его довольно развитой нотации было потрачено много усилий, однако, рисование диаграмм, иллюстрирующих модель системы – это далеко не единственная цель UML. Более важная цель – это, как было сказано выше, моделирование системы, поэтому совершенно естественно то, что современные CASE средства основаны на UML. Также естественно, что такие средства, направленные на автоматизацию процесса разработки ПО, имеют внутреннее
представление
UML-модели,
проектируемой
системы,
отличное
от
обобщенного представления графов-схем. Модель системы – главный артефакт процесса моделирования, имеет определенное целостное представление, с которым можно производить дальнейшие действия, например, давать на вход различным инструментам, таким, например, как генераторы кода на языке реализации и анализаторы свойств модели. Но довольно о UML в целом, на эту тему написано и пишется много хороших книг, краткий обзор которых будет сделан в следующей части. Вернемся к теме этой работы, а в частности, к словосочетанию «модель UML». Как следует из вышесказанного, это должен быть результат моделирования унифицированного языка моделирования. Что же именно представляет собой модель UML? Модель UML – это конечное множество сущностей и отношений между ними. Сущности и отношения модели – это экземпляры мета классов метамодели4. Таким образом, рассматривая модель UML с наиболее общих позиций, можно сказать, что это граф (точнее, гипер-мульти-псевдо-орграф [1]), в котором вершины и ребра нагружены дополнительной информацией и могут иметь сложную внутреннюю структуру. Вершины этого графа называются сущностями, а ребра – отношениями. Различают логическую и физическую метамодели UML. Под логической моделью понимается описание графа, т.е. сущностей и отношений между ними. Это описание выполнено авторами языка в разделе семантики спецификации UML. А под физической – представление этого графа в памяти вычислительной машины. Итак, в данной работе мы рассматриваем логическую и физические метамодели UML с целью создания программного компонента, который поддерживает все сущности и 3
Что вполне согласуется с наиболее общим смыслом слова модель – абстрактное описание чего-
4
Метамодель – описание языка, на котором описывается модель.
либо.
9
отношения, используемые в UML 1.4. Наша модель UML эффективна в следующем смысле – она строго соответствует стандарту, не содержит неэффективных или плохо реализуемых программных конструкций (например, множественного наследования реализации) и практична в использовании. Помимо этого, реализовано взаимодействие с существующими приложениями через компонент, отвечающий за загрузку программного представления модели из текстового представления в формате XMI (см. раздел 3.7). Также учтены потребности реального CASE средства и реализован механизм оповещения об изменениях модели, который может быть использован, в частности, для синхронизации модели (внутреннего представления) с традиционным древовидным навигатором (внешним представлением, с которым работает пользователь).
10
1 Обзор литературы и постановка задачи 1.1 Объектно-ориентированный анализ и проектирование Как известно, разработка программного обеспечения является довольно сложной задачей. За время существования информационной индустрии выработано множество подходов, позволяющих упростить (а скорее противостоять экспоненциальному росту сложности в зависимости от объема задачи) процесс разработки. Языки третьего и четвертого
поколения,
высокоуровневые
программные
спецификации,
объектная
ориентированность, методологии, языки моделирования, а также различные программные средства, упрощающие разработку. Большое количество усилий было затрачено на создание языков моделирования, которые формально определяли бы семантику моделей OA&D5. И эта деятельность очень важна, так как такой язык может быть использован для описания разрабатываемых систем. Языки моделирования точно определяют семантику таких концепций как объект и класс. В то же время, нам представляется, что средства разработки программного обеспечения еще не реализовали своих настоящих потенциальных возможностей. Одной из главных преград на пути к этому является то, что в то время как многие из них реализуют нотацию поддерживаемого языка моделирования, они «забывают» о полной поддержке семантики языка.
1.2 CASE средства Назначение CASE средства – облегчение и автоматизация задач по разработке и поддержке программного обеспечения. Таким образом, любой инструмент, который сокращает трудозатраты при разработке или поддержке ПО, может быть квалифицирован как CASE средство. Следовательно, текстовый редактор, может рассматриваться как CASE средство, так как он может быть использован для создания и редактирования документов,
которые
специализированные
описывают средства
спецификацию
используются
для
и
дизайн
продукта.
удовлетворения
разработчиков на различных фазах жизненного цикла программы.
5
Аббревиатура Object Analysis and Design (объектный анализ и проектирование).
11
Более
потребностей
Нас будут интересовать в первую очередь CASE средства, которые поддерживают моделирование программного продукта. Эти средства позволяют выразить модель с помощью
определенного
языка
моделирования.
Инструменты,
как
правило,
предоставляют возможность рисования, что позволяет проектировщику запечатлеть конструкцию системы в графическом виде, используя нотацию языка. Такие средства могут использоваться как для быстрого прототипирования проектных решений, так и для документирования законченных решений. Это позволяет проектировщику обмениваться своими идеями с другими разработчиками, а также недвусмысленно
выражать
свои
мысли.
Создание
более
совершенных
языков
моделирования и средств, их поддерживающих, вооружает проектировщиков для решения более сложных задач.
1.3 Унифицированный язык моделирования В 70-е и 80-е годы были споры и несогласия среди людей, которые «верили» в моделирование данных и тех, кто «верили» в моделирование функциональности. В то время идеи использования диаграмм потоков управления и диаграмм «сущность - связь» представлялись взаимоисключающими. В конце 80-х между двумя лагерями произошло перемирие. Было достигнуто понимание того, что многие проекты выиграют, если будут использовать одновременно функциональные модели и модели данных. После этого произошло зарождение нескольких объектно-ориентированных методов анализа и проектирования. К сожалению, каждый метод имел свою собственную нотацию и каждый определял по-своему такие понятия, как объект, тип, класс. За период с 1989 по 1994 число различных языков моделирования возросло от менее 10 до более 50. Большинство методов определяло две вещи: язык моделирования и процесс разработки. Язык моделирования, в свою очередь, в основном состоял из графической нотации. Нотация использовалась для описания дизайна, а процесс определял какие шаги необходимо предпринимать во время разработки. Авторами наиболее популярных методов были Гради Буч, Джим Рамбо и Айвар Джекобсон. Каждый из них написал книгу по своей собственной методологии Booch [2], OMT[3], OOSE [4]. В конце 1994-го Буч и Рамбо анонсировали начало консолидированных работ над “unified method 6 ”. Позднее к ним присоединился и
6
Унифицированный метод
12
Джекобсон. В результате нескольких лет экспериментов с различными нотациями и концепциями, они остановились на нескольких конкретных и выбрали из них лучшее. В 1996 году проект “Unified method” перешел в «Унифицированный язык моделирования» (UML). Новое название подчеркивало, что это более не метод. Разработчики языка сконцентрировались на разработке выразительной унифицированной нотации и точном определении семантики используемых языковых концепций, оставляя вопрос выбора метода (процесса разработки) открытым. Конечно, создавая язык, авторы имели в виду определенный процесс и даже написали об этом книгу [5], но было принято важное решение отделения метода от языка. UML вырос на фундаменте методов Booch, OMT и OOSE, впитав лучшее из их языков и, безжалостно отсекая ненужное, были учтены сильные стороны и других методологий. UML был хорошо воспринят индустрией программного обеспечения и достаточно быстро стал стандартом де-факто для языка моделирования. В настоящее время развитием UML руководит Object Modeling Group (OMG), текущая версия стандарта – UML 1.4 [10].
1.4 Объектная модель UML Язык UML определяет: •
Элементы, которые составляют модель семантики (семантика).
•
Нотацию для визуального представления элементов модели (синтаксис).
•
Правила использования (прагматика).
В UML определены механизмы расширения и специализации центральных концепций языка. Язык UML не определяет и не диктует: •
Язык программирования – UML имеет семантическую модель, которая хорошо отображается на семейство объектно-ориентированных языков, но он не зависит ни от одного конкретного языка реализации.
•
Инструменты – UML не выдвигает требований к CASE средствам, основанным на UML, тем более не определяет их использование. Тем не менее, естественно ожидать, что инструмент, поддерживающий UML, должен близко следовать семантике языка.
•
Процесс разработки – как отмечалось ранее, определение процесса разработки не было целью авторов языка, UML умышленно был сделан независимым от процесса. 13
Все основные концепции моделирования в UML определены в спецификации UML как элементы модели (ModelElement), начиная от привычных и вполне конкретных языковых конструкций, таких как класс, заканчивая такими элементами модели как, варианты использования (Use Case). Рассмотрим простой пример, предположим, мы хотим описать класс Human (человек) (Рис. 1.1). Класс Human наследует абстрактный класс Mammal (млекопитающее), у человека есть 2 ноги (композиция с классом Leg). Как же такая модель запишется в терминах модели UML? Каждый класс будет представлен отдельным элементом, отношение обобщения и ассоциация также являются элементами, даже полюсы ассоциации (AssociationEnd) между Human и Leg являются элементами модели. Все эти элементы являются частью семантической модели UML. Mammal
1 Human
2 Leg
Рис. 1.1. Пример модели
На Рис. 1.2 приведена диаграмма объектов, которая иллюстрирует представление нашего примера в терминах семантической модели UML. Заметим, что мы намерено немного упростили отображение – кратности полюсов ассоциации могли задаваться в виде диапазонов, поэтому в общем случае свойство multiplicity содержит коллекцию объектов типа MultiplicityRange, и, конечно, для обозначения типа агрегирования имеется специальный тип данных, а не просто строка (“composite”), но так пример получился более понятным и наглядным.
14
: Class
+parent
name = “Mammal” isAbstract = true
+specialization : Generalization +generalization
+child : Class
: Class
name = “Human” isAbstract = false
: Association +association
+association
name = “Leg” isAbstract = false +participant
+participant +connection : AssociationEnd aggregation = “composite” multiplicity = 1
+connection : AssociationEnd aggregation = “none” multiplicity = 2
Рис. 1.2. Отображение на экземпляры классов метамодели
На Рис. 1.3 приведена диаграмма классов, иллюстрирующая часть семантической модели UML, собственно мы изобразили здесь только те элементы метамодели и отношения между ними, которые участвуют в нашем примере. Полная модель семантики UML (логическая метамодель) описана в разделе «Семантика» спецификации UML [10]. Семантика описана с уровнем детализации, необходимым разработчикам инструментам, поддерживающим UML, для предоставления пользователю инструмента возможности в полной мере оперировать объектно-ориентированными концепциями для построения модели, так как он этого вправе ожидать от инструмента, заявляющего о поддержке UML. Итак, любая модель UML выражается в терминах объектов мета классов, описанных в разделе семантики спецификации UML.
15
Element
ModelElement name : Name
Relationship
+parent GeneralizableElement 1..1 isAbstract : Boolean +child
Namespace
+specialization
1..1
Classifier
1..1
+association
+participant
*
Generalization
* +generalization *
AssociationEnd 1..1 +connection Association aggregation : AggregationKind 2..* +association multiplicity : Multiplicity {ordered}
Рис. 1.3. Отношения между мета классами
Ранее мы уже неоднократно использовали ученую приставку “мета-“, настало время пояснить ее употребление в контексте метамоделирования. Архитектура UML базируется на четырехуровневой структуре, которая состоит из следующих уровней: объекты пользователя, модель, метамодель и мета-метамодель. Этот подход к метамоделированию является достаточно общим и часто используемым. Функциональность каждого уровня с примерами его типичных представителей, приведена в Табл. 1. Уровень
Описание
Пример
мета-
Инфраструктура для архитектуры мета-
MetaClass, MetaAttribute,
метамодель
модели. Определяет язык для описания
MetaOperation
метамоделей. метамодель
Экземпляр мета-метамодели. Определяет Class, Attribute, Operation, язык для описания модели.
модель объекты
Component
Экземпляр метамодели. Определяет язык CompactDisc, Author, getPrice, для описания предметной области.
CompactDiscElectronicStore
Экземпляр модели. Определяет специ-
, “John Smith”,
пользователя фичные данные предметной области. Табл. 1. Уровни метамоделирования
16
777.55
Таким образом, основная идея описания UML вполне традиционна и согласуется с общепринятой практикой: мета-метамодель – это описание используемого формализма; метамодель – это и есть собственно описание языка (элементов моделирования); там, где формализм не срабатывает, на помощь приходит естественный язык и все это сопровождается примерами фрагментов моделей. Аналог понятия «модель» для традиционного языка программирования – программа на этом языке, ну а объекты пользователя – это, те объекты, которые появляются в процессе выполнения программы. Действительно,
рассмотрим
описание
какого-либо
из
обычных
языков
программирования. Чтобы никого не обидеть, пусть этот язык называется X. Если описание хорошее, то в самом начале указывается язык (иногда его так и называют – метаязык), который используется для описания языка X. Например, приводится фраза такого типа: «синтаксис языка X описан с помощью контекстно-свободной грамматики, правила которой записаны в форме Бэкуса-Наура (БНФ), контекстные условия и семантика описаны на естественном языке». Если описание не очень хорошее, то такая фраза может и отсутствовать, но она все равно неявно подразумевается и от читателя требуется понять используемый метаязык по контексту. Далее с помощью метаязыка более или менее формально описываются конструкции языка X; все, что не удается описать формально, описывается на естественном языке (при этом зачастую вводится большое
количество
«новых»
терминов
и
используются
трудные
для
чтения
канцеляризмы). Все это, как правило, сопровождается многочисленными конкретными примерами фрагментов текстов на языке X. Чтобы читатель не путался, где текст на языке X, а где текст на метаязыке, или где общее описание, а где конкретный пример, применяются различные полиграфические приемы: изменение гарнитуры и начертания шрифта, отступы, подчеркивание и т.д. В результате получается неплохо: многоязыковая смесь становится вполне удобочитаемой (при минимальном навыке).
1.5 Современные CASE средства, основанные на UML Любой продукт, поддерживающий моделирование на UML, имеет то или иное внутреннее представление модели. Наиболее типичный класс продуктов – CASE средства, такие как Rational Rose, Together Control Center, MS Visio Enterprise, ArgoUML/Poseidon. В этом разделе мы приведем краткий обзор современных CASE средств, основанных на UML.
17
Во времена ранних спецификаций UML7 (период с 1995 по 1997 год) CASE средств, поддерживающих UML, было мало. Но постепенно UML становился более зрелым 8 , и поддержка языка стала появляться в существующих инструментах. Как правило, такая поддержка добавлялась довольно прямолинейно, просто как еще один вид отображения уже существующих элементов. Это утверждение справедливо и для Rational Rose – продукта Rational Software. Компания Rational была одной из главных движущих сил разработки UML, а Rational Rose, соответственно – флагманский CASE инструмент компании. Rational Rose поддерживал нотацию OMT и Booch, и его разработчики просто заменили
старые
символы
подходящими
новыми
[12]
(сохранив
возможность
отображения в старой форме). Позднее появилась поддержка для большинства ключевых элементов. Тем не менее, не была обеспечена поддержка моделирования семантики, определяемой метамоделью UML. Большинство других средств пошло по этому же эволюционному пути. Даже разработчики средств, которые создавались уже специально для поддержки UML, не поддерживают семантику языка на должном уровне. И что же мы имеем в результате? Не очень радостную картину – большинство разработчиков существующих инструментов моделирования, основанных на UML, сконцентрировало свои усилия на создании достаточно выразительных средств поддержки рисования диаграмм, представленных в разделе, описывающем нотацию UML, забывая при этом точно следовать семантике UML. Rolf W. Rasmussen в своей работе [12] охарактеризовал такой подход, «как попытку готовить, глядя на картинки из поваренной книги, вместо того, чтобы точно следовать рецептам». Важно понимать, что диаграммы UML – это всего лишь проекции модели, проектируемой системы, и вообще говоря, между диаграммами и моделью нет взаимно однозначного соответствия. Конечно, диаграммы тоже важны, но уж точно не менее важна модель в целом, которая и является главным артефактом процесса моделирования. Отметим, что с моделью можно делать много полезного (генерация кода, проверка модели на удовлетворение определенным свойствам, например, непротиворечивость и т.п.), а на диаграммы, в основном, можно только смотреть. Далее, начиная с версии UML 1.3 [9], стандарт определяет формат текстового представления модели в XML Metadata Interchange (XMI) [10,11]. Большинство современных средств моделирования, основанных на UML, поддерживают сериализацию
7
Авторы UML называют эту фазу развития «UML 0.8-0.91» – что соответствует номерам версий.
8
В ноябре 1997 года вышла версия UML 1.1, уже под логотипом OMG.
18
в XMI, но, к сожалению, они делают это по-разному – сохранение делается в разные версии XMI, и при этом, не всегда корректно. Разработчики Together, зная, что им придется иметь дело с огромным количеством вариаций XMI, реализовали в своем инструменте импорт и экспорт в 7 различных форматов. Благодаря хорошему уровню интеграции между Rose и Together, мы смогли использовать совместно сильные стороны обоих этих продуктов. В Табл. 2 приведены наши субъективные оценки характеристик лидирующих UML инструментов. Разберем подробнее значения в ячейках. В основном инструменты поддерживают или заявляют о поддержке UML версии 1.3 (исключение – Microsoft Visio). Поддержка XMI: в таблице мы просто указали есть она или нет, следует отметить, что в Rational Rose эта поддержка достигается благодаря специальному расширению, разработанному компанией UniSys. Для ArgoUML, напротив, XMI является базовым и единственным форматом, сохранение модели всегда осуществляется только в XMI, но, к сожалению, используется устаревшая версия – 1.0, что серьезно ограничивает возможности взаимодействия этого инструмента с другими. В Together поддержка XMI выглядит наиболее убедительно, это единственный инструмент, в котором уже реализовано сохранение в XMI 1.1 для UML 1.4. Поддержка семантики: наиболее полным и точным образом, на наш взгляд, семантика UML реализована в ArgoUML. Причина этого в том, что модель, используемая в ArgoUML, получена непосредственной генерацией по модели стандарта9, к сожалению, не все элементы модели доступны из имеющегося пользовательского интерфейса. В Rational Rose модель основана на старой, поддерживающей методы Booch и OMT, что создает иногда заметные проблемы. MS Visio – вообще говоря, это приложение является редактором деловой графики, видимо поэтому UML-моделирование не самая его сильная сторона, справедливости ради, надо сказать, что разработчики UML-трафарета Visio, по крайней мере в одном аспекте, оказались впереди других – в Visio Enterprise реализована проверка модели на соответствие правилам непротиворечивости [10]. Выразительные возможности. Рисование диаграмм выполнено по-разному, в каждом инструменте есть свои сильные стороны. Например, в Rational Rose, пожалуй, наилучшим образом реализована поддержка диаграмм классов (хотя этот элемент, неплохо выполнен в большинстве инструментов). Visio, как универсальный графический редактор, позволяет нарисовать диаграмму практически любой сложности (особенно если
9
В ArgoUML используется компонент, разработанный компанией Novosoft.
19
пользоваться нестандартным трафаретом
10
). Отметим, что без этого редактора
невозможно было бы подготовить подавляющие количество диаграмм, включенных в диссертацию. Together – рисует весьма красиво, интерфейс пользователя имеет ряд интересных находок, но нам было удобнее некоторые вещи (например, рисование больших диаграмм классов) делать в Rose. В ArgoUML, несомненно, есть ряд прорывов: довольно полная модель, система критиков, очень оригинальный интерфейс редактора диаграмм. Но в настоящее время этот проект имеет версию с номером меньше единицы, хотя на его основе уже есть коммерческая разработка – Poseidon, главный его недостаток, на наш взгляд – это плохая интеграция с другими средствами. Инструмент Rational Rose
Версия
Поддержка
Семантика
UML
XMI
UML
Визуализация
1.3
Да
4
4
Together CC
1.3
Да
4
5
MS Visio Enterprise
1.2
Нет
3
4
ArgoUML
1.3
Да
5-
5
Visual UML
1.3
Нет
3
4
Enterprise
Табл. 2.Сравнение характеристик современных CASE средств
Таким образом, общая картина такова: на рынке существует большое количество средств моделирования, основанных на UML. Эти инструменты различаются уровнем поддержки
UML,
степенью
интеграции
с
другими
инструментами,
а
также
позиционированием на рынке. Например, Together предлагает комплексное решение, позволяющее вести всю разработку в одном инструменте, начиная от формулировки требований и моделирования, заканчивая развертыванием и документированием. Rose подразумевает совместное использование с другими продуктами этой компании (например, Rational SODA – для генерации документации). Visio, ArgoUML и Visual UML – являются более легковесными решениями, предназначенными в основном именно для моделирования. Однако общее впечатление от уровня поддержки языка оставляет желать лучшего.
10
Мы используем трафарет, разработанный Pavel Hruby из компании Navision.
20
1.6 Постановка задачи Мы, конечно, не ставим перед собой целью за короткий срок восполнить все имеющиеся пробелы, а значит разработать с нуля лучший на рынке инструмент моделирования UML. Из сказанного в предыдущих разделах следует, что основой хорошего CASE средства, поддерживающего UML, да и вообще практически любого инструмента, имеющего дело с UML, является точная и полная поддержка семантики модели UML. В то же время, эта часть является довольно слабым местом существующих на рынке CASE средств. Итак, основной целью данной работы является проектирование и программная реализация объектной модели UML, обладающей следующими свойствами: •
Точно и в полном объеме соответствует семантике языка UML современной версии 1.4.
•
Не привязана к конкретному CASE средству и допускает совместное использование с любым средством, поддерживающим стандарт.
•
Проста
в
использовании
и
имеет
программный
интерфейс
для
манипулирования моделью. •
Имеет механизм оповещения внешних программ об изменении состояний элементов.
•
Имеет загрузчик модели в формате XMI, а поэтому обеспечивает интеграцию с существующими CASE средствами.
Следует отметить, что наша модель будет поставляться в виде набора классов, т.е. это будет не законченное приложение, а библиотека, каркас для построения приложения, нуждающегося в поддержке модели UML. Объектная модель UML может быть использована в различных программных продуктах, имеющих дело с UML: как в отдельных утилитах (конвертеры из других языков проектирования, например, из языка SDL в язык UML; средства анализа модели, например, на непротиворечивость) так и в составе самостоятельного CASE-средства. Итак, в данной работе мы сосредоточимся на реализации объектной модели UML, которая может быть использована для построения средств разработки ПО, которые «понимают» OA&D модель, определяемую UML. Рассматриваются вопросы, которые необходимо решить при переводе описания семантики модели (логической метамодели) в практическую объектную модель (физическую метамодель). Также рассматриваются требования практического средства разработки к такой модели (такие как поддержка уведомления об изменении элементов модели и импорт моделей в форматах 21
существующих средств моделирования) и что было сделано, чтобы эти требования удовлетворить.
22
2 Методы трансформации метамодели Исходные данные для построения искомой объектной модели фиксированы – это логическая метамодель UML, описанная в стандарте. По определению данная модель полна и точна. В принципе возможны два подхода: 1. построить независимым образом свою объектную модель и как-то доказать, что она соответствует стандарту и в то же время обладает требуемыми свойствами; 2. трансформировать модель стандарта в другую, так чтобы на каждом шаге сохранялось соответствие и добавлялись требуемые свойства. Первый подход представляется нам очень сложным, он фактически предполагает отбрасывание того, что уже сделано авторами языка (ведь семантика описана в терминах классов и отношений между ними). А главное, нам кажется весьма неопределенным вопрос о доказательстве соответствия двух взятых моделей. Следует отметить, однако, что этот подход довольно часто применялся на практике разработчиками инструментов, только без доказательства, ведь модели подавляющего большинства современных инструментов не полны, поэтому и доказывать нечего. В своей работе [12] Rasmussen рассказывает об успешном применении второго подхода.
Он
взял
за
основу
логическую
метамодель
и
путем
определенных
трансформаций перевел ее в физическую, реализуемую на конкретном языке. Khalsa и Simpson [13] тоже выбрали второй подход, но, кроме этого, они произвели ряд трансформаций, направленных на упрощение модели, путем сокращения числа классов (элиминации «лишних» классов, которые задают только новый тип, но не новое поведение), аргументируя это тем, что разработчики предпочтут пользоваться моделью, которая состоит из меньшего числа классов. Мы считаем, что, сокращая количество классов в модели, мы рискуем «сбиться» с выбранного
пути.
Во-первых,
крайне
сложно
доказывать
правомерность
таких
преобразований. Во-вторых, нам представляется, что разработчики наоборот предпочтут работать с моделью, как можно более точно соответствующей той, которая хорошо документирована в спецификации UML, чем с той, в которой на 20% меньше классов. Мартин Фаулер в своей работе, посвященной «ясному» дизайну [15], отмечает, что дизайн системы становится понятнее, если используются явные подклассы, вместо одного 23
универсального класса. В нашем же подходе между новыми и старыми классами есть однозначное (но не взаимно однозначное) соответствие. Итак, выбран второй подход, называемый трансформационный. Преобразование выполняется с помощью систематического применения образцов. Если для каждого применяемого образца мы видим, что он сохраняет семантику и добавляет нужное нам свойство, то, тем самым, можно быть уверенным что в результате получена полная и точная модель с требуемыми свойствами. Особенности реализации данного подхода рассматриваются в следующей главе. А в этой главе мы расскажем об образцах (patterns) проектирования, которые использовались.
2.1 Образцы проектирования Образцы проектирования 11 – это сравнительно новый способ обмена опытом, удачными проектными решениями. С момента выхода в свет книги «банды четырех»
12
[14] образцы стали одной из наиболее горячих тем, обсуждаемых
программисткой общественностью. Эта книга была написана на основе докторской диссертации Эриха Гамма. По словам архитектора Кристофера Александра, «любой образец описывает задачу, которая снова и снова возникает в нашей работе, а также принцип ее решения, причем таким образом, что это решение можно потом использовать миллион раз, ничего не изобретая заново» [14]. Хотя Александр имел в виду образцы, возникающие при проектировании зданий и городов, но его слова верны и в отношении образцов объектноориентированного проектирования. Наши решения выражаются в терминах объектов и интерфейсов, а не стен и дверей, но в обоих случаях смысл образца – предложить решение определенной задачи в конкретном контексте. В общем случае образец состоит из четырех основных элементов: 1. Имя. Ссылаясь на имя, мы можем кратко описать проблему проектирования, ее решения и их последствия. Это позволяет проектировать на более высоком уровне абстракции. Словарь образцов позволяет вести обсуждение с коллегами, использовать образцы при документировании системы. Подбор хорошего имени очень важная задача при составлении каталога.
11
Design Pattern – в русский язык уже довольно прочно вошла транслитерация с английского –
«паттерн» (pattern), но мы все же будем придерживаться русского эквивалента – «образец». 12
GoF – Gang of Four (Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides).
24
2. Задача. Описание контекста применения образца. Может описываться конкретная проблема проектирования, например, способ представления алгоритмов в виде объектов. Также уместен перечень условий, при выполнении которых имеет смысл применить данный образец. 3. Решение. Описание элементов дизайна, отношений между ними, функции каждого элемента. Дается абстрактное описание задачи проектирования и ее обобщенное решение. 4. Результаты. Здесь описываются следствия применения образца и разного рода компромиссы. Влияние на степень гибкости, расширяемости и переносимости системы. Образцы проектирования – это не то же самое, что структуры данных, такие как списки или деревья, которые можно реализовать в виде класса и повторно использовать без каких-либо модификаций. Но это и не строго предметно-ориентированные решения для конкретного приложения или подсистемы. Образец идентифицирует и абстрагирует ключевые аспекты решения типовой задачи, которые и позволяют применить его для создания повторно используемого архитектурного решения. В следующих разделах мы расскажем об образцах, которые нашли применение в нашей системе. Отметим, что большинство используемых образцов не опубликовано широко, фактически нам удалось найти лишь один источник, где они были документированы как образцы – диссертация Rasmussen [12]. Нам кажется, что эти образцы заслуживают гораздо более подробного внимания. Поэтому далее мы приводим их подробное описание.
2.2 Элиминация множественного наследования (Avoiding Multiple Inheritance) Задача У вас имеется промежуточный архитектурный проект системы, и вы заняты его совершенствованием, например, меняете его с учетом языка реализации. Промежуточный дизайн использует множественное наследование реализации, но конечный дизайн не должен этого делать по ряду причин (будь то трудности, возникающие при множественном наследовании или просто потому, что используемый язык не поддерживает множественного наследования).
25
На Рис. 2.1 изображена диаграмма классов с множественным наследованием. Заметим, что здесь имеет место так называемое наследование через общих предков [27]. Действительно, класс ClassF наследует реализацию от ClassC и ClassD, которые, в свою очередь, унаследованы от ClassA, в результате проблема – ClassF унаследует атрибут body от ClassC и ClassD, что, вероятно, не входило в планы проектировщика. Следует отметить, что в редких случаях все же желательно создавать две копии наследуемых полей данных [27]. В языке C++ для управления этим используется виртуальное наследование (ключевое слово virtual). ClassA
ClassB
body:String
ClassC
ClassD
ClassF
ClassE
ClassG
Рис. 2.1. Промежуточное проектное решение
Пожелания к конечному дизайну: •
Мы хотим сохранить семантику типов, там, где это возможно.
•
Несмотря на то, что мы не можем унаследовать реализацию от нескольких классов, мы не хотим реализовывать одинаковые части кода в разных классах.
Решение Сначала заменяем все абстрактные классы на «чисто абстрактные», т.е. интерфейсы. Затем, создаем для них парные классы, реализующие функциональность, которая была сосредоточена в этих абстрактных классах. Соответственно, все классы, которые реализуют интерфейс, делегируют его реализацию этим парным классам. На Рис. 2.2. представлен вариант исправленного дизайна без множественного наследования реализации. Произведем анализ изменений. Во-первых, абстрактные классы ClassB и ClassD мы сделали интерфейсами, также добавились парные им реализации – ClassBImpl и ClassDImpl соответственно. Во-вторых, отношения наследования 26
заменились на отношения реализации между классами и интерфейсами. В-третьих, исчезло обобщение между ClassA и ClassD, т.к. ClassD теперь интерфейс, в связи с этим появилось обобщение от ClassG к ClassA. Это сделано для сохранения семантики типов, ClassF тоже реализует ClassD, но он наследует ClassC, поэтому автоматически является подклассом ClassA. «interface» ClassB
ClassA body:String
«interface» ClassD
ClassC
ClassF
ClassE
ClassG
ClassDImpl
ClassBImpl
Рис. 2.2. Окончательный дизайн
Отметим, что семантика типов, по большей части сохранена, так, например, ClassG по-прежнему является подклассом классов ClassA, ClassB и ClassD. Однако он более не наследует реализацию от ClassB и ClassD, а вместо этого делегирует ее ClassDImpl.
Результаты В
случае
использования
языка
программирования,
не
поддерживающего
множественного наследования классов, но допускающего множественное наследование интерфейсов (например, Java), применение образца Элиминация множественного наследования делает возможным реализацию нашего дизайна на данном языке. Если же используется язык, в котором имеется поддержка множественного наследования классов
27
(например, C++), то применение этого образца позволяет достичь более понятного дизайна (устраняются связанные с множественным наследованием реализации проблемы). Следует отметить, однако, что представленная процедура отнюдь не универсальная для получения наилучшего возможного дизайна, поэтому в общем случае, элиминация множественного наследования может потребовать больших усилий. Далее будет приведен конкретный пример из нашей системы (см. раздел 3.3), где мы не располагали достаточным количеством абстрактных классов, чтобы напрямую применить данную процедуру.
Родственные образцы Когда высота иерархии наследования довольно большая (например, в метамодели UML высота иерархии – 6), а интерфейсы суперклассов хотелось бы отделить от интерфейсов
подклассов,
имеет
смысл
совместно
с
образцом
Элиминация
множественного наследования использовать образец Грань. Это активно применяется при элиминации множественного наследования реализации в нашей модели. Подробнее пример
их
совместного
использования
будет
рассмотрен
в
следующей
главе
(см. раздел 3.3).
2.3 Грань (Facet) Контекст Вы создаете класс, представляющий абстракцию, у которой есть много разных свойств, которые могут быть разбиты на логические группы. Также может использоваться в том случае, если главный класс делегирует обязанности по реализации своих методов нескольким другим.
Задача Избежать создания «большого» класса с интерфейсом, у которого огромное количество никак логически неупорядоченных методов. В то же время: •
Мы не можем просто разбить существующую абстракцию на несколько меньших.
•
Мы по-прежнему хотим, чтобы экземпляр класса представлял всю абстракцию целиком 28
•
Может быть так, что реализацию сложно разбить на несколько классов: в то время как методы довольно четко разделяются на отдельные группы, их реализация требует возможности обращаться к состоянию целой абстракции.
Решение На Рис. 2.3 представлена трансформация класса Main, у которого есть две ярко выраженные группы методов (groupA и groupB). Мы создаем два новых класса-грани AFacet и BFacet, в которые помещаем атрибуты и методы группы «A» и группы «B», соответственно, а в главном классе Main заводим методы доступа к этим граням.
Main
AFacet
«groupA» attributeA1 attributeA2 «groupB» attributeB1 attrbuteB2
attributeA1 attributeA2
«becomes»
«groupA» methodA1() methodA2()
methodA1() methodA2()
Main getAFacet():AFacet getBFacet():BFacet
BFacet attributeB1 attributeB2
«groupB» methodB1() methodB2()
methodB1() methodB2()
Рис. 2.3. Образец проектирования Грань
Результаты Применение образца Грань позволяет структурировать сложные интерфейсы «больших» классов, сохраняя при этом целостность абстракции, которую представляет класс. Применения этого образца в дизайне, где главный класс делегирует обязанности по реализации своих методов одному или нескольким специальным классам (такая картина часто получается в результате применения образца Элиминация множественного наследования на достаточно сложной системе классов), позволяет повысить уровень повторного использования. Поскольку применение Грани упрощает интерфейс главного класса – несколько методов заменяется одним, возвращающим нужную Грань, то это избавляет разработчика от обязанности повторять реализацию методов (пусть даже через простое делегирование) в главном классе.
29
Реализация Реализация конкретной грани тесно связана с главным классом, то есть класс-грань может нуждаться в ссылке на главный класс. Эту ссылку резонно передать параметром в конструктор класса-грани. Если целевой язык поддерживает внутренние классы (как, например, Java), то реализация класса-грани как внутреннего может быть хорошим решением, т.к. внутренние классы имеют доступ к состоянию внешнего. Приведем пример реализации на Java класса Main (Рис. 2.3) и его граней: public class Main { private String theMainClassState = "fine"; private AFacet aFacet = new Main.AFacet(); private BFacet bFacet = new Main.BFacet(); public AFacet getAFacet() { return aFacet; } public BFacet getBFacet() { return bFacet; } public class AFacet { private int attributeA1 = 5; private int attributeA2 = 10; public int methodA1() { return attributeA1; } public int methodA2() { return attributeA2; } } public class BFacet { private String attributeB1 = "attributeB1_Value"; private String attributeB2 = "attributeB2_Value"; public String methodB1() { return attributeB1 + theMainClassState; } public String methodB2() { return attributeB2 + theMainClassState; } } }
30
Родственные образцы Элиминация
множественного
наследования
–
позволяет
избавиться
от
множественного наследования реализации в промежуточном дизайне. Часто применение этого образца приводит в контекст образца Грань.
2.4 Коммуникационный концентратор (Communication Hub) Задача На Рис. 2.4 изображены две подсистемы (subject – наблюдаемый и observer – наблюдатель) одна из которых наблюдает (образец Observer – Наблюдатель [14]) за состоянием другой подсистемы. Обе подсистемы состоят из большого количества объектных структур, таких как Компоновщик [14]. Объекты в наблюдающей системе должны оповещаться об изменениях в состояниях объектов наблюдаемой системы.
«» «»
Observer
subject
observer
Composite «mirrors»
«» «»
«» «»
Composite
Рис. 2.4. Контекст образца Коммуникационный концентратор
Требования к системе оповещения: •
Подсистемы должны быть слабо связаны13.
•
Механизм
оповещения
между
системами
объектов
должен
уметь
динамически адаптироваться к структурным изменениям в наблюдаемой подсистеме. •
Число объектов в обеих подсистемах может быть настолько велико, что наивное решение (Рис. 2.5) – регистрация по наблюдателю на каждую пару объектов не подойдет.
13
Устоявшийся английский термин – “loosely coupled”.
31
•
Система оповещения должна позволять системе-наблюдателю выполнять зеркальное отображение наблюдаемой системы.
subject
observer Observer
a
«» «»
A
subject
observer Observer
b
«» «»
B
subject
observer
Observer
d
«» «»
D
subject
observer Observer
c
«» «»
C
Рис. 2.5. «Наивное» решение – каждой паре по Наблюдателю
Применение образца Наблюдатель к каждой паре объектов «наблюдательнаблюдаемый» может быть довольно неудачным решением, так как необходимо учесть динамическую структуру наблюдаемой системы (при добавлении/удалении объектов из Компоновщика необходимо заботится о регистрации/удалении Наблюдателей). Кроме этого, система может состоять из большого количества объектов и регистрация по наблюдателю для каждого составляющего объекта превращается в далеко не самую приятную задачу.
Решение Сделаем так, чтобы все сообщения об изменениях проходили через один коммуникационный канал. Для этого выберем по объекту в каждой подсистеме, которые будут играть роль концентраторов. Объект-концентратор в наблюдаемой подсистеме будет собирать в себе все события изменения в подсистеме. Объект-концентратор в системе-наблюдателе будет доставлять полученные события об изменениях до адресата (конкретного объекта в подсистеме-наблюдателе). К этим двум объектам-концентраторам мы, в свою очередь, применим образец Наблюдатель, таким образом, установив канал связи между двумя подсистемами. Диаграмма кооперации на Рис. 2.6 иллюстрирует структуру образца Коммуникационный концентратор. 32
subject
observer Observer
observerHub
«» «»
subjectHub
A
a
B
b
D
d
C
c
Рис. 2.6. Структура образца Коммуникационный концентратор
Итак, когда происходит изменение состояния наблюдаемой системы (Рис. 2.7), создается объект-событие, который описывает собственно изменение и задает его локализацию. Событие передается вверх по иерархии компоновщика до объектаконцентратора, затем событие передается через канал связи на концентратор системынаблюдателя. После этого концентратор системы-наблюдателя анализирует событие и принимает решение о действиях, которые необходимо предпринять. Client
C
B
A
SubjectHub
ObserverHub
setProperty1(newVal)
Update() HandleEvent()
Рис. 2.7. Диаграмма последовательности передачи сообщения
33
Результаты Применение образца проектирования Коммуникационный концентратор позволяет удовлетворить предъявленные к системе оповещения требования. Действительно, наблюдаемая система не имеет жесткой привязки к наблюдателям. Образец одинаково хорошо применим как в случае, когда система-наблюдатель является прямым отражением наблюдаемой, так и когда объектные структуры систем абсолютно различны (например, наблюдатель просто печатает протокол полученных событий). Для наблюдения за состоянием всей системы достаточно организовать наблюдение за соответствующим ей концентратором.
Реализация Следует отметить, что для иерархических структур, полученных в результате применения образца Компоновщик, резонно использовать в качестве Коммуникационного концентратора корневой объект иерархии (в нашей обобщенной системе на Рис. 2.6 в роли концентраторов можно было бы задействовать объекты A и a соответственно).
Родственные образцы •
Фасад [14] предоставляет унифицированный интерфейс к множеству интерфейсов в некоторой подсистеме. Определяет интерфейс более высокого уровня, облегчающий работу. Объекты-концентраторы могут рассматриваться как фасады специального назначения, предназначенные для упрощения коммуникаций между двумя подсистемами.
•
Наблюдатель [14] определяет между объектами зависимость типа один-комногим, так что при изменении состояния одного объекта все зависящие от него получают извещение. Этот образец проектирования используется для организации доставки сообщений между концентраторами. Заметим, что, таким
образом,
Коммуникационный
концентратор
позволяет
иметь
произвольное количество систем-наблюдателей. •
Цепочка обязанностей [14] позволяет избежать жесткой зависимости (связности) отправителя запроса от его получателя, при этом запросом начинает
обрабатываться
один
из
нескольких
объектов.
Объекты-
получатели связываются в цепочку, и запрос передается по цепочке, пока какой-нибудь объект его не обработает. В наблюдаемой системе, имеющей структуру компоновщика, передача событий вверх до концентратора может 34
напомнить Цепочку обязанностей. Действительно, компонент [14] в Компоновщике
не
концентратору.
имеет
Все
жесткой
включаемые
зависимости
по
отношению
объекты-компоновщики
к
имеют
возможность доставить сгенерированное событие. В отличие от Цепочки обязанностей, событие, не будет доставлено, пока не пройдет всех вложенных Компоновщиков.
2.5 Компоновщик (Composite) Образец Компоновщик компонует объекты в древовидные структуры для представления иерархий вида часть-целое. Позволяет клиентам единообразно трактовать индивидуальные и составные объекты. На Рис. 2.8 представлена структура образца Компоновщик, а на Рис. 2.9 приводится структура типичного составного объекта, полученного в результате применения образца Компоновщик. Итак, разберем участников, составляющих структуру образца: •
Класс Component – компонент, определяет интерфейс для компонуемых объектов, объявляет интерфейс для доступа к потомкам и управления ими, определяет интерфейс для доступа к родителю компонента в рекурсивной структуре и при необходимости реализует его.
•
Класс Leaf – лист, представляет листовые узлы и не имеет потомков, определяет поведение примитивных объектов в композиции.
•
Класс Composite – составной объект, определяет поведение компонентов, у
которых
есть
потомки,
хранит
компоненты-потомки,
реализует
относящиеся к управлению потомками операции в интерфейсе класса Component. •
Класс Client – клиент, манипулирует объектами композиции через интерфейс Component.
35
Component add(component : Component) remove(component : Component) getChild(index : int) : Component
Client
Leaf
Composite
operation()
children
add(component : Component) remove(component : Component) getChild(index : int) : Component
Рис. 2.8. Структура образца Компоновщик
aComposite
aLeaf
aLeaf
aComposite
aLeaf
aLeaf
aLeaf
aLeaf
Рис. 2.9. Структура типичного составного объекта
Этот образец исчерпывающим образом описан в [14], мы не будем здесь приводить его описание. Тем не менее, мы считаем необходимым рассказать о том, как этот образец используется в метамодели UML, потому что это оказало серьезное влияние на ряд проектных решений. Создатели UML понимали, что объектная модель, где все составляющие ее объекты свалены в кучу, будет крайне не практична в использовании. Такие операции как обход модели, сохранение ее на внешний носитель и даже собственно сохранение и получение информации из конкретных ее элементов могут быть крайне сложны, если для модели не задана иерархия. Использующее приложение должно иметь возможность уникально идентифицировать любой элемент модели. Поэтому было принято решение, о том, что каждый элемент модели, который не является частью другого элемента модели, должен быть объявлен в одном и только в одном пространстве имен [6].
36
Это
задает
иерархию
вложенности,
которая
позволяет
получить
структурированную модель. Пространство имен формирует центральный образец Компоновщик, который определяет структуру модели. На Рис. 2.10 представлена диаграмма классов, демонстрирующая применение образца Компоновщик для структуризации модели UML. В метамодели UML определен класс Namespace, который участвует в образце Компоновщик (на диаграмме – кооперация Composite) в роли составного объекта (composite) в роли же компонента (component) выступает ModelElement, т.е. любой элемент модели. Таким образом, все элементы модели, которые являются подклассами Namespace, могут содержать в себе другие элементы модели. +ownedElement +importedElement *
ModelElement
*
name:Name component
ElementImport visibility:VisibilityKind alias:Name isSpecification:boolean
«» «»
Composite composite GeneralizableElement
Namespace 0..1
ElementOwnership *
Package
visibility:VisibilityKind isSpecification:boolean
Рис. 2.10. Пространства имен UML
Примером такого подкласса служит Package (пакет). Помимо этого, пакет связан отношением агрегирования с ModelElement, параметры агрегирования задаются в классе ElementImport. Это позволяет использовать совместно элементы из разных пакетов, т.е. определяется импорт элементов. Аналогия из языков программирования – оператор import в Java и оператор use в C++.
2.6 Одиночка (Singleton) Образец Одиночка [14] гарантирует, что у класса есть только один экземпляр, и предоставляет к нему глобальную точку доступа. Нам этот образец понадобится при реализации загрузчика из XMI. Одиночка достаточно подробно рассмотрен в [14], поэтому здесь мы приведем только его структуру (Рис. 2.11)
37
Singleton instance():Singleton { return uniqueInstance }
uniqueInstance : Singleton singletonData instance() : Singleton singletonOperation() getSingletonData()
Рис. 2.11. Структура образца Одиночка
В структуре образца Одиночка только один участник – собственно класс Singleton. Этот класс определяет операцию instance, которая позволяет клиентам получать доступ к единственному экземпляру, instance является операцией класса, то есть статической функцией-членом в терминологии C++ или статическим методом в Java. Клиенты получают доступ к экземпляру класса Singleton только через его операцию instance. Приведем пример реализации этого образца на языке Java: public class Singleton { private static Singleton instance; private Singleton() { } public static Singleton instance() { if (instance == null) { instance = new Singleton(); } return instance; } }
Для того чтобы запретить прямое создание экземпляра класса, мы сделали его конструктор закрытым (private). Это не всегда удобно, так как одновременно запрещает порождение наследников от этого класса, поэтому иногда (а скорее, всегда, кроме тех случаев, когда вы по каким-то причинам хотите запретить порождение подклассов) рекомендуется делать конструктор защищенным (protected). Интересный факт: несмотря на то, что образец Одиночка является, пожалуй, самым простым по своей структуре из 23 рассмотренных в книге «банды четырех», тем не менее, он же явился одним из наиболее горячо обсуждаемых образцов. Одиночка – это шаг вперед, относительно глобальных переменных, поскольку позволяет избежать засорения пространства имен глобальными переменными, в которых хранятся уникальные экземпляры [14]. Одним из достоинств одиночки по отношению к глобальным 38
переменным является то, что, вообще говоря, в последствии разработчик может безболезненно (не изменяя клиентского кода) изменить свое решение о единственности экземпляра [14,21]. Однако, поскольку одиночки имеют много общего с глобальными переменными, то им свойственны многие связанные с ними проблемы [20,21]. Поступали даже предложения переименовать этот образец в GlobalObject (аналогия с global variable) [22], с тем, чтобы само название настораживало разработчика, перед тем, как использовать этот образец. Значительная часть дискуссий посвящена вопросам реализации одиночки [21]. Peter Haggar [24] разбирает ряд, иногда достаточно неожиданных, нюансов, которые необходимо учитывать при реализации этого образца для многопоточной системы на Java.
39
3 Проектирование и реализация программного продукта Проектирование
и
реализацию
мы
описываем
как
протокол
фактически
выполненных шагов с обоснованием каждого шага. В начале каждого раздела этой главы приводится краткое описание (выделено курсивом) того, что мы намереваемся сделать. Эти намерения сформулированы либо в форме «исходный артефакт» → «результирующий артефакт», либо простым предложением. Всего шагов восемь: в разделах 3.1-3.4 выполняется моделирование - описываются трансформации метамодели стандарта; далее осуществляется переход от модели в UML-инструменте к программному коду на Java (раздел 3.5); после некоторых модификаций (раздел 3.6) мы имеем реализацию модели; в завершении главы (разделы 3.7,3.8) мы реализуем загрузчик из XMI и механизм оповещения о событиях.
3.1 Физическая метамодель UML 1.4. Физическая модель из стандарта UML 1.4 в формате XMI → Модель в инструменте моделирования (Rational Rose). Семантика UML поддерживается посредством реализации логической метамодели UML. Логическая модель UML, содержит около полутораста мета классов. В стандарте приводится детальное описание всех мета классов и отношений между ними. Наша модель (физическая метамодель UML) должна поддерживать представление элементовэкземпляров этих мета классов. Следует отметить, что часть работы по трансформации логической метамодели в физическую, была выполнена авторами языка. В стандарте UML есть раздел, посвященный обмену моделями в формате XMI, в этом разделе приводится физическая метамодель языка, которая использовалась авторами языка для генерации DTD 14 , соответствующего UML модели. Кроме DTD, была выполнена генерация IDL-файлов, которые определяют интерфейсы CORBA-хранилища UML модели. В заключение авторы сохранили эту метамодель в виде XML, следуя тому же стандарту XMI для представления метамоделей [11]. Естественно, хотелось бы повторно использовать этот артефакт. Возможно несколько подходов к использованию этой метамодели: 14
Аббревиатура Document Type Definition – описание структуры XML документа.
40
•
Взять распечатки диаграмм физической модели и как-нибудь с ними работать (например, восстановить их в одном из UML инструментов, стараясь нигде не ошибиться).
•
Взять интерфейсы IDL и дать их на вход одному из IDL-компиляторов, например, idl2java, а затем произвести трансформацию кода.
•
Взять DTD и также обработать их подходящим инструментом, например, средством моделирования, поддерживающим построение классов модели по DTD, или использовать один из существующих генераторов классов по DTD (технология XML Binding15).
•
Наконец взять представление XMI метамодели и загрузить его в инструмент моделирования для последующей обработки метамодели.
Khalasa
и
Simpson
[13]
использовали
второй
подход
(IDL-файлы)
для
автоматизации процесса реализации метамодели. Однако прямое использование IDL приводит к получению неэффективной в смысле удобства использования модели. Нам представляется
более
привлекательным
использовать
подход,
который
хорошо
согласуется с наилучшими известными практиками. Которые, в свою очередь, состоят в том, чтобы в начале разработки получить модель и ее графическое представление в инструменте моделирования. Модель, которой мы полностью доверяем в смысле полноты и на которую можем посмотреть, благодаря имеющимся диаграммам. Только после этого мы приступим к генерации кода (кодированию). В пользу этого решения говорит еще один обнаруженный нами факт – при трансформации логической модели в физическую, авторами были проигнорированы ограничения на ассоциации, например, ассоциаций между Collaboration и Operation xor Classifier (здесь xor – это исключающее или) (см. Рис. 3.1), т.е. одновременно может существовать только одна из этих ассоциаций. Мы считаем, что проверку
этого
ограничения
можно
возложить
на
специальный
верификатор
непротиворечивости модели (см. раздел 4.2). Наш подход к разработке позволяет выявить и учесть подобные различия в физической и логической моделях стандарта.
15
Например, JAXB (Java Architecture for XML Binding) от Sun Microsystems.
41
Operation Collaboration
{xor} Classifier
Рис. 3.1. Пример ограничения на ассоциацию в модели семантики
Поскольку мы приняли решение работать с физической метамоделью стандарта, то в принципе, нам не важно какой из четырех артефактов использовать – главное, чтобы его умел правильно загружать инструмент моделирования. Естественно предположить, что лучше всего модель должна была сохраниться в XMI. На Рис. 3.2 представлен фрагмент метамодели в формате XMI. Опытная проверка этого факта в Rational Rose подтвердила наше предположение. Поэтому мы произвели загрузку метамодели из XMI в модель Rational Rose, используя конвертер фирмы UniSys.
42
Рис. 3.2. Метамодель UML в формате XMI
Итак, мы получили представление физической метамодели UML в проекте выбранного инструмента моделирования. На Рис. 3.3 представлена диаграмма, иллюстрирующая зависимости между пакетами полученной модели.
43
Data_Types
Core
Model_Management
Common_Behavior
Collaborations
Use_Cases
State_Machines
Activity_Graphs
Рис. 3.3. Структура пакетов метамодели.
3.2 Проекции модели на диаграммы классов Метамодель UML в Rational Rose → Проекции модели на диаграммы классов. Как отмечалось ранее, вообще говоря, между диаграммами и моделью не существует взаимно однозначного отображения. К тому же для машины диаграммы и вовсе бессмысленны. Они нужны только человеку – для того, чтобы посмотреть на модель в определенной проекции. Сама модель может быть довольно сложна – состоять из большого числа сущностей и отношений. Метамодель UML одна из таких сложных моделей, поэтому нам удобнее анализировать ее не целиком, а по частям – в виде отдельных
диаграмм.
Авторы
UML
предлагают
нам
такой
набор
диаграмм,
представленный в разделе описания физической метамодели. Мы построим в нашем проекте эквивалентные проекции модели. На Рис. 3.4 изображен пример одной из 20 диаграмм – центральная диаграмма Backbone. Следует подчеркнуть, что эта диаграмма – не копия рисунка из стандарта. Она получена проекцией модели, которую мы подготовили на предыдущем шаге. Диаграмма из стандарта служила вдохновляющим образцом при создании этой диаграммы. То есть мы не рисовали эти классы и отношения между ними с нуля, а выполняли проектирование их на плоскость диаграммы, отбрасывая несущественные элементы. 44
Element
ModelElement +ownedElement
isSpeci fication : Boolean name : Name visibil ity : VisibilityKind
*
+namespace
+constrainedElement * {ordered}
0..1
+constraint
Namespace
General izableElement
Parameter
isAbstract : Boolean isLeaf : Boolean isRoot : Boolean
*
Constrai nt
defaultVal ue : Expression kind : ParameterDirecti onKind
body : BooleanExpression
+typedParameter * +parameter
* {ordered}
Feature ownerScope : ScopeKi nd +feature
* {ordered} 0..1 +owner
Classifier
+type 1..1
+type 1..1
Structural Feature
BehavioralFeature
changeabili ty : Changeabl eKind +typedFeature multiplicity : Multi plicity ordering : OrderingKind * targetScope : ScopeKind
isQuery : Boolean
Operation
Attribute initial Value : Expression
concurrency : CallConcurrencyKind isRoot : Boolean isLeaf : Boolean isAbstract : Boolean specification : String
+behavi oral Feature 0..1
1..1 +specification
+method *
Method body : ProcedureExpressi on
Рис. 3.4. Backbone.
3.3 Устранение множественного наследования Модель и ее проекции, содержащие множественное наследование реализации → Модель и соответствующие диаграммы, без множественного наследования. Физическая метамодель UML интенсивно использует множественное наследование реализации. Как известно, множественное наследование создает много проблем. Проблема множественного наследования обсуждается во многих книгах, в частности [25,26,27]. При интенсивном использовании множественного наследования легко попасть в двусмысленную ситуацию (см. образец Элиминация множественного наследования, раздел 2.2). По этой причине большинство современных промышленных языков 45
программирования (Delphi, Java, C#, VB.NET) не поддерживают множественного наследования
реализации,
но
зато
поддерживают
множественное
наследование
интерфейсов. Резонно возникает вопрос – почему же авторы языка UML так интенсивно использовали множественное наследование? Конечно, авторам было известно о тех сложностях, которые могут возникать при реализации множественного наследования, но их задача состояла не в том, чтобы предоставить физическую модель UML на все случаи жизни (легко реализуемую на любом языке программирования), а дать исчерпывающее, понятное разработчикам описание семантики UML. И для достижения этой цели множественное наследование является весьма неплохим приемом. Действительно, возьмем,
например,
мета
класс
AssociationClass
(класс-ассоциация)
–
он
характеризуется тем, что обладает, как свойствами ассоциации (Association), так и свойствами класса (Class) – это легко показать на диаграмме классов, достаточно определить отношения обобщения от AssociationClass к Class и Association соответственно (Рис. 3.5), не правда ли все понятно и лаконично? Заметим, что от AssociationClass до базового класса ModelElement существует 4 различных пути в
графе
наследования.
ModelElement
имеет
атрибут
name,
и,
конечно,
AssociationClass не должен иметь 4 различных таких атрибута, ясно, что у всех потомков ModelElement должен быть только один атрибут name (в стандарте на эту тему даже сказаны специальные слова). Итак, нам необходимо произвести первую трансформацию модели – устранить множественное наследование, для того, чтобы наша метамодель была реализуема на Java, то есть мы находимся в контексте применимости образца Элиминация множественного наследования. Из сказанного выше следует, что устранение множественного наследования было бы полезно, даже если бы в качестве языка реализации был выбран другой язык (например, C++ поддерживает множественное наследование реализации). В результате проведенного анализа модели, были выявлены следующие элементы, участвующие во множественном наследовании (см. Рис. 3.5).
46
ModelElement
Namespace
Package
GeneralizableElement
Classifier
Relationship
Association
Link
Instance
(from Common_Behavior)
(from Common_Behavior)
Generalization
Object (from Common_Behavior)
(from Model_Management)
Subsystem (from Model_Management)
LinkObject Class
(from Common_Behavior)
AssociationClass
Рис. 3.5. Множественное наследование в метамодели UML
Заметим, что мета класс Generalization не участвует во множественном наследовании, и здесь мы его привели только для того, чтобы на его примере показать, как трансформации модели затронут подобные ему элементы. Далее, видим, что этот граф наследования логически хорошо делится на две диаграммы: базовые элементы метамодели (Рис. 3.6) и второстепенные (Рис. 3.7).
47
ModelElement
Namespace
GeneralizableElement
Package
Classifier
Relationship
Association
Generalization
(from M odel_Management)
Subsystem
Class
(from M odel_Management)
AssociationClass
Рис. 3.6. Базовые элементы метамодели UML, участвующие во множественном наследовании ModelElement
Link
Instance
(from Common_Behavior)
(from Common_Behavior)
Object (from Common_Behavior)
LinkObject (from Common_Behavior)
Рис. 3.7. Второстепенные элементы, участвующие во множественном наследовании
Произведем элиминацию множественного наследования для базовых элементов. Используя образец Элиминация множественного наследования, мы идентифицируем классы, которые можно заменить интерфейсами, в результате получаем иерархию наследования, представленную на Рис. 3.8.
48
ModelElement
BasicModelElement
Namespace
GeneralizableElement
Package (from M odel_Management)
Subsystem (from M odel_Management)
Classifier
Relationship
Association
Generalization
Class BasicAssociation
AssociationClass
Рис. 3.8. Результат замена классов на интерфейсы и добавление нескольких новых
Здесь
GeneralizableElement,
Relationship,
Classifier
и
Association стали интерфейсами. Класс ModelElement был разбит на два: собственно интерфейс ModelElement и реализацию BasicModelElement. Это позволяет интерфейсам GeneralizableElement и Relationship расширять интерфейс ModelElement. Мы сделали интерфейсами именно GeneralizableElement и Relationship, а не Namespace, потому что гораздо важнее сохранить отношение обобщения между Namespace и реализацией ModelElement (BasicModelElement), чем между GeneralizableElement или Relationship и BasicModelElement. Причина этого заключается в том, что Namespace и BasicModelElement являются участниками образца Компоновщик, описанного в предыдущей главе (см. образец Компоновщик, Рис. 2.10), который используется для определения пространства имен UML. Пространство имен UML – это одна из центральных составляющих семантики языка, поэтому так важно сохранить это отношение обобщения. 49
После замены классов на интерфейсы мы должны предоставить реализацию этих интерфейсов в виде отдельных классов, чтобы их можно было повторно использовать, на Рис. 3.9 представлена диаграмма классов с новыми классами реализации (суффикс ‘Impl’ в имени классов) и отношениями делегирования, там, где это необходимо. «interface» ModelElement
BasicModelElement
Namespace
«interface» GeneralizableElement
«interface» Classifier
Package
«interface» Relationship
GeneralizableImpl Class
Subsystem
Generalization
«interface» Association
ClassifierImpl
AssociationClass
BasicAssociation
AssociationImpl
RelationshipImpl
Рис. 3.9. Добавление классов с реализацией интерфейсов
Заметим, что, в силу особенностей класса Relationship, RelationshipImpl, вообще говоря, не нужен – мы его привели только для общности. Дело в том, что Relationship не задает никакого нового поведения, а определяет только тип, чтобы сделать семантику типов интуитивно понятной, действительно, сразу видно, что бывает два вида отношений (relationships) – Association и Generalization. Таким образом, здесь вполне достаточно интерфейса. Итак, мы избавились от множественного наследования реализации. В результате получили достаточно большое количество классов, которым мы будем делегировать реализацию, причем, как правило, они неоднократно используются, т.е. мы должны были бы реализовать большое количество методов с короткими телами (просто передача запроса классу-реализации). Избежать этого позволяет образец Грань. Заменим классы GeneralizableImpl, ClassifierImpl и AssicationImpl на GeneralizableFacet, ClassifierFacet и AssicationFacet соответственно (Рис. 3.10.). Эти классы хорошо выступят в роли граней, потому что их интерфейсы, в общем, достаточно обособлены. В результате, мы получили параллельные иерархии 50
наследования: классы-грани образуют такую же иерархию, как соответствующие им интерфейсы (GeneralizableElement, Classifier и Association). BasicModelElement
Namespace
«» «»
Facet
«interface» GeneralizableElement
Package GeneralizableFacet
getGeneralizableFacet()
getGeneralizableFacet()
«» «»
Facet
«interface» Classifier
Class
getClassifierFacet()
getGeneralizableFacet() getClassifierFacet()
ClassifierFacet
main «interface» Association
«» «»
Facet facet
BasicAssociation getGeneralizableFacet() getAssociationFacet()
getAssociationFacet()
AssociationFacet
Рис. 3.10. Переход к классам-граням
Приведем пример использования класса-грани при реализации класса Class, следующий фрагмент кода немного упрощен – мы сознательно вырезали из него все, что относится к поддержке атрибутов, добавляемых Class, как по отношению к своим родителям (isActive), так и другие, не относящиеся к граням моменты: public class Class extends Namespace implements Classifier { private ClassifierFacet classifierFacet = new ClassifierFacet(this); public ClassifierFacet getClassifierFacet(){ return classifierFacet; } public GeneralizableFacet getGeneralizableFacet(){ return classifierFacet; } }
51
Видим, что объект-грань хранится как закрытый атрибут (classifierFacet) главного класса (Class), поэтому объект-грань имеет то же время жизни, что и основной объект. В теле методов getClassifierFacet и getGeneralizableFacet стоят одинаковые
операторы,
GeneralizableFacet.
т.к.
ClassifierFacet
Остается
заметить,
что
является параметром
подклассом конструктора
ClassifierFacet является ссылка на объект главного класса – это сделано для поддержки двухсторонней связи Facet и Main классов (согласно представленной выше диаграмме классов), нам это понадобится, в частности, при реализации механизма оповещения о событиях. Итак, мы произвели элиминацию множественного наследования для базовых элементов. Подытожим, что мы имеем после выполнения этой процедуры: •
две новые диаграммы (Рис. 3.8 и Рис. 3.10);
•
некоторые
классы
стали
интерфейсами
(например,
Association,
GeneralizableFacet); •
новые
сущности:
базовые
BasicAssociation),
классы
классы-грани
(BasicModelElement, (ClassifierFacet,
AssociationFacet); •
появились отношения реализации вместо отношений обобщения (там, где обобщаемые классы стали интерфейсами);
•
кое-где нарушена семантика.
Нас все это устраивает, кроме последнего пункта, прокомментируем его на примере мета класса Class. Для этого обратимся к оригинальной диаграмме Classifiers (Рис. 3.11) видим, что Class является далеко не единственным классификатором, то есть мы должны теперь поправить эту диаграмму – заменить отношение обобщения между конкретными классификаторами (Class, Interface, DataType и т.д.) на отношение реализации интерфейса Classifier и обобщения класса Namespace. Заметим, что, так как мы удалили из модели отношение обобщения между Class и Classifier, когда разбирались с базовыми элементами, то Class на диаграмме классификаторов будет «висеть» сам по себе – это позволяет нам сразу заметить, что модель в этом месте требует корректировок. Исправленный вариант диаграммы классификаторов приведен в приложении 1, Рис. 6. После этого необходимо систематически проработать все оставшиеся диаграммы, производя корректировки с
52
целью восстановления семантики элементов. Полный набор диаграмм нашей модели представлен в приложении 1. Classifier feature : Feature powertypeRange : Generalization
Component * +implementation Artifact deploymentLocation : Node residentElement : ElementResidence +implementationLocation * implementation : Artifact * 1..1 +container +deployedComponent
Class isActive : Boolean DataType
Interface +residentElement *
+deploymentLocation
ElementResidence
* Node deployedComponent : Component
Primitive
visibility : VisibilityKind container : Component resident : ModelElement +elementResidence
*
+resident 1..1
ProgrammingLanguageDataType expression : TypeExpression
ModelElement
Enumeration +enumeration literal : EnumerationLiteral 1..1
+literal 1..* {ordered}
EnumerationLiteral enumeration : Enumeration
Рис. 3.11. Классификаторы
3.4 Типы данных Отображение базовых типов данных UML на типы данных Java. В предыдущем разделе мы устранили множественное наследование реализации в метамодели. В процессе наших преобразований остался незатронутым только один пакет – Data Types (типы данных). Из названия следует, что этот пакет содержит классы, представляющие типы данных UML, которые участвуют в описании семантики UML. В этом разделе мы определим отображение этих типов на типы данных языка реализации (Java). Некоторые типы данных, такие как Integer, Boolean и String очевидным образом отображаются на соответствующие типы Java (примитивные int, boolean и класс String). Но есть и такие, которые не имеют прямых эквивалентов в Java, например, 53
UnlimitedInteger. В таких случаях мы должны либо создать новые классы, которые будут представлять эти типы данных, либо отобразить их каким-нибудь образом на существующие типы. UnlimitedInteger
может
быть
представлен
типом
int,
здесь
‘-1’
соответствует unlimited (бесконечности). Действительно, этот тип данных в метамодели
используется
только
для
верхней
границы
диапазона
кратности
(MultiplicityRange), и, по определению, представляет множество неотрицательных чисел, пополненное специальным элементом unlimited [10]. Заметим, что такое же отображение используется в XMI [11]. В
стандарте
целый
ряд
типов
определен
с
использованием
стереотипа
«enumeration», например, OrderingKind (все они имеют суффикс Kind) задает два значения: ok_ordered, ok_unordered. В Java нет специальной конструкции для перечисляемых типов (что-нибудь вроде enum в C++). Рассмотрим два основных подхода к реализации таких типов. Первый подход предлагает использовать интерфейс для создания группы числовых констант, представляющих каждое значение из диапазона. Приведем пример кода, реализующего этот подход: public interface OrderingKind { int OK_UNORDERED = 0, OK_ORDERED = 1; }
Главный недостаток такого подхода – отсутствие безопасности типов, то есть легко можно подставить вместо OrderingKind любое значение типа int. Мы используем другой прием, который позволяет получить безопасный перечисляемый тип. Опуская некоторые второстепенные методы, приведем реализацию OrderingKind: public final class OrderingKind extends BasicKind { private OrderingKind(int val) { value = val; } public final static OrderingKind OK_UNORDERED = new OrderingKind(0), OK_ORDERED = new OrderingKind(1); }
Подобный подход к представлению перечисляемых типов Java применяет Брюс Эккель [25], подробно вопросы реализации таких типов в Java рассмотрены на
54
соответствующей странице Wiki Enumerated Types in Java [23]. В базовом классе BasicKind определена операция сравнения перечисляемых типов:
55
public class BasicKind { protected int value; public boolean equals(BasicKind kind) { return value == kind.value; } }
Остальные типы данных реализуются в виде отдельных классов в соответствие с тем, как они определены в модели. В Табл. 3, приведено искомое отображение. Тип данных UML
Java
Комментарий
Integer
int
UnlimitedInteger
int
String
String
Boolean
boolean
Kind
Kind
-1 вместо ‘unlimited’
Все
перечисляемые
реализуются,
как
типы было
показано ранее Expression
Expression
Expression Expression Все подклассы Expression Multiplicity
Multiplicity
MultiplictiyRange
MultiplictiyRange
Name
Name
Класс с атрибутом типа String
Табл. 3. Отображение типов данных UML на типы Java
Итак, в предыдущем разделе мы описали трансформации, которые позволили нам получить
из
физической
метамодели
стандарта
метамодель,
пригодную
для
непосредственной реализации на Java, теперь у нас есть модель и отображение типов данных – можно приступать к реализации.
3.5 Генерация кода Модель в Rational Rose → Код на Java. В этом разделе мы расскажем о переходе от модели к программному коду. Моделирование мы выполняли в Rational Rose, потому что с нашей точки зрения этот инструмент наилучшим образом удовлетворяет потребностям, возникающим при моделировании метамодели UML. Вероятно одна из причин этого в том, что авторы языка 56
тоже использовали этот инструмент (диаграммы стандарта подготовлены в Rational Rose). К сожалению, генерация кода в Rational Rose выполняется не столь хорошо. Поэтому для этой операции мы выбрали другой инструмент моделирования – Together. Этот продукт поддерживает интеграцию с Rational Rose, что позволило нам безболезненно произвести из него загрузку модели Rose. Конечно, не обошлось без ошибок, но простой проход по проекту в Together позволил легко их устранить. Together поддерживает однозначное соответствие кода и модели. Эта возможность Together очень удобна, при использовании инструмента на стадии погружения модели в код, т.к. она позволяет нам производить оперативные исправления в коде, одновременно отслеживая последствия этих изменений на диаграммах классов модели. Таким образом, мы получили набор java-классов – отображение нашей метамодели на реальный код языка реализации. Несколько слов следует сказать о том, как же работает отображение модели в код Java. Для этого мы должны вернуться к нашей модели. Видим, что она состоит преимущественно из сущностей, которые хорошо отображаются на объектную модель Java. Действительно, наша модель – это множество классов и отношений между ними. Классы модели переходят в классы Java. С отношениями реализации и обобщения все ясно – они имеют эквиваленты в Java (ключевые слова implements и extends соответственно). Остается рассмотреть отношения ассоциации, сразу отметим, что по умолчанию ассоциации транслируются Together в комментарии в программном коде, этим, правда, можно управлять – отдельно для каждой ассоциации задавая как именно ее нужно представить в программном коде. Но мы считаем, что в нашем случае такой подход довольно не удобен. Поясним почему: в UML есть понятие производный (выводимый) элемент (derived element) [6,10] – элемент, который можно вычислить по другим элементам. Он вводится в диаграмму для большей ясности или каких-либо других целей, несмотря на то, что не несет собственной семантической нагрузки. На Рис. 3.12 приведен пример выводимого атрибута – у класса Человек (Person) есть два атрибута: дата рождения (birthDate) и возраст (age), здесь age – выводимый атрибут – это обозначено символом ‘/’, а в комментарии указано очевидное правило вычисления данного атрибута (из текущей даты нужно вычесть дату рождения).
57
Person {age = currentDate - birthDate}
birthDate /age
Рис. 3.12. Пример выводимого атрибута
Возникает вопрос – зачем же нужны выводимые атрибуты, например, в теории реляционных баз данных есть правило, которое гласит, что не надо хранить то, что можно вычислить. Однако, как известно, на каждое правило есть исключение – если значение определенной величины часто бывает востребовано клиентом, а вычисление его требует заметных ресурсов, то эффективнее (по времени, но не по памяти) хранить эту величину. Так даже в нашем простом примере, если клиенту часто требуется получать возраст человека, то, возможно, эффективнее сохранить его явно. На Рис. 3.13 приведен другой пример выводимых атрибутов – между классами ClassA и ClassB определена ассоциация с ролями полюсов roleA и roleB соответственно. Таким образом, у каждого из этих классов атрибуты roleB и roleA выводимы по ассоциации. Одним из наиболее заметных отличий в описаниях физических метамоделей стандартов UML 1.3 и UML 1.4 является то, что в последнем авторы интенсивно использовали выводимые атрибуты в том же контексте, что и мы на Рис. 3.13, то есть для указания ролей элементов, с которыми ассоциирован данный элемент модели. ClassA
+roleA
/roleB
+roleB
ClassB /roleA
Рис. 3.13. Другой пример использования выводимых атрибутов
Таким образом, мы можем воспользоваться выводимыми атрибутами для реализации ассоциаций. Следует отметить, что раньше (в UML 1.3) при описании метамодели использовались как двунаправленные ассоциации, так и однонаправленные, в UML 1.4 все ассоциации стали двунаправленными. Мы считаем, что авторы сделали это для упрощения и некоторого обобщения модели (направления ассоциаций как бы подразумеваются, но, в то же время, решение о направленности ассоциации, возлагается на разработчика модели) однако, при добавлении выводимых атрибутов, были учтены прежние направления ассоциаций. Мы примем гипотезу о том, что авторы действительно ввели выводимые атрибуты везде и только там, где они нужны. Проверить эту гипотезу мы сможем в приложениях нашей модели, забегая вперед, скажем, что наша гипотеза подтвердилась. Далее, ассоциации можно разделить на 3 подгруппы: 58
•
«один-к-одному»
•
«один-ко-многим»
•
«много-ко-многим»
В случае, когда кратность полюса ассоциации принимает значения от 0 до 1, мы вводим атрибут у соответствующего класса – ссылка на ассоциированный с ним класс (собственно это уже сделано авторами языка), а когда кратность может принимать значения больше 1 – на месте атрибута вводим коллекцию ссылок на ассоциированные классы.
3.6 Рефакторинг Инкапсуляция атрибутов, изменение структуры пакетов. Этот раздел носит не совсем понятное и явно не русского происхождения название. К сожалению, нам не удалось найти никакого варианта перевода английского термина ‘refactoring’, кроме транслитерации. Рефакторинг – это дисциплинированное изменение дизайна системы, без изменения ее функциональности, направленное на улучшение структуризации, гибкости и т.п. Этот термин появился сравнительно недавно, в 1992 году William Opdyke посвятил рефакторингу каркасов приложений (application framework) свою докторскую диссертацию [18]. Позднее, в 1999 году, Мартин Фаулер написал книгу с одноименным названием [16], и это дало решающий толчок – последовало большое количество разнообразных работ, и, начиная, с 2001 года стала появляться поддержка со стороны инструментов. Фаулер ведет общедоступный каталог [17] рефакторингов, в настоящий момент он составлен преимущественно на основе его книги. Авторы этих работ приводят различные виды рефакторинга, которые они регулярно применяют на практике. Don Roberts в своей диссертации рассматривает пути и проблемы создания программного инструмента, поддерживающего рефакторинг [19]. Практически
все
новые
среды
разработки
стремятся
в
той
или
иной
мере
автоматизировать рефакторинг. Уровень поддержки варьируется от такого простого, но очень часто применимого вида рефакторинга, как «переименование метода» (rename method), заканчивая такими, как «выделение метода» (extract method) – здесь уже требуется значительно более тонкая работа: необходимо проанализировать код, выделить временные переменные, принять решение о том, как и что с ними делать. Together также поддерживает некоторые виды рефакторинга (Move/Rename Class, Extract Interface/Superclass, Encapsulate Attribute и др.). Нас будет интересовать
59
“Encapsulate Attribute”16 (инкапсуляция, скрытие атрибута) для всех мета классов нашей модели. Цель этого рефакторинга – замена прямого доступа к атрибуту класса на опосредованный доступ через методы. Для этого производится замена спецификатора доступа атрибута класса с public (открытый) на private (закрытый), и вводится пара методов – для чтения и для записи значения атрибута соответственно. Скрытие атрибутов – общая рекомендация к объектно-ориентированному дизайну. Для нарушения инкапсуляции нужно иметь очень серьезные причины. В нашем случае таких причин мы не видим, напротив, есть обратные: опосредованный доступ к атрибутам даст нам необходимую гибкость для реализации механизма оповещения об изменениях в состояниях элементов, об этом будет рассказано в разделе 3.8. В технологии Java есть спецификация для повторно используемых программных компонентов – JavaBeans. JavaBean – это класс или набор классов, представляющих программный компонент, который обладает определенными свойствами, позволяющими внешней программе анализировать, использовать и производить настройку этого компонента. Стандарт включает соглашения по именованию методов доступа к свойствам компонента (getPropertyName, setPropertyName), а также требует наличия конструктора без параметра. Поскольку
компоненты
Java
Beans
обладают
целым
рядом
технических
преимуществ по отношению к обычным классам, то мы будем следовать спецификации JavaBeans. Итак, приведем наши классы в соответствие с JavaBeans спецификацией, используя рефакторинг Encapsulate Attribute. Одиночные атрибуты скрываются по умолчанию – заводятся соответствующие модификаторы доступа. Для коллекций – методы
getCollectionName
и
addCollectionElementName.
Рассмотрим,
например, трансформацию двух атрибутов BasicModelElement. После генерации кода мы имеем следующий код: public abstract class BasicModelElement implements ModelElement { public Namespace namespace; public Constraint constraint; }
Заметим, что выводимый атрибут constraint должен представлять коллекцию ссылок на ограничения (constraints) (см. приложение 1, Рис. 3). Поэтому мы заменим
16
Мартин Фаулер называет этот рефакторинг Encapsulate Field, но, видимо, разработчики решили
придать ему название, непосредственно соответствующее терминологии, принятой в UML.
60
этот атрибут списком, также мы должны теперь переименовать constraint на constraints. После трансформаций код принимает следующий вид: public abstract class BasicModelElement implements ModelElement { private Namespace namespace; private List constraints = new ArrayList(); public Namespace getNamespace() { return namespace; } public void setNamespace(Namespace namespace) { this.namespace = namespace; } public List getConstraints() { return constraints; } public void addConstraint(Constraint constraint) { constraints.add(constraint); } }
В результате применения описанной процедуры мы имеем работающую метамодель, которой, в принципе, уже можно пользоваться. Остается наделить ее некоторыми полезными свойствами, об этом мы расскажем в следующих разделах. Отметим, что для дальнейшей разработки мы будем использовать «традиционную» среду – JBuilder, просто потому, что нам там удобнее писать код, а возможности моделирования нам пока не понадобятся. Поэтому мы выполним построение проекта JBuilder «над» проектом Together (с тем, чтобы при необходимости иметь возможность в любой момент производить операции из Together). При этом мы выполним один простой, но очень важный вид рефакторинга – переименование пакетов. До сих пор структура пакетов была такая же, как в физической метамодели стандарта, мы сохраним общую структуру, но приведем имена пакетов в соответствие с соглашениями на именование пакетов в Java. Правильные имена пакетов очень важны в Java, они обеспечивают формирование уникальных пространств имен. Новая структура пакетов отражена на диаграмме – Рис. 3.14.
61
edu.spbstu.uml.om
datatypes
core
modelmanagement
commonbehavior
collaborations
usecases
statemachines
activitygraphs
Рис. 3.14. Новые имена пакетов
3.7 XMI Загрузчик модели Разработка загрузчика модели из XMI. В конце предыдущего раздела мы завершили реализацию семантики UML. В этом разделе мы расскажем о реализации загрузчика модели из XMI. Анализ имеющихся на рынке UML инструментов показал наличие большого количества используемых версий XMI. Together поддерживает наибольшее количество различных форматов, кроме того, является единственным инструментом, поддерживающим XMI UML 1.4. Поскольку мы реализуем семантику UML 1.4, то мы решили взять за образец модели в XMI UML 1.4, создаваемые в Together. В приложении 2 приведен пример XMI из стандарта UML 1.4 [10] и соответствующая ему диаграмма модели. XMI – это приложение XML, т.е. XML [30] определенного вида. Для XML существует большое количество различных инструментов и утилит, облегчающих работу с документами в этом формате. В частности, существует два признанных стандартных API для синтаксического анализа (parsing): DOM (Document Object Model) [31] и SAX (Simple XML API) [32].
62
Первый разработан W3C практически синхронно со спецификацией XML, второй – является открытым проектом и в настоящее время ведется SourceForge17. SAX был первым API для XML, реализованным на Java, в настоящее время есть реализации на различных языках программирования [32]. Не вдаваясь в подробности этих двух интерфейсов, обозначим основные отличия, определяющие выбор того или иного API в конкретном приложении. DOM – документ (точнее результат его разбора) загружается в память, после чего мы можем с ним работать. В результате разбора имеется иерархическая модель документа, каждый элемент XML, начиная от документа, заканчивая узлами (обозначенными тегами), их атрибутами и значениями является узлом (Node) дерева документа. То есть все манипуляции с документом происходят через работу с узлами дерева, представляющего документ. SAX – этот интерфейс основан на событиях («тег начался» – «тег закончился), возникновение
этих
событий
является
протоколом
разбора
документа,
причем
большинство парсеров разбирают документ за один проход. Таким образом, для ряда задач выгодно выбрать SAX, ведь документ не хранится в памяти. Однако если манипуляции с документом подразумевают многократный обход или произвольный доступ к элементам, то естественно использовать DOM. Проанализируем структуру XML, приведенного в приложении 2 (этот пример мы взяли из стандарта UML 1.4 [10]). Видим, что каждому элементу модели соответствует узел в документе XML, причем некоторые элементы имеют строковые идентификаторы (xmi.id), на которые другие ссылаются. Естественно ожидать, что при обработке такого документа нам потребуется произвольный доступ к его элементам, поэтому мы будем использовать DOM. Таким образом, анализ структуры XMI показал, что загрузку модели можно возложить на сами элементы модели. Для этого у каждого элемента мы заведем метод fromXML с параметром типа DOM Node. Элемент осуществляет загрузку своих атрибутов из Node. На Рис. 3.15 изображен порядок действий, которые выполняются при загрузке модели из XMI. Разберем их подробнее: первый шаг естественен – открытие документа XML, получение его объектного представления (объект doc типа Document).
17
Известный хостинг открытых проектов, http://sourceforge.net/.
63
На следующем шаге обходим дерево документа, начиная с узла, представляющего модель, результаты обхода записываем в хеш-таблицу (Рис. 3.16). В этой таблице данные – объекты специальной структуры, XmiElement, представленной на Рис. 3.17 (имеет два поля: Node и ModelElement), ключами служат идентификаторы элементов (xmi.id), то есть в таблицу попадают только те элементы, у которых есть атрибут xmi.id. Для инициализации свойства modelElement используется специальный параметризованный фабричный
метод
(образец
проектирования
Фабричный
метод
[14])
createModelElement с параметром Node, который по узлу создает соответствующий ModelElement. Отметим, что нам понадобится осуществлять доступ к этой таблице из различных контекстов, кроме того, нам не нужно иметь более одной таблицы одновременно, поэтому мы реализуем ее в виде одиночки (см. образец Одиночка, раздел 2.6). openDocument
doc : org.w3c.dom.Document
traverseTree(modelNode)
: XmiElementMap [filled]
buildModel
Рис. 3.15. Загрузка модели из XMI
64
java.util HashMap
XmiElementMap - instance : XmiElementMap + getMap() : XmiElementMap
Рис. 3.16. Таблица элементов XmiElement - node : org.w3c.dom.Node - modelElement : ModelElement + getNode() : Node + setNode(node) + getModelElement() : ModelElement + setModelElement(modelElement)
Рис. 3.17. Класс, представляющий XMI элемент
На заключительном этапе происходит непосредственное построение модели (buildModel). Для этого осуществляется проход по элементам таблицы, при этом каждому элементу посылается запрос на загрузку его свойств из соответствующего узла документа (посылка сообщения fromXML(node)): Set keys = XmiElementMap.getMap().keySet(); Iterator iter = keys.iterator(); while (iter.hasNext()) { String key = (String)iter.next(); XmiElement element = (XmiElement)XmiElementMap.getMap().get(key); element.getModelElement().fromXML(element.getNode()); }
Остается немного разобрать реализацию fromXML в элементах модели. Прежде всего, заметим, что в абстрактных классах, которые не могут иметь экземпляров (такие, как BasicModelElement) можно было бы не реализовывать загрузку, но тогда мы должны были бы повторить код по загрузке, например, атрибута name в каждом элементе модели. Поэтому мы, наоборот, систематически реализуем метод fromXML в каждом элементе модели, включая абстрактные. Причем, в начале каждого метода будет стоять вызов метода fromXML суперкласса. Классы-грани тоже реализуют загрузку по Node, относящемуся к главному классу, соответственно, в главном классе в реализации метода fromXML выполняются вызовы загрузчика суперкласса, а затем и классов-граней. 65
3.8 Механизм оповещения о событиях Оснащение модели механизмом оповещения об изменении состояний элементов модели. Итак, мы имеем компонент, реализующий семантику UML, который снабжен загрузчиком модели из XMI. Этот компонент уже можно использовать в некоторых инструментах, например, в верификаторе модели, сохраненной в XMI, на соответствие правилам непротиворечивости. То есть в таких инструментах, которые не работают с моделью в динамике, а получают на вход статическую модель и производят с ней какиелибо действия. Справедливости ради следует отметить, что спектр таких приложений достаточно широк: конвертеры из модели UML в заданное представление будь то другой язык моделирования (например, IDL) или же другая версия XMI, генераторы кода на интересующем языке реализации, анализаторы разнообразных свойств модели. Но для комплексного инструмента моделирования необходима поддержка синхронизации модели и ее внешнего представления (диаграмм, древовидного навигатора по модели и т.п.). Традиционно эту задачу решают с помощью образца проектирования Наблюдатель [14]. Об особенностях нашего решения мы расскажем в этом разделе. Во-первых, напомним, что элементы нашей модели суть – JavaBeans. То есть мы можем использовать для них все, что предусмотрено для JavaBeans. В частности, спецификация компании Sun на технологию JavaBeans [29] определяет работу с событиями (events) об изменении состояния объекта. Механизм оповещения о событиях, реализованный в JavaBeans основан на образце проектирования Наблюдатель [14]: определены соглашения о том, как наблюдатели подписываются на получение событий от наблюдаемого, а также о том, каким образом события передаются наблюдателям. Источники событий (наблюдаемые) реализуют методы, которые позволяют слушателям событий (наблюдателям) подписываться и отменять подписку на доставку событий. Событие доставляется всем слушателям (listeners), зарегистрированным у источника события, через специальный метод, определяемый интерфейсом слушателя. Все слушатели должны реализовывать этот интерфейс. Определен специальный интерфейс
для
слушателей
изменений
свойств
(properties)
JavaBean
–
java.beans.PropertyChangeListener, также определен класс, представляющий события – PropertyChangeEvent. Рассмотрим, как Rolf Rasmussen [12] использовал эти механизмы для реализации механизма оповещения. В UML определен ряд стандартных механизмов расширения (стереотипы, ограничения, именованные значения). Нас будут интересовать именованные 66
значения (tagged values) – это пара строк: «тэг-значение», которую можно прикреплять к любым типам элементов модели, и которая несет дополнительную информацию, например, информацию об управлении проектом, указания по генерированию кода и требуемые значения стереотипов [6]. Механизм оповещения JavaBeans также использует строковые идентификаторы для свойств (properties). На Рис. 3.18 представлена диаграмма, иллюстрирующая механизм оповещения, использующий технологию JavaBeans и именованные значения. В BasicModelElement реализованы
через
делегирование
методы
addPropertyChangeListener
и
removePropertyChangeListener – они отвечают за регистрацию слушателей. Именованные значения хранятся в отображении (java.util.Map) методы установки и получения именованного значения отвечают за возбуждение события через метод firePropertyChange,
это
отмечено
в
комментариях
к
элементу
BasicModelElement. Итак, мы получили возможность регистрировать слушателей изменений именованных значений во всех элементах модели. Описанную схему оповещения можно было бы реализовать для всех других атрибутов элементов нашей модели. Вместо этого Rasmussen [12] предлагает использовать именованные значения более широко – а именно: повторно использовать уже сделанное, то есть все атрибуты хранить как именованные значения. BasicModelElement имеет константу TAG_NAME, которая хранит ключ именованного значения для атрибута name, соответственно реализация метода setName заключается в вызове setTaggedValue с правильными параметрами (см. Рис. 3.18). java.beans
BasicModelElement - TAG_NAME=”name” : String - taggedValues : Map
-pcSupport
PropertyChangeSupport addPropertyChangeListener(listener) removePropertyChangeListener(listener) firePropertyChange(oldValue, newValue, name)
setTaggedValue(tag:String, value:Object) getTaggedValue(tag:String):Object setName(name:String) getName():String addPropertyChangeListener(listener) removePropertyChangeListener(listener)
0..* «interface» PropertyChangeListener propertyChange(event:PropertyChangeEvent)
setTaggedValue(TAG_NAME, name); PropertyChangeEvent Object oldValue = taggedValues.get(tag); taggedValues.put(tag, value); pcSupport.firePropertyChange(tag, oldValue, value);
Рис. 3.18. Механизм оповещения, основанный на JavaBeans и именованных значениях
67
Этот подход представляется нам очень удачным, но у него есть один серьезный недостаток – использование именованных значений несколько не по назначению. Действительно, легко получить коллизию – если клиент модели установит именованное значение, имя которого совпадает с именем атрибута элемента. Конечно, можно с этим побороться, например, запретить заводить такие именованные значения. Однако это противоречит семантике языка, так как в стандарте [10] ничего не сказано об ограничениях такого рода на именованные значения. Поэтому мы поступим несколько по-другому, а именно: заведем отдельную таблицу
(Map)
атрибутов
элементов
и
пару
методов
setPropertyValue
и
getPropertyValue, заметим, что мы установили видимость этих методов protected, чтобы они были доступны из подклассов, но скрыты от клиента. На Рис. 3.19 представлена модернизированная схема механизма оповещения, здесь setName теперь использует setPropertyValue, первым параметром передается константа (строковый идентификатор имени атрибута), объявленная в классе, а вторым – значение. Аналогичным образом реализован и метод getName. java.beans
BasicModelElement
-pcSupport
- NAME=”name” : String - propertyValues : Map
PropertyChangeSupport addPropertyChangeListener(listener) removePropertyChangeListener(listener) firePropertyChange(oldValue, newValue, name)
#setPropertyValue(propertyName:String, value:Object) #getPropertyValue(propertyName:String):Object +setName(name:String) +getName():String +addPropertyChangeListener(listener) +removePropertyChangeListener(listener)
0..* «interface» PropertyChangeListener
return (Name)getPropertyValue(NAME);
propertyChange(event:PropertyChangeEvent)
setPropertyValue(NAME, name); PropertyChangeEvent Object oldValue = propertyValues.get(propertyName); propertyValues.put(propertyName, value); pcSupport.firePropertyChange(propertyName, oldValue, value);
Рис. 3.19. Модернизированный механизм оповещения
Реализуем описанный механизм: для этого необходимо модифицировать методы доступа
к
свойствам
элементов
модели,
по
образцу
свойства
name
BasicModelElement. После этой операции мы имеем модель, к каждому элементу которой можно зарегистрировать объект-слушатель (listener). Рассмотрим, как это можно применить, например, в древовидном навигаторе модели. Мы имеем модель и ее представление – tree view, каждый элемент дерева соответствует элементу модели. Оба объекта (модель и ее представление) являются 68
составными: в случае модели мы имеем иерархию пространств имен (см. образец Компоновщик, раздел 2.5) дерево, в свою очередь состоит из узлов. При изменении одного элемента модели, например, его имени необходимо обновить представление. Конечно, можно перерисовать все дерево заново, но это довольно расточительно с точки зрения вычислительных ресурсов. Гораздо лучше, дать команду перерисовать конкретный узел, представляющий изменившийся элемент. Поскольку измениться может любой элемент модели, то мы должны наблюдать за всеми. С текущим механизмом оповещения мы вынуждены отдельно регистрировать наблюдателей для каждого элемента модели, что не очень хорошо, особенно, когда модель большая. Хотелось бы иметь механизм, позволяющий «одной командой» зарегистрировать наблюдателя на модель в целом. Видим, что мы находимся в контексте применимости образца проектирования Коммуникационный концентратор (см. раздел 2.4). В качестве концентратора в наблюдаемой системе логично выбрать Namespace. На Рис. 3.20 представлена структура механизма оповещения о событиях изменения. java.beans BasicModelElement
-pcSupport
PropertyChangeSupport addPropertyChangeListener(listener) removePropertyChangeListener(listener) firePropertyChange(oldValue, newValue, name)
Namespace 0..*
getOwnedElements():List addOwnedElement(ownedElement:ModelElement) removeOwnedElement(ownedElement:ModelElement) propertyChange(event : PropertyChangeEvent) addNamespaceListener(listener : NamespaceListener) removeNamespaceListener(listener : NamespaceListener)
«interface» PropertyChangeListener propertyChange(event:PropertyChangeEvent) PropertyChangeEvent
addOwnedElement(ownedElement:ModelElement) { ownedElement.addPropertyChangeListener(this); ownedElements.add(ownedElement); }
om.event «interface» NamespaceListener 0..*
namespaceContentsChanged(event:PropertyChangeEvent) PropertyChangeEvent
Рис. 3.20. Роль пространств имен в механизме оповещения
Здесь Namespace – является слушателем событий (реализует интерфейс PropertyChangeListener), поступающих от элементов, которыми он владеет (ownedElements). Концентратор на стороне системы-наблюдателя реализует интерфейс 69
NamespaceListener и подписывается у корневого элемента модели на получение всех событий (посредством метода addNamespaceListener). В методе propertyChange реализована передача события вверх по иерархии пространств имен, а также оповещение всех наблюдателей: if (this.getNamespace() != null) { this.getNamespace().propertyChange(event); } Iterator iter = namespaceListenerList.iterator(); while (iter.hasNext()) { NamespaceListener theListener = (NamespaceListener)iter.next(); theListener.namespaceContentsChanged(new NamespaceEvent(event)); }
Таким образом, для наблюдения за изменениями состояний элементов модели достаточно зарегистрировать слушателя (listener) для корневого Namespace, то есть элемента класса Model. Отметим, что возможна регистрация наблюдателей на любое пространство имен, то есть можно наблюдать за конкретным поддеревом модели, также возможно создание персональных наблюдателей для каждого элемента модели.
70
4 Тестирование и применение программы В предыдущей главе мы рассказали о разработке компоненты, поддерживающей семантику
UML 1.4.
Данная
глава
посвящена
тестированию
и
применению
реализованного продукта. В разделе «Тестирование» мы рассказываем о проверке работы механизма оповещения, а в разделе «Применение» – о реальном приложении основанном, на нашей модели.
4.1 Тестирование Тестирование является одним из важнейших столпов, на которых держится качественный программный продукт. За время существования программной индустрии было выработано множество различных подходов к тестированию. Разработан целый класс инструментов, предназначенных для систематизации и автоматизации процесса тестирования. Традиционный подход к тестированию заключается в том, что разработчик приводит программу в более-менее рабочее состояние, после чего продукт передается в отдел QA
18
, где тестеры каким-либо образом (создают представительный набор
автоматизированных тестов, выполняют тестирование вручную) проверяют программу на удовлетворение функциональной спецификации и т.п. Тестеры отмечают найденные ошибки, и разработчик приступает к их устранению, после чего цикл повторяется. Существуют различные нормы и правила организации процесса тестирования, в частности, есть такие, которые требуют полной независимости тестера от разработчика. В последние годы хорошо себя зарекомендовала методология XP
19
, среди
основополагающих практик, рекомендуемых авторами этой методологии, есть Test-Driven Development (разработка, ведомая тестами). Идея состоит в том, что сначала пишется тест, который незамедлительно выполняется. В результате разработчик видит, что тест не проходит (fail), после чего добивается его прохода (pass), тем самым, реализует
18
Аббревиатура Quality Assurance – «гарантия качества»
19
XP – аббревиатура eXtreme Programming – «экстремальное программирование». Первую книгу по
этой методологии написал Кент Бек. XP продолжает набирать популярность, «домашней страницей» этой методологии принято считать www.xprogramming.org.
71
требуемую функциональность. Каждый класс (модуль) имеет соответствующий тест, причем эти тесты регулярно выполняются при каждом изменении кода. Таким образом, в XP ответственность за тестирование в большей степени ложится на разработчика. Тесты модулей служат своеобразной формой документации, причем у них есть, по меньшей мере, одно неоспоримое преимущество: в отличие, от комментариев и проектных документов, они всегда «говорят правду». Кент Бек и Эрих Гамма разработали специальный каркас для написания модульных тестов – JUnit. В настоящее время JUnit является открытым проектом 20 , многие современные среды разработки интегрированы с JUnit (Together, JBulder, IDEA). На Рис. 4.1 приведена диаграмма основных классов каркаса JUnit, отметим, что представленная структура – пример реализации образца проектирования Компоновщик (см. раздел 2.5) Действительно, интерфейс Test выступает в роли компонента, TestCase – в роли листа, а TestSuite – в роли составного объекта. TestCase предоставляет методы для установки условий (среды) тестирования (setUp), запуска теста, получения результата (TestResult), а также восстановления состояния среды (tearDown). TestSuite хранит множество тестов, которые следует выполнить. В состав каркаса также входят две стандартных программы, предназначенных для запуска тестов (test runners) – с графическим и консольным интерфейсами [28]. «interface» Test *
run(:TestResult)
TestCase TestResult
TestSuite
run(result:TestResult) runTest() setUp() tearDown() getName() : String
run(result : TestResult) addTest(test :Test)
Рис. 4.1. Основные классы JUnit
Мы воспользуемся JUnit для тестирования механизма оповещения о событиях изменения
состояния.
Возьмем,
например,
модель,
приведенную
на
Рис.
4.2,
зарегистрируем слушателя на всю модель, затем произведем изменения в элементах модели и проверим доставку событий до наблюдателя. Поступим следующим образом:
20
Последняя информация о проекте расположена по адресу - www.junit.org
72
заведем
класс
TestEventNotificationSystem
(унаследованный
от
класса
TestCase), который реализует интерфейс NamespaceListener. В setUp зададим модель и подпишемся на события, в методе-получателе событий произведем проверку доставленных событий (простое сравнение с ожидаемыми значениями, с помощью метода assertEquals), остается инициировать генерацию событий, сделаем это в методе-тесте, путем установки isActive=true элемента ClassA. Полный код данного теста приведен в приложении 3. The model The package
ClassA
ClassB
Рис. 4.2. Пример модели
Заметим, что поскольку основа реализации механизма оповещения сосредоточена в BasicModelElement и в Namespace, а поддержка этого механизма со стороны других классов модели минимальна – однострочные модификаторы доступа к атрибутам. То для того, чтобы убедиться в корректности работы механизма оповещения, практически достаточно такого, далеко неполного (в смысле покрытия кода) теста. Остается очевидным образом проверить корректность работы для различного числа наблюдателей (один/несколько), и для корней поддеревьев модели.
4.2 Применение Откроем небольшой секрет – когда мы начинали разрабатывать эту систему, мы уже знали, что у нее будет, по крайней мере, одно реальное приложение. А именно – верификатор
модели
UML
на
соответствие
правилам
непротиворечивости,
разрабатываемый параллельно Н. Андреевым [33]. Правила непротиворечивости (well-formedness rules) определены в стандарте UML, в разделе описания семантики языка [10]. В процессе разработки нашей модели были учтены некоторые специфические потребности верификатора модели. В частности, в стандарте
UML
определены
дополнительные 73
операции
(additional
operations),
используемые в правилах, анализ этих операций показал, что подавляющее большинство из них может быть реализовано через внешний интерфейс модели. Поэтому мы не стали брать на себя ответственность за их реализацию. Тем не менее, расширение функциональности все же потребовалась. Для описания правил используется язык OCL (Object Constraint Language), в этом языке есть операция allInstances, возвращающая все экземпляры данного типа (класса). Эта операция не реализуется эффективно с помощью существующего интерфейса модели. Поэтому мы добавим непосредственную поддержку в нашу модель. Для этого заведем специальную таблицу с глобальной точкой доступа, в которой будем хранить пары: «класс» – «список экземпляров» (Рис. 4.3), здесь использован стереотип utility, потому что все методы данного класса являются статическими. По запросу allInstances с параметром, определяющим класс, возвращается конкретный список. Итак, у нас есть хранилище, остается обеспечить его своевременное пополнение. Для этого в конструкторе BasicModelElement добавим пополнение таблицы: при создании любого элемента модели происходит автоматическая запись ссылки на него в соответствующий список в таблице. «utility» AllInstancesMap - map : Map allInstances(: java.lang.Class) : List allInstances() : Map put(: java.lang.Class, allInstances : List)
Рис. 4.3. Класс-хранилище экземпляров
Правила непротиворечивости привязаны к конкретным элементам модели (Class, Association, ModelElement и т.п.) В данном приложении (верификаторе) производится проверка всех правил, причем модели загружаются из XMI. А поскольку в метамодели практически нет элементов, для которых не были бы определены правила непротиворечивости, то верификатор покрывает практически 100% кода нашей модели. Таким образом, корректность модели и загрузчика XMI была дополнительно проверена при разработке верификатора. Такой
подход
к
тестированию
программного
продукта
хорошо
себя
зарекомендовал в системах с открытым для модификации и использования кодом (open source software). Действительно, немаловажную роль в достижении высокого качества продуктов этого класса играют пользователи, которые начинают использовать продукт (будь то в качестве конечных потребителей или строя самостоятельный продукт на его 74
основе) на самых ранних стадиях. Наличие надежной и быстрой обратной связи с разработчиком приложения, основанного на нашей модели, позволило своевременно обнаружить и исправить допущенные ошибки.
75
5 Вопросы охраны труда и эргономики Для решения задач системного программирования необходимо использование вычислительной техники. Практика работы с вычислительной техникой показывает, что наиболее эффективной работы и улучшения условий ее эксплуатации можно добиться при одновременном использовании большого числа электронно-вычислительных машин, сосредоточенных в вычислительном центре (ВЦ), и при тесном сотрудничестве специалистов различного профиля. Критериями выбора оптимального варианта организации работы в ВЦ является техническая эффективность и соответствие требованиям эргономики и охраны труда. Эргономикой изучаются возможности и особенности деятельности человека в процессе труда с целью создания таких условий, методов и организаций трудовой деятельности, которые делают трудовой процесс наиболее производительным и вместе с тем обеспечивают безопасность и удобство работающему, сохраняют его здоровье и работоспособность.
Общая характеристика условий труда Разработка программного продукта производится на рабочем месте программиста, т.е. в помещении, оснащенном соответствующим для этого оборудованием. К данному оборудованию относится компьютер класса Pentium 4. К используемым материалам относятся: магнитные и лазерные диски, на которые копируется информация. Все рабочие места не реже двух раз в пять лет подлежат аттестации. По результатам аттестации рабочие места подразделяются на три группы: •
аттестованные – рабочие места, показатели которых полностью соответствуют предъявленным требованиям;
•
подлежащие рационализации – рабочие места, несоответствующие показатели которых могут быть доведены до уровня этих требований в процессе рационализации;
•
подлежащие ликвидации – рабочие места, показатели которых не соответствуют и не могут быть доведены до уровня установленных требований [34]. 76
В результате проведенной аттестации рабочего места программиста имеем следующие данные по условиям труда. Микроклимат производственного помещения определяется температурой (°С), относительной влажностью (%), скоростью движения воздуха (м/с). Согласно [34] к категории 1а относятся работы, производимые сидя и не требующие физического напряжения, при которых расход энергии составляет до 120 ккал/ч; к категории 1б относятся
работы,
производимые
сидя,
стоя
или
связанные
с
ходьбой
и
сопровождающиеся некоторым физическим напряжением, при которых расход энергии составляет от 120 до 150 ккал/ч. Разработка программного продукта, производимая на данном рабочем месте, относится по величине энергозатрат к категории 1а.
Эргономические требования Рассмотрим более подробно требования эргономики, предъявляемые к рабочему месту оператора при работе с видеотерминалами (ВТ). Под рабочим местом понимается зона, оснащенная необходимыми техническими средствами, в которой совершается трудовая деятельность исполнителя или группы исполнителей, совместно выполняющих одну работу или операцию. Организацией рабочего места называется система мероприятий по оснащению рабочего места средствами и предметами труда и их размещению в определенном порядке. В соответствии с требованиями эргономики, рабочее место должно быть приспособлено для конкретного вида деятельности и для работников определенной квалификации с учетом их физических и психических возможностей и особенностей. Конструкция рабочего места должна обеспечивать быстроту, безопасность, простоту и экономичность технического обслуживания в нормальных и аварийных условиях; полностью отвечать функциональным требованиям и предполагаемым условиям эксплуатации. При конструировании производственного оборудования необходимо предусматривать возможность регулирования отдельных его элементов с тем, чтобы обеспечивать оптимальное положение работающего. При организации рабочего места учитываются антропометрические данные программистов, а также предусматривается соответствующее размещение элементов оборудования в зависимости от характера выполняемой работы. При размещении ВТ на рабочем месте учитываются границы полей зрения программиста, которые определяются положением глаз и головы. Различают зоны зрительного наблюдения в вертикальной плоскости, ограниченные определенными 77
углами, в которых располагают экран ВТ (45°-60°), пюпитр (35°-45°) и клавиатура. При периодическом
наблюдении
за
экраном
рекомендуется
располагать
элементы
оборудования так, чтобы экран находился справа, клавиатура – напротив правого плеча, а документы – в центре угла обзора. При постоянной работе экран должен быть расположен в центре поля обзора, документы – слева на столе или специальной подставке [38]. Рабочий стол должен иметь стабильную конструкцию. Плоскость стола выбирается в зависимости от размера документов. При больших размерах документов она должна быть 160 x 90 см. Плоскость стола, а также сидение программиста должны регулироваться по высоте. Высоту плоскости стола необходимо регулировать в диапазоне 65-85 или 68-84 см. При этом высота от горизонтальной линии зрения до рабочей поверхности стола при выпрямленной рабочей позе должна быть 40-50 см. Высота сидения от пола должна регулироваться в пределах 42-55 см. По желанию программиста может быть установлена подставка для ног размером 40x30x15 см и углом наклона 0-20° с нескользящим покрытием и неперемещаемая по полу (Табл. 5.1). 150 155 59/22 57/18 58/21 56/17 57/20 55/16 56/19 54/15 55/18 53/14 54/17 52/13 53/16 51/12 52/15 50/11 51/14 49/10 50/13 48/9 49/12 47/8 48/11 46/7 47/10 45/6 46/9 44/5 45/8 43/4
160 55/14 54/13 53/12 52/11 51/10 50/9 49/8 48/7 47/6 46/5 45/4 44/3 43/2 42/1 41/0
165 53/10 52/9 51/8 50/7 49/6 48/5 47/4 46/3 45/2 44/1 43/0 42/0 41/0 40/0 39/0
170 51/6 50/5 49/4 48/3 47/2 46/1 45/0 44/0 43/0 42/0 41/0 40/0 39/0 38/0 37/0
175 49/2 48/1 47/0 46/0 45/0 44/0 43/0 42/0 41/0 40/0 39/0 38/0 37/0 36/0 35/0
180 47/0 46/0 45/0 44/0 43/0 42/0 41/0 40/0 39/0 38/0 37/0 36/0 35/0 34/0 33/0
Табл. 5.1. Оптимальная высота сидения и подставки для ног в зависимости от роста человека (см).
Покрытие стола должно быть матовым (с коэффициентом отражения 20-50%) и легко чиститься; углы и передняя грань доски должны быть закругленными. Высота пространства под столом для ног рекомендуется порядка 60 см на уровне колен и не менее 80 см на уровне ступней. Тип рабочего кресла выбирается в зависимости от продолжительности работы. При длительной работе кресло должно быть массивным, при кратковременной – легкой конструкции, свободно отодвигающееся. 78
Сидение должно быть удобным, иметь закругленные края, наклоняться по отношению к горизонтали вперед на 2° и назад на 14°. Его размеры не должны превышать 40x40 см. Кроме того, сидение должно быть покрыто латексом толщиной около 1 см, сверху которого накладывается влагонепроницаемый материал (меланжевая ткань, натуральное волокно). Высота спинки кресла рекомендуется 48-50 см от поверхности сидения и с регулировкой в передне-заднем направлении. На высоте 10-20 см от сидения спинка должна быть оборудована поясничным опорным валиком. Подлокотники рекомендуются лишь при эпизодической работе с ВТ, при постоянной работе подлокотники ограничивают движение, а, следовательно, в этой ситуации креслом с подлокотниками пользоваться не рекомендуется [38].
Микроклиматические условия Устанавливаемые ГОСТ12.1.005-88 и СанПиН 2.2.2.542-96 оптимальные нормы температуры (23-25°С) и относительной влажности (60-40%) для теплого периода года, а также (22-24 °С) и (60-40 %) соответственно для холодного и переходного, скорости движения воздуха соблюдаются благодаря наличию системы кондиционирования, работающей в режиме автоматического регулирования. Скорость движения воздуха не превышает допустимого значения (