It often happens that having decided to deal with some new topic, concept, programming tool, I read one article after another on various websites on the Internet. And, if the topic is complex, then these articles may not bring me a step closer to understand. And suddenly there is an article that instantly gives insight and all the puzzles are put together. It is difficult to determine what distinguishes such an article from others. Correctly chosen words, optimal presentation logic, or just a more relevant example. I do not pretend that my article will be a new word in C # or the best learning article. But perhaps for someone it will be the one that will allow to sort out, remember and begin to correctly apply those concepts that will be discussed.
In object-oriented programming languages, there are three ways to organize interaction between classes. Inheritance is when the heir class has all the fields and methods of the parent class, and, as a rule, adds some new functionality or / and fields. Inheritance is described by the word "is". The car is a car. It is only natural if he will be his heir.
```class Vehicle { bool hasWheels; } class Car : Vehicle { string model = "Porshe"; int numberOfWheels = 4 }```
An association is when one class includes another class as one of the fields. The association is described by the word "has." The car has an engine. It is quite natural that he will not be the heir to the engine (although such an architecture is also possible in some situations).
There are two special cases of association: composition and aggregation.
')
Composition is when the engine does not exist separately from the car. It is created when you create a car and is completely driven by a car. In a typical example, an engine instance will be created in the car's designer.
``` class Engine { int power; public Engine(int p) { power = p; } } class Car { string model = "Porshe"; Engine engine; public Car() { this.engine = new Engine(360); } } ```
Aggregation is when an engine instance is created somewhere else in the code, and is passed to the car designer as a parameter.
``` 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); ```
Although there are discussions about the advantages of one or another way of organizing interaction between classes, there is no abstract rule. The developer chooses one or another path based on elementary logic (“is” or “has”), but also takes into account the possibilities and limitations that these methods give and impose. In order to see these features and limitations, I tried to write an example. Simple enough for the code to remain compact, but also sufficiently developed to allow all three methods to be applied in one program. And, most importantly, I tried to make this example as less abstract as possible - all objects and instances are understandable and tangible.
Let's write a simple game - tank battle. They play two tanks. They alternately shoot and lose the one whose health fell to zero. The game will have different types of shells and armor. In order to inflict damage, you must firstly hit the enemy’s tank, and secondly, penetrate its armor. If the armor is not broken, the damage is not applied. The logic of the game is based on the principle of “rock-paper-scissors”: that is, one type of armor well opposes certain types of shells, but does not hold other shells badly. In addition, shells that pierce armor well cause little “armor” damage, and, on the contrary, the most “lethal” shells are less likely to penetrate armor.
Create a simple class for the gun. It will have two private fields: caliber and barrel length. The damage and, in part, the ability to break through armor depends on the caliber. From the length of the barrel - shooting accuracy.
``` public class Gun { private int caliber; private int barrelLength; } ```
Let's make also the designer for a gun:
``` public Gun(int cal, int length) { this.caliber = cal; this.barrelLength = length; } ```
Let's make a method for getting caliber from other classes:
``` public int GetCaliber() { return this.caliber; } ```
Remember that to hit a target two things must happen: hitting the target and breaking through the armor? So, the gun will be responsible for the first of them: hit. Therefore, we make the boolean method IsOnTarget, which takes a random variable (dice) and returns the result: whether or not it fell:
``` public bool IsOnTarget(int dice) { return (barrelLength + dice) > 100; } ```
The entire gun class is as follows:
``` 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; } } ```
Now we make the shells - this is the most obvious case for applying inheritance, but the aggregation in it is also applicable. Any projectile has its own characteristics. Just some hypothetical shells does not happen. Therefore, we make the class abstract. We make it a string type field.
Shells make for guns. For certain guns. A projectile of one caliber will not fire a gun of another caliber. Therefore, we add to the projectile field-link to the instance of the gun We do the designer.
``` public abstract class Ammo { Gun gun; public string type; public Ammo(Gun someGun, string type) { gun = someGun; this.type = type; } } ```
Here we have applied aggregation. A gun will be created somewhere. Then shells that have a pointer to the cannon will be created for this gun.
Specific types of shells will be the heirs of an abstract shell. Heirs can simply inherit the methods of the parent, but they can also be redefined, that is, work differently than the parent method. But we know for sure that any projectile must have a number of methods. Any projectile must do damage. The GetDamage method simply returns a caliber multiplied by three. In general, the damage of the projectile depends on the caliber. But this method will be redefined in child classes (remember that projectiles that penetrate armor well, as a rule, inflict less “zaronevaya” damage. To be able to override the method in the child class, we use the word virtual.
``` public virtual int GetDamage() { //TO OVERRIDE: add logic of variable damage depending on Ammo type return gun.GetCaliber()*3; } ```
Any projectile must pierce (or at least try to pierce) armor. In the general case, the ability to pierce armor also depends on the caliber (well, and also on a lot - the initial speed, for example, but we will not complicate it). Therefore, the method returns a caliber. That is, roughly speaking, a projectile can pierce armor, equal in thickness to its caliber. This method will not be overridden in child classes.
``` public int GetPenetration() { return gun.GetCaliber(); } ```
In addition, for convenient debugging and organization of console output, it makes sense to add the ToString method, which simply allows us to see what kind of projectile and what caliber:
``` public override string ToString() { return $" " + type + " " + gun.GetCaliber(); } ```
Now we will make different types of shells that will inherit an abstract shell: high-explosive, cumulative, sub-caliber. High-explosive causes the greatest damage, cumulative - less, sub-caliber - even less. Child classes have no fields and call the designer of the basic projectile, passing it a gun, and a string type. In the child class, the GetDamage () method is overridden — coefficients are inserted that will increase or decrease the damage compared to the default one.
HE (default damage):
``` public class HECartridge : Ammo { public HECartridge(Gun someGun) : base(someGun, "") { } public override int GetDamage() { return (int)(base.GetDamage()); } } ```
Cumulative (default damage x 0.6):
``` public class HEATCartridge : Ammo { public HEATCartridge(Gun someGun) : base(someGun, "") { } public override int GetDamage() { return (int)(base.GetDamage() * 0.6); } } ```
Subcaliber (default damage x 0.3):
``` public class APCartridge : Ammo { public APCartridge(Gun someGun) : base(someGun, "") { } public override int GetDamage() { return (int)(base.GetDamage() * 0.3); } } ```
Notice that in the overridden method, GetDamage also calls the base class method. That is, by redefining the method, we also retain the ability to access the default method using the keyword base).
So, for the projectiles, we applied both aggregation (cannon in the base class) and inheritance.
Create armor for the tank now. Only inheritance is applicable here. Any armor is thick. Therefore, the abstract armor class will have a field thickness, and a string field type that will be defined when creating child classes.
``` public abstract class Armour { public int thickness; public string type; public Armour(int thickness, string type) { this.thickness = thickness; this.type = type; } } ```
Armor will be defined in our game if they are punched or not. Therefore, it will have only one method, which will be redefined in the child ones, depending on the type of armor.
``` public virtual bool IsPenetrated(Ammo projectile) { return projectile.GetDamage() > thickness; } ```
And whether they are broken or not depends on what kind of projectile has arrived: in the default case, what caliber. Therefore, the method takes a copy of the projectile and returns a boolean result: broken or not. Create several types of armor - the heirs of abstract armor. I will give the code of only one type - the logic is about the same as in the projectiles. Homogeneous armor holds a high-explosive projectile well, but poorly - subcaliber. Therefore, if a sub-caliber projectile, which has high armor penetration, has arrived, then in calculations our armor is getting thinner. And so on: each type of armor has its own set of coefficients of resistance to a particular projectile.
``` 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; } } } ```
Here we use one of the wonders of polymorphism. The method takes any projectile. The signature specifies the base class, not the children. But inside the method, we can see what kind of projectile came - what type. And depending on this, we implement one or another logic. If we did not use inheritance for the shells, but simply made three unique classes of types of shells, then the armor penetration check would have to be organized differently. We would have to write as many overloaded methods as there are types of projectiles in our game, and call one of them depending on which projectile arrived. This would also be quite elegant, but not relevant to the topic of this article.
Now we are ready to create a tank. There will be no inheritance in the tank, but there will be composition and aggregation. Of course, the tank will have a name. The tank will have a gun (aggregation). For our game, we make the assumption that a tank can “change clothes” for armor before each turn - choose one or another type of armor. For this, the tank will have a list of armor types. The tank will have an ammunition - a list of shells that will be filled with shells created in the tank constructor (composition!). The tank will have health (decreases when it hits it), and, the tank will have the current selected armor and the current selected projectile.
``` public class Panzer { private string model; private Gun gun; private List<Armour> armours; private List<Ammo> ammos; private int health; public Ammo LoadedAmmo { get; set; } public Armour SelectedArmour { get; set; } } ```
In order for the tank designer to remain more or less compact, we will make two auxiliary private methods that add three types of armor of the appropriate thickness, and fill the ammo pack with 10 shells of each of the three types:
``` 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)); } } ```
Now the tank designer looks like this:
``` public Panzer(string name, Gun someGun, int armourWidth, int h) { model = name; gun = someGun; health = h; armours = new List<Armour>(); ammos = new List<Ammo>(); AddArmours(armourWidth); LoadAmmos(); LoadedAmmo = null; SelectedArmour = armours[0];
Please note that here we again use the possibilities of polymorphism. Our ammunition contains shells of any type, as the list has the data type Ammo - the parent shell. If we were not inherited, but created unique types of shells, we would have to make a separate list for each type of projectile.
The user interface of the tank consists of three methods: select armor, load a gun, shoot.
Select armor:
``` public void SelectArmour(string type) { for (int i = 0; i < armours.Count; i++) { if (armours[i].type == type) { SelectedArmour = armours[i]; break; } } } ```
Charge the gun:
``` 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 + " !"); } ```
As I mentioned at the beginning, in this example I tried to get away from the abstract concepts that need to be kept in my head as much as possible. Therefore, each copy of the projectile we have is equal to the physical projectile, which was put into combat before the battle. Consequently, the shells can end at the most inopportune moment!
Fire:
``` 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; } ```
Here - in more detail. First, there is a check whether the gun is charged. Secondly, the projectile that flew out of the barrel no longer exists for this tank, it is no longer in the cannon or in the combat pack. But physically, he still exists - flies towards the goal. And if it falls, it will participate in the calculation of the penetration of armor and damage to the target. Therefore, we save this projectile in a new variable: Ammo firedAmmo. Since on the very next line this projectile will cease to exist for this tank, you will have to use the IClonable interface for the projectile base class:
``` public abstract class Ammo : ICloneable ```
This interface requires the implementation of the Clone () method. Here she is:
``` public object Clone() { return this.MemberwiseClone(); } ```
Now everything is super realistic: when a shot is fired, a dice is generated, the gun counts the hit with its IsOnTarget method, and if there is a hit, the Shoot method will return a copy of the projectile, and if it misses, it will return null.
The last method of the tank is its behavior when it hits an enemy projectile:
``` public void HandleHit(Ammo projectile) { if (SelectedArmour.IsPenetrated(projectile)) { this.health -= projectile.GetDamage(); } else Console.WriteLine(" ."); } ```
Again polymorphism in all its glory. A projectile flies to us. Any. Based on the selected armor and the type of projectile, is calculated broken armor or not. If it is pierced, the method of a specific projectile type GetDamage () is called.
All is ready. It remains only to write a console (or non-console) output, in which the user interface will be provided and alternate moves of players are implemented in the cycle.
Let's sum up. We wrote a program in which we used inheritance, composition, and aggregation, I hope, we understood and remembered the differences. Actively involved the possibilities of polymorphism, first, when any instances of the child classes can be added to a list that has the data type of the parent, and second, creating methods that take the parent instance as a parameter, but within which the child methods are called. In the course of the text, I mentioned possible alternative implementations - the replacement of inheritance with aggregation, and there is no universal recipe. In our implementation, inheritance gave us the ease of adding new details to the game. For example, to add a new type of projectile we only need:
- in fact, copy one of the existing types, replacing the name and the string field passed to the constructor;
- add another if to the child armor classes;
- add an additional item in the projectile selection menu in the user interface.
Similarly, to add another type of reservation, you only need to describe this type and add an item to the user interface. Modifying other classes or methods is not required.
Below is a diagram of our classes.

In the final code of the game, all the "magic numbers" that were used in the text are rendered into a separate static class Config. We can access the public fields of a static class from any fragment of our code and it is not necessary (and impossible) to create an instance of it. This is how it looks like:
``` public static class Config { public static List<string> ammoTypes = new List<string> { "", "", "" }; public static List<string> armourTypes = new List<string> { "", "", "" }; // - , , 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; } ```
And thanks to this class, we can make further customization, changing the parameters only here, without further deepening into classes and methods. If, for example, we came to the conclusion that the piercing projectile turned out too strong, then we change one dial in Config.
All the code of the game can be seen
here .