Диаграмма классов: крупным планом

 















Диаграмма классов: крупным планом


Аннотация


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


1. Как класс изображается на диаграмме UML?


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

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


Рис. 1.1.

2. А что внутри?


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

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

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

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


Таблица 1.

Символ Значение +public - открытый доступ-private - только из операций того же класса#protected - только из операций этого же класса и классов, создаваемых на его основе

Рассмотренный ранее пример с телевизором средствами UML (конечно же, это очень высокоуровневая абстракция) можно изобразить так (рис.1.2):


Рис. 1.2.


Не правда ли, все понятно и предельно просто? Зачем, например, пользователю знать числовые значения частот каналов? Он знает, что достаточно запустить процедуру автоматического поиска каналов и телевизор все сделает за него. Вот вам и инкапсуляция - оказывается, она повсюду вокруг нас. Оглянитесь и подумайте, сколько вещей вокруг имеют скрытые свойства и выполняют скрытые операции. Испугались? Вот то-то же!


. Как использовать объекты класса?


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

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

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

Кстати, посмотрите внимательнее на пульт ДУ или на экран программы удаленного контроля. Что вы видите - кнопки? Или кнопки, сгруппированные по функциональному признаку? Да, именно так: кнопки, переключающие каналы, расположены отдельно, рядом - группа кнопок, отвечающих за регулировку громкости звука, рядом - группа программируемых кнопок, и т. д. В принципе, можно сказать, что пульт реализует не один, а несколько интерфейсов - по числу функциональных групп кнопок. Впрочем, это уже формализм: мы просто хотели проиллюстрировать слова "логическая группа" в определении интерфейса.

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

Интерфейс всегда реализуется некоторым классом, который в таком случае называют классом, поддерживающим интерфейс. Как мы уже говорили ранее, один и тот же объект может иметь несколько интерфейсов. Это означает, что класс этого объекта реализует все операции этих интерфейсов. К данному моменту в голове читателя может созреть вопрос: "Мы же, вроде бы, говорили о классах и объектах, а теперь вдруг перешли на интерфейсы. Да и вообще, используются ли они в практике программирования или являются просто изящной теоретической конструкцией?". Ответ на этот вопрос прост: многие из существующих технологий программирования (например, COM, CORBA, Java Beans) не только активно используют механизм интерфейсов, но и, по сути, полностью основаны на нем.

Что ж, наверное, пришло время поговорить о том, как интерфейс изображается на диаграммах. Изображаться он может несколькими способами. Первый и самый простой из них - это класс со стереотипом <<interface>> (рис. 1.3):


Рис. 1.3.


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


Рис. 1.4.


Обратите внимание на маленький значок на закладке папки ConduitSet. Это обозначение подсистемы, мы могли бы не рисовать его, а просто использовать стереотип <<subsystem>>. Впрочем, об этом мы еще поговорим.

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


Рис. 13.5.


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

Действительно, на диаграммах довольно часто можно увидеть такую картинку (рис. 1.6):


Рис. 1.6.


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


4. Всегда ли нужно создавать новые классы?


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

инкапсуляция наследование полиморфизм класс


Рис. 1.7.


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

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

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

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

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

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

Для того чтобы научиться эффективно моделировать наследование, обратимся к классикам, а именно к Г. Бучу. Он советует проводить эту процедуру в такой последовательности:

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

2.Вынесите эти элементы в некоторый общий суперкласс, а если такого не существует, то создайте новый класс.

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


Рис. 1.8.

А вот и пример применения этого подхода (рис. 1.9):


Рис. 1.9.


На первый взгляд, кажется странным, что класс "точка" не имеет никаких атрибутов, а круг имеет только радиус. С прямоугольником, вроде бы, все понятно - ширина и высота, но вот только где он расположен в пространстве, этот прямоугольник? Давайте попробуем следовать советам Буча. Итак, положение всех трех фигур можно однозначно определить с помощью пары чисел. Для точки - это вообще единственные ее характеристики, для круга и прямоугольника - их центры (под центром прямоугольника мы понимаем точку пересечения его диагоналей). Вот они, общие атрибуты! Таким образом, мы создали суперкласс "Фигура", имеющий два атрибута - координаты центра. Все остальные классы на этой диаграмме связаны с классом "Фигура" отношением обобщения, т. е. в них нужно доопределить только "недостающие" атрибуты - радиус, ширину и высоту. Атрибуты, описывающие координаты центра, эти классы имеют изначально как потомки класса "Фигура" - они их наследуют. Заметим, что операции классов мы тут не рассматриваем: понятно, что с ними была бы та же история.

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

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

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

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

Отношения между классами

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

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


Рис. 1.10.


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

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


Рис. 1.11.


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


Рис. 1.12.


И насчет ролей, и насчет кратности на этой диаграмме все понятно - человек может вообще не работать, работать в одной или более компаниях, а вот компании в любом случае нужен хотя бы один сотрудник. Кстати, о кратности. Ассоциация может объединять три и более класса. В этом случае она называется n-арной и изображается ромбом на пересечении линий, как показано на этой диаграмме, позаимствованной нами из Zicom Mentor (рис. 1.13):


Рис. 1.13.


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


Рис. 1.14.


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

И, наконец, еще одна важная вещь, касающаяся ассоциации. В отношении между двумя классами сама ассоциация тоже может иметь свойства и, следовательно, тоже может быть представлена в виде класса. Пример прост (рис. 1.15):


Рис. 1.15.


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

А вот более сложный, но, опять-таки, взятый из реальной жизни пример моделирования отношений между классами, позаимствованный нами из Zicom Mentor (рис. 1.16):


Рис. 1.16.

И наконец, доказательство того, что UML можно использовать для чего угодно, в том числе и для записи сказок: диаграмма, описывающая предметную область сказки о Курочке Рябе и взятая с сайта конкурса шуток на UML (рис. 1.17):


Рис. 1.17.


Узнаете рассказ, знакомый с детства?


Выводы


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

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

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

Инкапсуляция, наследование и полиморфизм - три кита, на которых держится ООП.

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

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

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

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


Диаграмма классов: крупным планом Аннотация Диаграмма классов - один из наиболее часто исп

Больше работ по теме:

КОНТАКТНЫЙ EMAIL: [email protected]

Скачать реферат © 2017 | Пользовательское соглашение

Скачать      Реферат

ПРОФЕССИОНАЛЬНАЯ ПОМОЩЬ СТУДЕНТАМ