Композиция (программирование)
Агрегирование в общем смысле — это объединение нескольких элементов в единое целое. Результат агрегирования называют агрегатом.
В программировании под агрегированием (так же называемым композицией и включением) подразумевают методику создания нового класса из уже существующих классов. Об агрегировании также часто говорят как об «отношении принадлежности» по принципу «у машины есть корпус, колеса и двигатель».
Вложенные объекты нового класса обычно объявляются закрытыми, что делает их недоступными для прикладных программистов, работающих с классом. С другой стороны, создатель класса может изменять эти объекты, не нарушая работы существующего клиентского кода. Кроме того, замена вложенных объектов на стадии выполнения программы позволяет динамически изменять её поведение. Механизм наследования такой гибкостью не обладает, поскольку для производных классов устанавливаются ограничения, проверяемые на стадии компиляции.
На базе агрегирования реализуется методика делегирования, когда поставленная перед внешним объектом задача перепоручается внутреннему объекту, специализирующемуся на решении задач такого рода.
Wikimedia Foundation . 2010 .
- Композиция «Золотое руно»
- Композиция (музыка)
Смотреть что такое «Композиция (программирование)» в других словарях:
- Агрегирование (программирование) — В этой статье не хватает ссылок на источники информации. Информация должна быть проверяема, иначе она может быть поставлена под сомнение и удалена. Вы можете отредактировать эту статью, добавив ссылки на авторитетные источники … Википедия
- Субъектно-ориентированное программирование — Парадигмы программирования Агентно ориентированная Компонентно ориентированная Конкатенативная Декларативная (контрастирует с Императивной) Ограничениями Функциональная Потоком данных Таблично ориентированная (электронные таблицы) Реактивная … Википедия
- Класс (программирование) — У этого термина существуют и другие значения, см. Класс. Класс в программировании набор методов и функций. Другие абстрактные типы данных метаклассы, интерфейсы, структуры, перечисления характеризуются какими то своими, другими… … Википедия
- Класс (объектно-ориентированное программирование) — Класс, наряду с понятием «объект», является важным понятием объектно ориентированного подхода в программировании (хотя существуют и бесклассовые объектно ориентированные языки, например, Прототипное программирование). Под классом подразумевается… … Википедия
- Функциональная зависимость (программирование) — Функциональная зависимость концепция, лежащая в основе многих вопросов, связанных с реляционными базами данных, включая, в частности, их проектирование. Математически представляет бинарное отношение между множествами атрибутов данного… … Википедия
- That’s the Way Love Goes — «That’s the Way Love Goes» Сингл Джанет Джексон из альбома janet … Википедия
- New Order — Об альбоме группы Testament см. статью The New Order. New Order New … Википедия
- Диаграмма классов — Для улучшения этой статьи желательно?: Викифицировать статью. В UML диаграмма классов является типом диаграммы статичес … Википедия
- Vision of Love — «Vision of Love» Сингл Мэрайи Кэри из альбома Mariah Carey Выпущен 15 мая 1990 Формат CD сингл Кассетный сингл 7 сингл … Википедия
- Music (альбом Мадонны) — У этого термина существуют и другие значения, см. Music. Music … Википедия
Композиция в Python
Еще одной особенностью объектно-ориентированного программирования является возможность реализовывать так называемый композиционный подход. Заключается он в том, что есть класс-контейнер, он же агрегатор, который включает в себя вызовы других классов. В результате получается, что при создании объекта класса-контейнера, также создаются объекты других классов.
Чтобы понять, зачем нужна композиция в программировании, проведем аналогию с реальным миром. Большинство биологических и технических объектов состоят из более простых частей, также являющихся объектами. Например, животное состоит из различный органов (сердце, желудок), компьютер — из различного «железа» (процессор, память).
Не следует путать композицию с наследованием, в том числе множественным. Наследование предполагает принадлежность к какой-то общности (похожесть), а композиция — формирование целого из частей. Наследуются атрибуты, т. е. возможности, другого класса, при этом объектов непосредственно родительского класса не создается. При композиции же класс-агрегатор создает объекты других классов.
Рассмотрим на примере реализацию композиции в Python. Пусть, требуется написать программу, которая вычисляет площадь обоев для оклеивания помещения. При этом окна, двери, пол и потолок оклеивать не надо.
Прежде, чем писать программу, займемся объектно-ориентированным проектированием. То есть разберемся, что к чему. Комната – это прямоугольный параллелепипед, состоящий из шести прямоугольников. Его площадь представляет собой сумму площадей составляющих его прямоугольников. Площадь прямоугольника равна произведению его длины на ширину.
По условию задачи обои клеятся только на стены, следовательно площади верхнего и нижнего прямоугольников нам не нужны. Из рисунка видно, что площадь одной стены равна xz , второй – уz . Противоположные прямоугольники равны, значит общая площадь четырех прямоугольников равна S = 2xz + 2уz = 2z(x+y) . Потом из этой площади надо будет вычесть общую площадь дверей и окон, поскольку они не оклеиваются.
Можно выделить три типа объектов – окна, двери и комнаты. Получается три класса. Окна и двери являются частями комнаты, поэтому пусть они входят в состав объекта-помещения.
Для данной задачи существенное значение имеют только два свойства – длина и ширина. Поэтому классы «окна» и «двери» можно объединить в один. Если бы были важны другие свойства (например, толщина стекла, материал двери), то следовало бы для окон создать один класс, а для дверей – другой. Пока обойдемся одним, и все что нам нужно от него – площадь объекта:
class WinDoor: def __init__(self, x, y): self.square = x * y
Класс «комната» – это класс-контейнер для окон и дверей. Он должен содержать вызовы класса «ОкноДверь».
Хотя помещение не может быть совсем без окон и дверей, но может быть чуланом, дверь которого также оклеивается обоями. Поэтому имеет смысл в конструктор класса вынести только размеры самого помещения, без учета элементов «дизайна», а последние добавлять вызовом специально предназначенного для этого метода, который будет добавлять объекты-компоненты в список.
class Room: def __init__(self, x, y, z): self.square = 2 * z * (x + y) self.wd = [] def add_wd(self, w, h): self.wd.append(WinDoor(w, h)) def work_surface(self): new_square = self.square for i in self.wd: new_square -= i.square return new_square r1 = Room(6, 3, 2.7) print(r1.square) # выведет 48.6 r1.add_wd(1, 1) r1.add_wd(1, 1) r1.add_wd(1, 2) print(r1.work_surface()) # выведет 44.6
Практическая работа
Приведенная выше программа имеет ряд недочетов и недоработок. Требуется исправить и доработать, согласно следующему плану.
При вычислении оклеиваемой поверхности мы не «портим» поле self.square . В нем так и остается полная площадь стен. Ведь она может понадобиться, если в списке wd произойдут изменения, и придется заново вычислять оклеиваемую площадь.
Однако в классе не предусмотрено сохранение длин сторон, хотя они тоже могут понадобиться. Например, если потребуется изменить одну из величин у уже существующего объекта. Площадь же помещения всегда можно вычислить, если хранить исходные параметры. Поэтому сохранять саму площадь в поле не обязательно.
Исправьте код так, чтобы у объектов Room было четыре поля – width , length , height и wd . Площади (полная и оклеиваемая) должны вычислять лишь при необходимости путем вызова методов.
Программа вычисляет площадь под оклейку, но ничего не говорит о том, сколько потребуется рулонов обоев. Добавьте метод, который принимает в качестве аргументов длину и ширину одного рулона, а возвращает количество необходимых, исходя из оклеиваемой площади.
Разработайте интерфейс программы. Пусть она запрашивает у пользователя данные и выдает ему площадь оклеиваемой поверхности и количество необходимых рулонов.
Курс с примерами решений практических работ:
pdf-версия
X Скрыть Наверх
Объектно-ориентированное программирование на Python
Наследование, композиция, агрегация
Нередко случается, что решив разобраться с какой-то новой темой, понятием, инструментом программирования, я читаю одну за другой статьи на различных сайтах в интернете. И, если тема сложная, то эти статьи могут не на шаг не приблизить меня к понимаю. И вдруг встречается статья, которая моментально дает озарение и все паззлы складываются воедино. Трудно определить, что отличает такую статью от других. Правильно подобранные слова, оптимальная логика изложения или же просто более релевантный пример. Я не претендую на то, что моя статься окажется новым словом в C# или же лучшей обучающей статьей. Но, возможно для кого-то она станет именно той, которая позволит разобраться, запомнить и начать правильно применять те понятия, о которых пойдет речь.
В объектно-ориентированных языках программирования существует три способа организации взаимодействия между классами. Наследование — это когда класс-наследник имеет все поля и методы родительского класса, и, как правило, добавляет какой-то новый функционал или/и поля. Наследование описывается словом «является». Легковой автомобиль является автомобилем. Вполне естественно, если он будет его наследником.
```class Vehicle < bool hasWheels; >class Car : Vehicle < string model = "Porshe"; int numberOfWheels = 4 >```
Ассоциация – это когда один класс включает в себя другой класс в качестве одного из полей. Ассоциация описывается словом «имеет». Автомобиль имеет двигатель. Вполне естественно, что он не будет являться наследником двигателя (хотя такая архитектура тоже возможна в некоторых ситуациях).
Выделяют два частных случая ассоциации: композицию и агрегацию.
Композиция – это когда двигатель не существует отдельно от автомобиля. Он создается при создании автомобиля и полностью управляется автомобилем. В типичном примере, экземпляр двигателя будет создаваться в конструкторе автомобиля.
``` class Engine < int power; public Engine(int p) < power = p; >> class Car < string model = "Porshe"; Engine engine; public Car() < this.engine = new Engine(360); >> ```
Агрегация – это когда экземпляр двигателя создается где-то в другом месте кода, и передается в конструктор автомобиля в качестве параметра.
``` class Engine < int power; public Engine(int p) < power = p; >> class Car < string model = "Porshe"; Engine engine; public Car(Engine someEngine) < this.engine = someEngine; >> Engine goodEngine = new Engine(360); Car porshe = new Car(goodEngine); ```
Хотя ведутся дискуссии о преимуществах того или иного способа организации взаимодействия между классами, какого-либо абстрактного правила не существует. Разработчик выбирает тот или иной путь основываясь на элементарной логике (“является” или “имеет”), но также принимает во внимание возможности и ограничения, которые дают и накладывают эти способы. Для того, чтобы увидеть эти возможности и ограничения, я попытался написать пример. Достаточно простой, чтобы код оставался компактным, но и достаточно развитый, чтобы в рамках одной программы можно было применить все три способа. И, главное, я попытался сделать этот пример как можно менее абстрактным – все объекты и экземпляры понятны и осязаемы.
Напишем простенькую игру – танковый бой. Играют два танка. Они поочередно стреляют и проигрывает тот, здоровье которого упало до нуля. В игре будут различные типы снарядов и брони. Для того, чтобы нанести урон необходимо во-первых, попасть по танку противника, во-вторых, пробить его броню. Если броня не пробита, урон не наносится. Логика игры построена на принципе «камень-ножницы-бумага»: то есть броня одного типа хорошо противостоит снарядам определенного типа, но плохо держит другие снаряды. Кроме того, снаряды, которые хорошо пробивают броню, наносят малый «заброневой» урон, и, напротив, наиболее «летальные» снаряды имеют меньше шансов пробить броню.
Создадим простенький класс для пушки. Он будет иметь два приватных поля: калибр и длину ствола. От калибра зависит урон, и, частично, способность к пробитию брони. От длины ствола – точность стрельбы.
``` public class Gun < private int caliber; private int barrelLength; >```
Сделаем также конструктор для пушки:
``` public Gun(int cal, int length) < this.caliber = cal; this.barrelLength = length; >```
Сделаем метод для получения калибра из других классов:
``` public int GetCaliber() < return this.caliber; >```
Помните, что для поражения цели должно произойти две вещи: попадание в цель и пробитие брони? Так вот, пушка будет отвечать за первую из них: попадание. Поэтому делаем булевый метод IsOnTarget, который принимает случайную величину (dice) и возвращает результат: попали или нет:
``` public bool IsOnTarget(int dice) < return (barrelLength + dice) >100; > ```
Целиком класс пушки выглядит следующим образом:
``` public class Gun < private int caliber; private int barrelLength; public Gun(int cal, int length) < this.caliber = cal; this.barrelLength = length; >public int GetCaliber() < return this.caliber; >public bool IsOnTarget(int dice) < return (barrelLength + dice) >100; > > ```
Теперь сделаем снаряды – это наиболее очевидный случай для применения наследования, но и агрегацию в нем тоже применим. Любой снаряд имеет свои особенности. Просто неких гипотетических снарядов не бывает. Поэтому класс делаем абстрактным. Делаем ему строковое поле «тип».
Снаряды делают для пушек. Для определенных пушек. Снаряд одного калибра не выстрелит из пушки другого калибра. Поэтому добавляем снаряду поле-ссылку на экземпляр пушки. Делаем конструктор.
``` public abstract class Ammo < Gun gun; public string type; public Ammo(Gun someGun, string type) < gun = someGun; this.type = type; >> ```
Здесь мы применили агрегацию. Где-то будет создана пушка. Потом к этой пушке будут создаваться снаряды, которые имеют указатель на пушку.
Конкретные типы снарядов будут наследниками абстрактного снаряда. Наследники могут просто наследовать методы родителя, но могут и быть переопределены, то есть работать не так, как родительский метод. Но мы точно знаем, что любой снаряд должен иметь ряд методов. Любой снаряд должен наносить урон. Метод GetDamage просто возвращает калибр, умноженный на три. В общем случае, урон снаряда зависит от калибра. Но этот метод будет переопределяться в дочерних классах (помним, что снаряды, которые хорошо пробивают броню, как правило наносят меньший «заброневой» урон. Чтобы иметь возможность переопределить метод в дочернем классе, используем слово virtual.
``` public virtual int GetDamage() < //TO OVERRIDE: add logic of variable damage depending on Ammo type return gun.GetCaliber()*3; >```
Любой снаряд должен пробивать (или по крайней мере пытаться пробить) броню. В общем случае способность пробивать броню также зависит от калибра (ну, и еще от многого – начальной скорости, например, но мы не будем усложнять). Поэтому, метод возвращает калибр. То есть, грубо говоря, снаряд может пробить броню, равную по толщине своему калибру. Этот метод не будет переопределяться в дочерних классах.
``` public int GetPenetration() < return gun.GetCaliber(); >```
Кроме того, для удобной отладки и организации консольного вывода, имеет смысл добавить метод ToString, который просто позволит нам увидеть, что это за снаряд и какого калибра:
``` public override string ToString() < return $"Снаряд " + type + " к пушке калибра " + gun.GetCaliber(); >```
Теперь сделаем разные типы снарядов, которые будут наследовать абстрактный снаряд: фугасный, кумулятивный, подкалиберный. Фугасный наносит самый большой урон, кумулятивный – меньше, подкалиберный – еще меньше. Дочерние классы не имеют полей и вызывают конструктор базового снаряда, передавая ему пушку, и строковый тип. В дочернем классе переопределяется метод GetDamage() – вносятся коэффициенты, которые увеличат или уменьшат урон по сравнению с дефолтным.
Фугасный (дефолтный урон):
``` public class HECartridge : Ammo < public HECartridge(Gun someGun) : base(someGun, "фугасный") < >public override int GetDamage() < return (int)(base.GetDamage()); >> ```
Кумулятивный (дефолтный урон х 0.6):
``` public class HEATCartridge : Ammo < public HEATCartridge(Gun someGun) : base(someGun, "кумулятивный") < >public override int GetDamage() < return (int)(base.GetDamage() * 0.6); >> ```
Подкалиберный (дефолтный урон х 0.3):
``` public class APCartridge : Ammo < public APCartridge(Gun someGun) : base(someGun, "подкалиберный") < >public override int GetDamage() < return (int)(base.GetDamage() * 0.3); >> ```
Обратите внимание, что в переопределенном методе GetDamage вызывается и метод базового класса. То есть, переопределив метод, мы также сохраняем возможность обратиться к дефолтному методу, использовав ключевое слово base).
Итак, для снарядов мы применили и агрегацию (пушка в базовом классе), и наследование.
Создадим теперь броню для танка. Здесь применим только наследование. Любая броня имеет толщину. Поэтому абстрактный класс брони будет иметь поле thickness, и строковое поле type, которое будет определятся при создании дочерних классов.
``` public abstract class Armour < public int thickness; public string type; public Armour(int thickness, string type) < this.thickness = thickness; this.type = type; >> ```
Броня будет в нашей игре определять пробита они или нет. Поэтому, у нее будет лишь один метод, который будет переопределяться в дочерних, в зависимости от типа брони.
``` public virtual bool IsPenetrated(Ammo projectile) < return projectile.GetDamage() >thickness; > ```
А пробита они или нет – зависит от того, какой прилетел снаряд: в дефолтном случае какого калибра. Поэтому метод принимает экземпляр снаряда и возвращает булевый результат: пробита или нет. Создадим несколько типов брони – наследников абстрактной брони. Приведу код лишь одного типа – логика примерно такая же, как и в снарядах. Гомогенная броня хорошо держит фугасный снаряд, но плохо – подкалиберный. Поэтому, если прилетел подкалиберный снаряд, который имеет высокую бронепробиваемость, то в вычислениях наша броня как-бы становится тоньше. И так далее: каждый вид брони имеет свой набор коэфициентов устойчивости к тому или иному снаряду.
``` public class HArmour : Armour < public HArmour(int thickness) : base(thickness, "гомогенная") < >public override bool IsPenetrated(Ammo projectile) < if (projectile is HECartridge) < //Если фугасный, то толщина брони считается больше return projectile.GetPenetration() >this.thickness * 1.2; > else if (projectile is HEATCartridge) < //Если кумулятивный, то толщина брони нормальная return projectile.GetPenetration() >this.thickness * 1; > else < //Если подкалиберный, то считаем уменьшаем толщину return projectile.GetPenetration() >this.thickness * 0.7; > > > ```
Здесь мы используем одно из чудес, которые дает полиморфизм. Метод принимает любой снаряд. В сигнатуре указан базовый класс, а не дочерние. Но внутри метода, мы можем увидеть, что за снаряд прилетел – какого типа. И в зависимости от этого, реализуем ту или иную логику. Если бы мы не применили наследование для снарядов, а сделали просто три уникальных класса типов снарядов, то проверку пробития брони пришлось бы организовывать иначе. Нам пришлось бы писать столько перегруженных методов, сколько типов снарядов у нас в игре, и вызывать один из них в зависимости от того, какой снаряд прилетел. Это тоже было бы довольно изящно, но не относится к теме данной статьи.
Теперь у нас все готово для создания танка. В танке не будет наследования, но будет композиция и агрегация. Разумеется, у танка будет название. У танка будет пушка (агрегация). Для нашей игры сделаем допущение, что танк может «переодевать» броню перед каждым ходом – выбрать тот или иной тип брони. Для этого, у танка будет список типов брони. У танка будет боеукладка – список снарядов, который будет наполнен снарядами, созданными в конструкторе танка (композиция!). У танка будет здоровье (уменьшается при попадании в него), и, у танка будет текущая выбранная броня и текущий выбранный снаряд.
``` public class Panzer < private string model; private Gun gun; private Listarmours; private List ammos; private int health; public Ammo LoadedAmmo < get; set; >public Armour SelectedArmour < get; set; >> ```
Для того, чтобы конструктор танка остался более-менее компактным, сделаем два вспомогательных приватных метода, которые добавляют три типа брони соответствующей толщины, и наполняют боеукладку 10 снарядами каждого из трех типов:
``` private void AddArmours(int armourWidth) < armours.Add(new SArmour(armourWidth)); armours.Add(new HArmour(armourWidth)); armours.Add(new CArmour(armourWidth)); >private void LoadAmmos() < for(int i = 0; i < 10; i++) < ammos.Add(new APCartridge(this.gun)); ammos.Add(new HEATCartridge(this.gun)); ammos.Add(new HECartridge(this.gun)); >> ```
Теперь конструктор танка выглядит вот таким образом:
``` public Panzer(string name, Gun someGun, int armourWidth, int h) < model = name; gun = someGun; health = h; armours = new List(); ammos = new List(); AddArmours(armourWidth); LoadAmmos(); LoadedAmmo = null; SelectedArmour = armours[0]; //по умолчанию - гомогенная броня >```
Обратите внимание, что здесь мы снова используем возможности полиморфизма. Наша боекладка вмещает снаряды любого типа, так как список имеет тип данных Ammo – родительский снаряд. Если бы мы не наследовались, а создавали уникальные типы снарядов, пришлось бы делать отдельный список под каждый тип снаряда.
Пользовательский интерфейс танка состоит из трех методов: выбрать броню, зарядить пушку, выстрелить.
``` public void SelectArmour(string type) < for (int i = 0; i < armours.Count; i++) < if (armours[i].type == type) < SelectedArmour = armours[i]; break; >> > ```
``` public void LoadGun(string type) < for(int i = 0; i < ammos.Count; i++) < if(ammos[i].type == type) < LoadedAmmo = ammos[i]; Console.WriteLine("заряжено!"); return; >> Console.WriteLine($"сорян, командир, " + type + " закончились!"); > ```
Как я упомянул в начале, в этом примере я старался максимально уйти от абстрактных понятий, которые нужно все время держать в голове. Поэтому каждый экземпляр снаряда у нас равен физическому снаряду, который положили в боеукладку перед боем. Следовательно, снаряды могут закончится в самый неподходящий момент!
``` public Ammo Shoot() < if (LoadedAmmo != null) < Ammo firedAmmo = (Ammo)LoadedAmmo.Clone(); ammos.Remove(LoadedAmmo); LoadedAmmo = null; Random rnd = new Random(); int dice = rnd.Next(0, 100); bool hit = this.gun.IsOnTarget(dice); if (this.gun.IsOnTarget(dice)) < Console.WriteLine("Попадание!"); return firedAmmo; >else < Console.WriteLine("Промах!"); return null; >> else Console.WriteLine("не заряжено"); return null; > ```
Здесь – поподробнее. Во-первых, есть проверка заряжена ли пушка. Во-вторых, снаряд, который вылетел из ствола, уже не существует для данного танка, его уже нет ни в пушке, ни в боеукладке. Но физически он еще существует – летит по направлению к цели. И если попадет, будет участвовать в вычислении пробития брони и урона цели. Поэтому, мы сохраняем этот снаряд в новой переменной: Ammo firedAmmo. Поскольку на следующей же строке данный снаряд перестанет существовать для данного танка, придется использовать интерфейс IClonable для базового класса снаряда:
``` public abstract class Ammo : ICloneable ```
Этот интерфейс требует реализации метода Clone(). Вот она:
``` public object Clone() < return this.MemberwiseClone(); >```
Теперь все супер реалистично: при выстреле генерируется dice, пушка рассчитывает попадание своим методом IsOnTarget, и, если попадание есть, то метод Shoot вернет экземпляр снаряда, а если промах – то вернет null.
Последний метод танка – его поведение при попадании вражеского снаряда:
``` public void HandleHit(Ammo projectile) < if (SelectedArmour.IsPenetrated(projectile)) < this.health -= projectile.GetDamage(); >else Console.WriteLine("Броня не пробита."); > ```
Снова полиморфизм во всей красе. К нам прилетает снаряд. Любой. Исходя из выбранной брони и типа снаряда, вычисляется пробита броня или нет. Если пробита, то вызывается метод конкретного типа снаряда GetDamage().
Все готово. Остается только написать консольный (или неконсольный) вывод, в котором будет обеспечен пользовательский интерфейс и в цикле реализованы поочередные ходы игроков.
Подведем итоги. Мы написали программу, в которой использовали наследование, композицию и агрегацию, надеюсь, поняли и запомнили различия. Активно задействовали возможности полиморфизма, во-первых, когда любые экземпляры дочерних классов можно сложить в список, имеющий тип данных родительского, а во-вторых, создавая методы, которые принимают в качестве параметра родительский экземпляр, но внутри которых вызываются методы дочернего. По ходу текста я упоминал возможные альтернативные реализации – замену наследования на агрегацию, и, универсального рецепта тут нет. В нашей реализации наследование дало нам легкость добавления новых деталей в игру. Например, чтобы добавить новый тип снаряда нам нужно лишь:
- собственно, скопировать один из существующих типов, заменив название и строковое поле, передаваемое в конструктор;
- добавить еще один if в дочерние классы брони;
- добавить дополнительный пункт в меню выбора снаряда в пользовательском интерфейсе.
Ниже – приведена диаграмма наших классов.
В финальном коде игры все «магические числа», которые использовались в тексте, вынесены в отдельный статический класс Config. К публичным полям статического класса мы можем обратиться из любого фрагмента нашего кода и его экземпляр не нужно (и невозможно) создавать. Вот так он выглядит:
``` public static class Config < public static ListammoTypes = new List < "фугасный", "кумулятивный", "подкалиберный" >; public static List armourTypes = new List < "гомогенная", "разнесенная", "комбинированная" >; //трешхолд для пушки - величина, выше которой будем считать, что снаряд попал в цель public static int _gunTrashold = 100; //дефолтный коэффициент для заброневого действия базового снаряда public static int _defaultDamage = 3; //коэффициенты урона для снарядов разных типов public static double _HEDamage = 1.0; public static double _HEATDamage = 0.6; public static double _APDamage = 0.3; //коэффициенты стойкости брони //для гомогенной: //Если в гомогенную броню прилетает фугасный, то ее толщина считается большей - коэффициент 1.2 public static double _HArmour_VS_HE = 1.2; //Если в гомогенную броню прилетает кумулятивный, то ее толщина считается нормальной - коэффициент 1.0 public static double _HArmour_VS_HEAT = 1.0; //Если в гомогенную броню прилетает подкалиберный, то ее толщина считается меньшей - коэффициент 0.7 public static double _HArmour_VS_AP = 0.7; //для комбинированной брони //Если в комбинированную броню прилетает фугасный, то ее толщина считается нормальной - коэффициент 1 public static double _СArmour_VS_HE = 1.0; //Если в комбинированную броню прилетает фугасный, то ее толщина считается меньше - коэффициент 0.8 public static double _СArmour_VS_HEAT = 0.8; //Если в комбинированную броню прилетает фугасный, то ее толщина считается больше - коэффициент 1.2 public static double _СArmour_VS_AP = 1.2; //Для разнесенной брони //Если в разнесенную броню прилетает фугасный, то ее толщина считается меньше - коэффициент 0.8 public static double _SArmour_VS_HE = 0.8; //Если в разнесенную броню прилетает кумулятивный, то ее толщина считается больше - коэффициент 1.2 public static double _SArmour_VS_HEAT = 1.2; //Если в разнесенную броню прилетает подкалибереый, то ее толщина считается нормальной - коэффициент 1 public static double _SArmour_VS_AP = 1.0; > ```
И благодаря этому классу мы можем производить дальнейшую настройку, меняя параметры лишь здесь, без дальнейшего углубления в классы и методы. Если, например, мы пришли к выводу, что подкалиберный снаряд получился слишком сильным, то мы меняем одну циферку в Config.
Весь код игры можно увидеть вот здесь.
Programming stuff
Между двумя классами/объектами существует разные типы отношений. Самым базовым типом отношений является ассоциация (association), это означает, что два класса как-то связаны между собой, и мы пока не знаем точно, в чем эта связь выражена и собираемся уточнить ее в будущем. Обычно это отношение используется на ранних этапах дизайна, чтобы показать, что зависимость между классами существует, и двигаться дальше.
Рисунок 1. Отношение ассоциации
Более точным типом отношений является отношение открытого наследования (отношение «является», IS A Relationship), которое говорит, что все, что справедливо для базового класса справедливо и для его наследника. Именно с его помощью мы получаем полиморфное поведение, абстрагируемся от конкретной реализации классов, имея дело лишь с абстракциями (интерфейсами или базовыми классами) и не обращаем внимание на детали реализации.
Рисунок 2. Отношение наследование
И хотя наследование является отличным инструментом в руках любого ОО-программиста, его явно недостаточно для решения всех типов задач. Во-первых, далеко не все отношения между классами определяются отношением «является», а во-вторых, наследование является самой сильной связью между двумя классами, которую невозможно разорвать во время исполнения (это отношение является статическим и, в строготипизированных языках определяется во время компиляции).
В этом случае нам на помощь приходит другая пара отношений: композиция (composition) и агрегация (aggregation). Оба они моделируют отношение «является частью» (HAS-A Relationship) и обычно выражаются в том, что класс целого содержит поля (или свойства) своих составных частей. Грань между ними достаточно тонкая, но важная, особенно в контексте управления зависимостями.
Рисунок 3. Отношение композиции и агрегации
HINT
Пара моментов, чтобы легче запомнить визуальную нотацию: (1) ромбик всегда находится со стороны целого, а простая линия со стороны составной части; (2) закрашенный ромб означает более сильную связь – композицию, незакрашенный ромб показывает более слабую связь – агрегацию.
Разница между композицией и агрегацией заключается в том, что в случае композиции целое явно контролирует время жизни своей составной части (часть не существует без целого), а в случае агрегации целое хоть и содержит свою составную часть, время их жизни не связано (например, составная часть передается через параметры конструктора).
class CompositeCustomService // Композиция private readonly CustomRepository _repository
= new CustomRepository(); public void DoSomething()
// Используем _repository > >
class AggregatedCustomService // Агрегация private readonly AbstractRepository _repository; public AggregatedCustomService(AbstractRepository repository)
<
_repository = repository;
> public void DoSomething()
// Используем _repository > >
CompositeCustomService для управления своими составными частями использует композицию, а AggregatedCustomService – агрегацию. При этом явный контроль времени жизни обычно приводит к более высокой связанности между целым и частью, поскольку используется конкретный тип, тесно связывающий участников между собой.
С одной стороны, такая жесткая связь может не являться чем-то плохим, особенно когда зависимость является стабильной (см. раздел «Стабильные и изменчивые зависимости» в прошлой заметке). С другой стороны мы можем использовать композицию и контролировать время жизни объекта, не завязываясь на конкретные типы. Например, с помощью абстрактной фабрики:
internal interface IRepositoryFactory AbstractRepository Create(); > class CustomService // Композиция private readonly IRepositoryFactory _repositoryFactory; public CustomService(IRepositoryFactory repositoryFactory)
<
_repositoryFactory = repositoryFactory;
> public void DoSomething()
var repository = _repositoryFactory.Create(); // Используем созданный AbstractRepository > >
В данном случае мы не избавляемся от композиции (CustomService все еще контролирует время жизни AbstractRepository), но делает это не напрямую, а с помощью дополнительной абстракции – абстрактной фабрики. Поскольку такой подход требует удвоения количества классов наших зависимостей, то его стоит использовать, когда явный контроль времени жизни является необходимым условием.
Интересной особенностью разных отношений между классами является то, что логичность их использования может зависеть от точки зрения проектировщика, от того, с какой стороны он смотрит на задачу и какие вопросы он задает себе при ее анализе. Именно поэтому одну и ту же задачу можно решить десятком разных способов, при этом в одном случае мы получим сильно связанный дизайн с большим количеством наследования и композиции, а в другом случае – эта же задача будет разбита на более автономные строительные блоки, объединяемые между собой с помощью агрегации.
Например, нашу задачу с сервисами и репозитариями можно решить множеством разных способов. Кто-то скажет, что здесь подойдет наследование и сделает SqlCustomService наследником от AbstractCustomService; другой скажет, что этот подход неверен, поскольку CustomService у нас один, а иерархия должна быть у репозитариев.
Рисунок 4. Наследование vs Агрегация
Каждый вариант приводит к одному и тому же конечному результату, при этом связанность изменяется от очень высокой (при наследовании) к очень слабой (при агрегации).
Заключение
Существует несколько достаточно объективных критериев для определения связности дизайна по диаграмме классов: большие иерархии наследования (глубокие или широкие иерархии), и повсеместное использование композиции, а не агрегации скорее всего говорит о сильно связанном дизайне.
Большое количество наследования говорит о том, что проектировщики забыли о старом добром совете Банды Четырех, который сводится к тому, что следует предпочесть агрегацию наследованию, поскольку первая дает большую гибкость и динамичность во время исполнения.
Обилие же композиции говорит о нарушении Принципа Инверсии Зависимостей, сформулированном Бобом Мартином, которую сейчас можно выразить в терминах агрегации и композиции: предпочитайте агрегацию вместо композиции, поскольку первая стимулирует использование абстракций, а не конкретных классов.
В следующий раз: перейдем к рассмотрению конкретных DI паттернов и начнем с самого популярного из них – с Constructor Injection.