Что такое композиция в программировании
Перейти к содержимому

Что такое композиция в программировании

  • автор:

Композиция в 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

Агрегация и композиция

Здравствуйте! Всегда считал, что агрегация — это синоним композиции, однако наткнулся на блог в интернете, где приводятся отличия композиции от агрегации. Мне это снесло крышу. Поясните, пожалуйста, плюсы/минусы и того и другого на небольших примерах. Как это влияет на расширяемость, тестируемость и т. д.

Отслеживать
11.5k 8 8 золотых знаков 43 43 серебряных знака 70 70 бронзовых знаков
задан 28 ноя 2016 в 6:36
2,642 1 1 золотой знак 17 17 серебряных знаков 31 31 бронзовый знак
Там пояснено достаточно подробно. Вы лучше конкретные вопросы напишите.
28 ноя 2016 в 10:56

да особо нечего спросить так сходу. хорошо бы если бы кто-нибудь разъяснил плюсы/минусы и того и другого на небольших примерах. как это влияет на расширяемость, тестируемость и т.д.

28 ноя 2016 в 11:45

1 ответ 1

Сортировка: Сброс на вариант по умолчанию

Существует несколько видов взаимодействия объектов, объединенных под общим понятием «Has-A Relationship» или «Part Of Relationship». Это отношение означает, что один объект является составной частью другого объекта.

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

Давайте рассмотрим пример из .NET Framework, чтобы увидеть, какие ограничения/последствия несут эти отношения: StringWriter + StringBuilder .

Класс StringWriter — это специализированная версия класса TextWriter , которые активно используются при сериализации и для получения текстового представления объектов.

Конкретно StringWriter создает строковое представление объекта или графа объектов и опирается на экземпляр StringBuilder в своей работе. Т.е. мы можем сказать, что ‘StringWriter HAS A StringBuilder’ или ‘StringBuilder is part of StringWriter’. Теперь давайте посмотрим, как решить, должен ли StringWriter получать экземпляр StringBuilder -а извне или создавать его?

С одной стороны, нам, как клиенту класса StringWriter часто бывает все равно, что именно используется внутри этого класса для получения строкового представления. Это значит, что с точки зрения простоты использования лучше, чтобы StringWriter создавал экземпляр StringBuilder -а самостоятельно.

Но, с другой стороны, конкретный объект StringWriter -а может отвечать лишь за получения части строкового представления, а другая часть строки может вычисляться другим способом. С этой точки зрения, лучше, чтобы StringWriter принимал экземлпяр StringBuilder -а в конструкторе. Это же справедливо и для высоконагруженных систем, в которых разумно использование пула объектов.

Поскольку StringWriter — это библиотечный класс, который должен поддерживать оба сценария, то у него есть перегруженные версии конструктора: один из них создает StringBuilder внутри, а другой — принимает его снаружи.

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

Композиция: объект A управляет временем жизни объекта B

  • Композиция позволяет скрыть отношение использования объектов от глаз клиента.
  • Делает API использования класса более простым и позволяет перейти от использования одного класса, к другому (например, StringWriter мог бы поменять реализацию и начать использовать другой тип, например CustomStringBuilder ).
  • Отношение достаточно жесткое, поскольку один объект должен уметь создавать другой: он должен знать конкретный тип и иметь доступ к функции создания. Композиция не позволяет использовать интерфейсы (без привлечения фабрик) и требует, чтобы класс имел доступ к конструктору другого класса: представьте, что конструктор StringBuilder -а является внутренним или же это интерфейс IStringBuilder и только клиенский код знает, какой экземпляр должен использоваться здесь и сейчас.

Агрегация: объект А получает ссылку на объект B

  • Более слабая связанность между объектом и его клиентом. Теперь мы можем использовать интерфейсы и одному объекту не нужно знать, как именно создавать другой объект.
  • Большая гибкость. Вытекает из первого пункта
  • Выставление наружу деталей реализации. Поскольку клиент класса должен предоставить зависимость в момент создания объекта (передать экземпляр StringBuilder -а в момент создания StringWriter -а, то сам факт этого отношения становится известен клиенту.
  • Из первого пункта вытекает увеличение сложности в работе клиентов, а также большая «жесткость» решения в долгосрочной перспективе. Теперь автор класса TextWriter уже не может принять решение самостоятельно и перейти от StringBuilder -а к чему-то другому. Да, можно «добавить еще один уровень абстракции» и выделить интерфейс IStringBuilder , но разорвать это отношение совсем будет невозможно без поломки всех существующих клиентов.

В заключении: дизайн — это поиск компромисса между различными факторами. Композиция проще с точки зрения клиентов класса, но налагает определенные ограничения: «целое» должно уметь создавать «составную часть». Агрегация гибче, но налагает другие ограничения: теперь «целое» не скрывает о существовании «составной части», а значит и не сможет заменить ее на другую «составную часть» в будущем.

P.S. Если очень хочется использовать пример из реального мира, то для объяснения композиции и агрегации может подойти . отвертка. Если отвертка цельная, т.е. ручка и насадка намертво связаны друг с другом, то мы имеем отношение композиции. Если же насадка съемная и может существовать без ручки или же использоваться с другой ручкой, то мы имеем отношение агрегации.

Что такое композиция в программировании

Читала Брюса Эккеля и решила поискать более развернутую информацию с примерами composition, delegation и синтеза composition+inheritance, так как синтаксис там совсем другой, это не обычное наследование. В этой статье, к сожалению, примеров не нашла. Очень хорошо объяснено, про отношение has-a, но как оно воплощается в коде? UPD: Если кому-то интересно, то вот здесь нашла пример Пример Композиции По сути это просто использование методов/переменных другого класса через instance variable (поправьте, если я не права). Ну и вот тут еще мистер Кумар в деле показывает, как это работает) Видео о композиции

Denis Gritsay Уровень 37
19 октября 2023

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

Never Ending Pie Throwing Robot Уровень 30
11 октября 2023

Хорошо, нам понятно объяснили что такое композиция и агрегация, но почему же это важно?? Это настолько важно, что они даже придумали термины для обозначения этого, но почему же.

Anonymous #3336441 Уровень 45
4 октября 2023

Я так понимаю что композиция и агрегирование никак не относятся к наследованию, и являются отдельными взаимоотношениями между классами. По идеи при композиции классы должны быть вложенными, а при агрегации нет. Поправьте меня если не прав) И как при агрегации, объект является частью другого объекта.

дмитрий Уровень 22
31 августа 2023

Статья без приведения примера чистого и ясного кода иллюстрирующего сказанное треп ни о чем. Чтоб до конца понять тему нужно читать другие статьи (в сторонних источниках). Кстати, во всех качественных источниках (с кодом) написание кода предваряется вышеприведенными в статье рассуждениями. То есть, чтение этой статьи потеря времени. Идем на хабр. Да, не обязательно — если сениор, то умеешь обьяснять. А вообще статья напоминает старый одесский анекдот: «Изя вам Моцарт нравиться? Нет. Вы что слушали Моцарта. Нет мне Моня напел.»

Наследование, композиция, агрегация

Нередко случается, что решив разобраться с какой-то новой темой, понятием, инструментом программирования, я читаю одну за другой статьи на различных сайтах в интернете. И, если тема сложная, то эти статьи могут не на шаг не приблизить меня к понимаю. И вдруг встречается статья, которая моментально дает озарение и все паззлы складываются воедино. Трудно определить, что отличает такую статью от других. Правильно подобранные слова, оптимальная логика изложения или же просто более релевантный пример. Я не претендую на то, что моя статься окажется новым словом в 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.
Весь код игры можно увидеть вот здесь.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *