Отношения классов — от UML к коду
Введение
Диаграмма классов UML позволяет обозначать отношения между классами и их экземплярами. Для чего они нужны? Они нужны, например, для моделирования прикладной области. Но как отношения отражаются в программном коде? Данное небольшое исследование пытается ответить на этот вопрос — показать эти отношения в коде.
Сначала попробуем прояснить, как относятся друг к другу отношения между классами в UML. Используя различные источники удалось построить следующую структурную схему, демонстрирующую разновидности отношений:
Рис. 1 — Отношения между классами
Ассоциации имеют навигацию: двунаправленную или однонаправленную, указывающую на направление связи. То есть у каждого вида ассоциации еще есть два подвида, которое на рисунке не показаны.
1. Обобщение
Итак, наша цель — построить UML-диаграмму классов (Class Model), а затем отразить ее в объектно-ориентированном коде.
В качестве прикладной области возьмем отдел кадров некого предприятия и начнем строить его модель. Для примеров будем использовать язык Java.
Отношение обобщения — это наследование. Это отношение хорошо рассматривается в каждом учебнике какому-либо ООП языку. В языке Java имеет явную реализацию через расширение(extends) одного класса другим.
Рис. 2 — Отношение обобщения
Класс «Man»(человек) — более абстрактный, а «Employee»(сотрудник) более специализированный. Класс «Employee» наследует свойства и методы «Man».
Попробуем написать код для этой диаграммы:
2. Ассоциация
2.1 Бинарная
В модель добавили класс «IdCard», представляющий идентификационную карточку(пропуск) сотрудника. Каждому сотруднику может соответствовать только одна идентификационная карточка, мощность связи 1 к 1.
Рис. 3 — Бинарная ассоциация
В теле программы создаем объекты и связываем их:
Класс Employee имеет поле card, у которого тип IdCard, так же класс имеет методы для присваивания значения(setIdCard) этому полю и для
получения значения(getIdCard). Из экземпляра объекта Employee мы можем узнать о связанном с ним объектом типа IdCard, значит
навигация (стрелочка на линии) направлена от Employee к IdCard.
2.2 N-арная ассоциация
Представим, что в организации положено закреплять за работниками помещения. Добавляем новый класс Room.
Каждому объекты работник(Employee) может соответствовать несколько рабочих помещений. Мощность связи один-ко-многим.
Навигация от Employee к Room.
Рис. 4 — N-арная ассоциация
Теперь попробуем отразить это в коде. Новый класс Room:
Добавим в класс Employee поле и методы для работы с Room:
2.3 Агрегация
Введем в модель класс Department(отдел) — наше предприятие структурировано по отделам. В каждом отделе может работать один или более человек. Можно сказать, что отдел включает в себя одного или более сотрудников и таким образом их агрегирует. На предприятии могут быть сотрудники, которые не принадлежат ни одному отделу, например, директор предприятия.
Рис. 5 — Агрегация
Итак, наш класс, помимо конструктора и метода изменения имени отдела, имеет методы для занесения в отдел нового сотрудника, для удаления сотрудника и для получения всех сотрудников входящих в данный отдел. Навигация на диаграмме не показана, значит она является двунаправленной: от объекта типа «Department» можно узнать о сотруднике и от объекта типа «Employee» можно узнать к какому отделу он относится.
Так как нам нужно легко узнавать какому отделу относится какой-либо сотрудник, то добавим в класс Employee поле и методы для назначения и получения отдела.
2.3.1 Композиция
Предположим, что одним из требований к нашей системе является требование о том, чтоб хранить данные о прежней занимаемой должности на предприятии.
Введем новый класс «pastPosition». В него, помимо свойства «имя»(name), введем и свойство «department», которое свяжет его с классом «Department».
Данные о прошлых занимаемых должностях являются частью данных о сотруднике, таким образом между ними связь целое-часть и в то же время, данные о прошлых должностях не могут существовать без объекта типа «Employee». Уничтожение объекта «Employee» должно привести к уничтожению объектов «pastPosition».
Рис. 6 — Композиция
В класс Employee добавим свойства и методы для работы с данными о прошлой должности:
3. Зависимость
Для организации диалога с пользователем введем в систему класс «Menu». Встроим один метод «showEmployees», который показывает список сотрудников и их должности. Параметром для метода является массив объектов «Employee». Таким образом, изменения внесенные в класс «Employee» могут потребовать и изменения класса «Menu».
Рис. 7 — Зависимость
Заметим, что класс «Menu» не относится к прикладной области, а представляет собой «системный» класс воображаемого приложения.
Класс «Menu»:
4. Реализация
Реализация, как и наследование имеет явное выражение в языке Java: объявление интерфейса и возможность его реализации каким-либо классом.
Для демонстрации отношения «реализация» создадим интерфейс «Unit». Если представить, что организация может делиться не только на отделы, а например, на цеха, филиалы и т.д. Интерфейс «Unit» представляет собой самую абстрактную единицу деления. В каждой единице деления работает какое-то количество сотрудников, поэтому метод для получения количества работающих людей будет актуален для каждого класса реализующего интерфейс «Unit».
Рис. 8 — Реализация
Реализация в классе «Department»:
Как видим, реализация метода «getPersonCount» не совсем актуальна для класса «Department», так как он имеет метод «getEmployees», который возвращает
коллекцию объектов «Employee».
Выводы
Язык моделирования UML имеет набор отношений для построения модели классов, но даже такой развитой ООП язык, как Java имеет только две явные конструкции для отражения связей: extends(расширение) и interface/implements(реализация).
В результате моделирования получили следующую диаграмму:
Рис. 8 — Диаграмма классов
Литература
1) Г. Буч, Д. Рамбо, А. Джекобсон. Язык UML Руководство пользователя.
3) Эккель Б. Философия Java. Библиотека программиста. — СПб: Питер, 2001. — 880 с.
4) Орлов С. Технологии разработки программного обеспечения: Учебник. — СПб: Питер, 2002. — 464 с.
5) Мухортов В.В., Рылов В.Ю.Объектно-ориентированное программирование, анализ и дизайн. Методическое пособие. — Новосибирск, 2002.
Наследование, композиция, агрегация
Нередко случается, что решив разобраться с какой-то новой темой, понятием, инструментом программирования, я читаю одну за другой статьи на различных сайтах в интернете. И, если тема сложная, то эти статьи могут не на шаг не приблизить меня к понимаю. И вдруг встречается статья, которая моментально дает озарение и все паззлы складываются воедино. Трудно определить, что отличает такую статью от других. Правильно подобранные слова, оптимальная логика изложения или же просто более релевантный пример. Я не претендую на то, что моя статься окажется новым словом в C# или же лучшей обучающей статьей. Но, возможно для кого-то она станет именно той, которая позволит разобраться, запомнить и начать правильно применять те понятия, о которых пойдет речь.
В объектно-ориентированных языках программирования существует три способа организации взаимодействия между классами. Наследование — это когда класс-наследник имеет все поля и методы родительского класса, и, как правило, добавляет какой-то новый функционал или/и поля. Наследование описывается словом «является». Легковой автомобиль является автомобилем. Вполне естественно, если он будет его наследником.
Ассоциация – это когда один класс включает в себя другой класс в качестве одного из полей. Ассоциация описывается словом «имеет». Автомобиль имеет двигатель. Вполне естественно, что он не будет являться наследником двигателя (хотя такая архитектура тоже возможна в некоторых ситуациях).
Выделяют два частных случая ассоциации: композицию и агрегацию.
Композиция – это когда двигатель не существует отдельно от автомобиля. Он создается при создании автомобиля и полностью управляется автомобилем. В типичном примере, экземпляр двигателя будет создаваться в конструкторе автомобиля.
Агрегация – это когда экземпляр двигателя создается где-то в другом месте кода, и передается в конструктор автомобиля в качестве параметра.
Хотя ведутся дискуссии о преимуществах того или иного способа организации взаимодействия между классами, какого-либо абстрактного правила не существует. Разработчик выбирает тот или иной путь основываясь на элементарной логике (“является” или “имеет”), но также принимает во внимание возможности и ограничения, которые дают и накладывают эти способы. Для того, чтобы увидеть эти возможности и ограничения, я попытался написать пример. Достаточно простой, чтобы код оставался компактным, но и достаточно развитый, чтобы в рамках одной программы можно было применить все три способа. И, главное, я попытался сделать этот пример как можно менее абстрактным – все объекты и экземпляры понятны и осязаемы.
Напишем простенькую игру – танковый бой. Играют два танка. Они поочередно стреляют и проигрывает тот, здоровье которого упало до нуля. В игре будут различные типы снарядов и брони. Для того, чтобы нанести урон необходимо во-первых, попасть по танку противника, во-вторых, пробить его броню. Если броня не пробита, урон не наносится. Логика игры построена на принципе «камень-ножницы-бумага»: то есть броня одного типа хорошо противостоит снарядам определенного типа, но плохо держит другие снаряды. Кроме того, снаряды, которые хорошо пробивают броню, наносят малый «заброневой» урон, и, напротив, наиболее «летальные» снаряды имеют меньше шансов пробить броню.
Создадим простенький класс для пушки. Он будет иметь два приватных поля: калибр и длину ствола. От калибра зависит урон, и, частично, способность к пробитию брони. От длины ствола – точность стрельбы.
Сделаем также конструктор для пушки:
Сделаем метод для получения калибра из других классов:
Помните, что для поражения цели должно произойти две вещи: попадание в цель и пробитие брони? Так вот, пушка будет отвечать за первую из них: попадание. Поэтому делаем булевый метод IsOnTarget, который принимает случайную величину (dice) и возвращает результат: попали или нет:
Целиком класс пушки выглядит следующим образом:
Здесь мы применили агрегацию. Где-то будет создана пушка. Потом к этой пушке будут создаваться снаряды, которые имеют указатель на пушку.
Теперь сделаем разные типы снарядов, которые будут наследовать абстрактный снаряд: фугасный, кумулятивный, подкалиберный. Фугасный наносит самый большой урон, кумулятивный – меньше, подкалиберный – еще меньше. Дочерние классы не имеют полей и вызывают конструктор базового снаряда, передавая ему пушку, и строковый тип. В дочернем классе переопределяется метод GetDamage() – вносятся коэффициенты, которые увеличат или уменьшат урон по сравнению с дефолтным.
Фугасный (дефолтный урон):
Кумулятивный (дефолтный урон х 0.6):
Подкалиберный (дефолтный урон х 0.3):
Обратите внимание, что в переопределенном методе GetDamage вызывается и метод базового класса. То есть, переопределив метод, мы также сохраняем возможность обратиться к дефолтному методу, использовав ключевое слово base).
Итак, для снарядов мы применили и агрегацию (пушка в базовом классе), и наследование.
Создадим теперь броню для танка. Здесь применим только наследование. Любая броня имеет толщину. Поэтому абстрактный класс брони будет иметь поле thickness, и строковое поле type, которое будет определятся при создании дочерних классов.
Броня будет в нашей игре определять пробита они или нет. Поэтому, у нее будет лишь один метод, который будет переопределяться в дочерних, в зависимости от типа брони.
Для того, чтобы конструктор танка остался более-менее компактным, сделаем два вспомогательных приватных метода, которые добавляют три типа брони соответствующей толщины, и наполняют боеукладку 10 снарядами каждого из трех типов:
Теперь конструктор танка выглядит вот таким образом:
Пользовательский интерфейс танка состоит из трех методов: выбрать броню, зарядить пушку, выстрелить.
Как я упомянул в начале, в этом примере я старался максимально уйти от абстрактных понятий, которые нужно все время держать в голове. Поэтому каждый экземпляр снаряда у нас равен физическому снаряду, который положили в боеукладку перед боем. Следовательно, снаряды могут закончится в самый неподходящий момент!
Этот интерфейс требует реализации метода Clone(). Вот она:
Теперь все супер реалистично: при выстреле генерируется dice, пушка рассчитывает попадание своим методом IsOnTarget, и, если попадание есть, то метод Shoot вернет экземпляр снаряда, а если промах – то вернет null.
Последний метод танка – его поведение при попадании вражеского снаряда:
Все готово. Остается только написать консольный (или неконсольный) вывод, в котором будет обеспечен пользовательский интерфейс и в цикле реализованы поочередные ходы игроков.
Подведем итоги. Мы написали программу, в которой использовали наследование, композицию и агрегацию, надеюсь, поняли и запомнили различия. Активно задействовали возможности полиморфизма, во-первых, когда любые экземпляры дочерних классов можно сложить в список, имеющий тип данных родительского, а во-вторых, создавая методы, которые принимают в качестве параметра родительский экземпляр, но внутри которых вызываются методы дочернего. По ходу текста я упоминал возможные альтернативные реализации – замену наследования на агрегацию, и, универсального рецепта тут нет. В нашей реализации наследование дало нам легкость добавления новых деталей в игру. Например, чтобы добавить новый тип снаряда нам нужно лишь:
Ниже – приведена диаграмма наших классов.
В финальном коде игры все «магические числа», которые использовались в тексте, вынесены в отдельный статический класс Config. К публичным полям статического класса мы можем обратиться из любого фрагмента нашего кода и его экземпляр не нужно (и невозможно) создавать. Вот так он выглядит:
BestProg
C++. Типы отношений между классами: is-a, has-a, uses. Примеры
Содержание
Поиск на других ресурсах:
1. Типы отношений между классами. Классификация
Между классами возможны два типа отношений:
2. Пример простейшего типа отношения is-a (наследование)
В классе Circle реализованы следующие элементы:
В производном классе CircleColor реализованы поля и методы, которые дополняют класс Color :
Демонстрационный текст программы на C++ следующий:
Результат выполнения программы
3. Примеры отношения между классами типа has-a
При отношении has-a класс содержит один или несколько объектов (экземпляров) другого класса. Существуют два вида отношения has-a:
3.1. Примеры агрегации для типа отношения has-a
В случае агрегации класс содержит множество (один или несколько) объектов других классов, которые не являются составной частью этого класса (не содержат код, который дополняет работу самого класса).
3.1.1. Сокращенный пример. Класс Figures содержит экземпляры классов Triangle (треугольник) и Circle (круг)
3.1.2. Подробный пример. Класс BusStation (Автостанция) содержит массивы экземпляров классов Bus (автобус) и Car (автомобиль)
В примере класс BusStation (Автостанция) содержит массивы экземпляров классов Bus (Автобус), Car (Автомобиль). Количество элементов в массивах классов Bus и Car может изменяться. Даже, если на автостанции в данный момент времени не будет ни одного автобуса или автомобиля, автостанция будет функционировать. Это есть агрегация.
Результат работы программы:
3.2. Примеры композиции для типа отношения has-a
3.2.1. Упрощенный пример. Класс Car (автомобиль), содержащий экземпляры классов Vehicle (Двигатель) и Wheel (Колесо)
3.2.2. Упрощенный пример. Класс Bike (Велосипед) содержит экземпляры классов Wheel (Колесо) и Saddle (седло)
Еще один пример композиции. Класс Bike (Велосипед) содержит экземпляры классов Wheel (Колесо) и Saddle (седло), которые являются его составной частью. Велосипед не может быть без колес или седла, поэтому это есть композиция.
Programming stuff
Страницы
понедельник, 3 декабря 2012 г.
Наследование vs Композиция vs Агрегация
Между двумя классами/объектами существует разные типы отношений. Самым базовым типом отношений является ассоциация (association), это означает, что два класса как-то связаны между собой, и мы пока не знаем точно, в чем эта связь выражена и собираемся уточнить ее в будущем. Обычно это отношение используется на ранних этапах дизайна, чтобы показать, что зависимость между классами существует, и двигаться дальше.
Рисунок 1. Отношение ассоциации
Более точным типом отношений является отношение открытого наследования (отношение «является», IS A Relationship), которое говорит, что все, что справедливо для базового класса справедливо и для его наследника. Именно с его помощью мы получаем полиморфное поведение, абстрагируемся от конкретной реализации классов, имея дело лишь с абстракциями (интерфейсами или базовыми классами) и не обращаем внимание на детали реализации.
Рисунок 2. Отношение наследование
И хотя наследование является отличным инструментом в руках любого ОО-программиста, его явно недостаточно для решения всех типов задач. Во-первых, далеко не все отношения между классами определяются отношением «является», а во-вторых, наследование является самой сильной связью между двумя классами, которую невозможно разорвать во время исполнения (это отношение является статическим и, в строготипизированных языках определяется во время компиляции).
В этом случае нам на помощь приходит другая пара отношений: композиция (composition) и агрегация (aggregation). Оба они моделируют отношение «является частью» (HAS-A Relationship) и обычно выражаются в том, что класс целого содержит поля (или свойства) своих составных частей. Грань между ними достаточно тонкая, но важная, особенно в контексте управления зависимостями.
Рисунок 3. Отношение композиции и агрегации
HINT
Пара моментов, чтобы легче запомнить визуальную нотацию: (1) ромбик всегда находится со стороны целого, а простая линия со стороны составной части; (2) закрашенный ромб означает более сильную связь – композицию, незакрашенный ромб показывает более слабую связь – агрегацию.
Разница между композицией и агрегацией заключается в том, что в случае композиции целое явно контролирует время жизни своей составной части (часть не существует без целого), а в случае агрегации целое хоть и содержит свою составную часть, время их жизни не связано (например, составная часть передается через параметры конструктора).
CompositeCustomService для управления своими составными частями использует композицию, а AggregatedCustomService – агрегацию. При этом явный контроль времени жизни обычно приводит к более высокой связанности между целым и частью, поскольку используется конкретный тип, тесно связывающий участников между собой.
С одной стороны, такая жесткая связь может не являться чем-то плохим, особенно когда зависимость является стабильной (см. раздел «Стабильные и изменчивые зависимости» в прошлой заметке). С другой стороны мы можем использовать композицию и контролировать время жизни объекта, не завязываясь на конкретные типы. Например, с помощью абстрактной фабрики:
В данном случае мы не избавляемся от композиции (CustomService все еще контролирует время жизни AbstractRepository), но делает это не напрямую, а с помощью дополнительной абстракции – абстрактной фабрики. Поскольку такой подход требует удвоения количества классов наших зависимостей, то его стоит использовать, когда явный контроль времени жизни является необходимым условием.
Интересной особенностью разных отношений между классами является то, что логичность их использования может зависеть от точки зрения проектировщика, от того, с какой стороны он смотрит на задачу и какие вопросы он задает себе при ее анализе. Именно поэтому одну и ту же задачу можно решить десятком разных способов, при этом в одном случае мы получим сильно связанный дизайн с большим количеством наследования и композиции, а в другом случае – эта же задача будет разбита на более автономные строительные блоки, объединяемые между собой с помощью агрегации.
Например, нашу задачу с сервисами и репозитариями можно решить множеством разных способов. Кто-то скажет, что здесь подойдет наследование и сделает SqlCustomService наследником от AbstractCustomService; другой скажет, что этот подход неверен, поскольку CustomService у нас один, а иерархия должна быть у репозитариев.
Рисунок 4. Наследование vs Агрегация
Каждый вариант приводит к одному и тому же конечному результату, при этом связанность изменяется от очень высокой (при наследовании) к очень слабой (при агрегации).
Заключение
Существует несколько достаточно объективных критериев для определения связности дизайна по диаграмме классов: большие иерархии наследования (глубокие или широкие иерархии), и повсеместное использование композиции, а не агрегации скорее всего говорит о сильно связанном дизайне.
Большое количество наследования говорит о том, что проектировщики забыли о старом добром совете Банды Четырех, который сводится к тому, что следует предпочесть агрегацию наследованию, поскольку первая дает большую гибкость и динамичность во время исполнения.
Обилие же композиции говорит о нарушении Принципа Инверсии Зависимостей, сформулированном Бобом Мартином, которую сейчас можно выразить в терминах агрегации и композиции: предпочитайте агрегацию вместо композиции, поскольку первая стимулирует использование абстракций, а не конкретных классов.
В следующий раз: перейдем к рассмотрению конкретных DI паттернов и начнем с самого популярного из них – с Constructor Injection.
Просто о композиции, агрегации и ассоциации в JavaScript
Данная небольшая заметка о типах отношений между объектами в ООП. Можно использовать как шпаргалку, если кто знал, но подзабыл или, если для кого эти термины будут в новинку используйте как первый шаг в изучение основ ООП.
Композиция, агрегация и ассоциация — все эти термины или точнее парадигмы ООП про отношения между объектами или классами между собой. Всего существует пять основных типов отношений:
— Ассоциация
— Агрегарция
— Композиция
и еще два типа, которые в этой статье рассматривать не будем:
— Наследования (иногда этот тип еще называют генерализацией)
— Реализация (это тип отношений базируется на интерфейсах. То есть создается интерфейсы, которые основной класс должен реализовать.)
Композиция, агрегация и ассоциация эти три понятия очень похожи друг на друга. Все они означают что внутри одного объекта будет существовать другой объект.
Самый простой способ понять эти термины это использовать аналогию из реального мира. Представим себе что у нас есть класс комната и есть два других класса мебель и стена. Мы можем сказать что у комнаты будет какая та мебель и какие то стены. То есть объект комната может использоваться объекты стены и мебель по мере необходимости. Но есть разница в отношения комната — стены и комната — мебель. Разница в том что стены никогда не выйдут из объекта комната. Стены не могут существовать вне комнаты. То есть стена всегда будет создаваться внутри объекта комната. Такая связь называется композиция. И эта связь будет жесткой. Зато мебель очень легко представить за пределами комнаты. Один экземпляр мебели может принадлежать с начало одной комнате потом другой. Такая связь называется ассоциацией или агрегацией. И такая связь будет более гибкой. О различие между ассоциацией или агрегацией чуть позже.
Рассмотрим эти связи подробнее.
Ассоциация
Ассоциация это такой тип при котором объекты будут ссылаться друг на друга. При этом они остаются полностью независимыми друг от друга.
Пример реализации ассоциации
Тут у нас есть атрибут stream, в котором должна быть ссылка на поток который куда то будет отправлять сообщение. Метод log проверяет что в классе задан stream и использует его для отправки сообщения. То есть ассоциация это когда у одного класса есть ссылка на другой класс. Это создает большую гибкость в отношениях между классами. Например у нас есть возможность смены потока во время выполнения программы.
Агрегация
Агрегация это тип отношений когда один объект является частью другого. Агрегация образует слабую связь между объектами. Все зависимые классы инициализируются вне основного объекта.
Пример реализации агрегации:
При агрегации у нас получается более прочная связь, чем при ассоциации. В данном случае связь образуется при создание экземпляра класса Logger, в конструктор которого мы передаем другой класс.
Композиция
Композиция это тип отношений при котором один объект может принадлежать только другому объекту и никому другому. При композиции образуется сильная связь между объектами. При таком типе отношений основной объект полностью обеспечивает жизненный цикл объектов от которых он зависит. Используем еще один пример из реальной жизни. Возьмем машину и двигатель. Машина и двигатель конечно могут существовать друг без друга, но суть не в этом. А в том что при конкретной реализации рабочей машины, один двигатель может принадлежать только одной машине, поэтому для такой связи логично использовать композицию.
Пример реализации композиции
В данном случае внутри конструктора создается экземпляр другого класса. При этом создается более крепкая связанность этих двух классов. То есть класс Logger обязательно знает что существует библиотека fs, у него есть класс fs и у него есть метод createWriteStream и т. д. Он полностью знает реализацию другого класса.
То есть в ассоциации и агрегации предполагалось что эти два класса создается независимо, а потом связываются ссылками, в композиции они создается вместе в момент вызова конструктора.
