Of the three key principles of OOP, inheritance brings the most problems. In this tip, I will try to explain why this is so and show three key examples of avoiding inheritance problems while observing all the principles of OOP.
Introduction
I try to show three key examples of avoiding inheritance problems while observing all the principles of OOP. The key principles of OOP, by which an application with an OOP approach is built, can be so contradictory that using them has more cons than pros. Of the three key principles of OOP, inheritance brings the most problems.
Background
Let's describe what inheritance in OOP is. Inheritance is an object-oriented programming concept whereby an abstract data type can inherit the data and functionality of some existing type, facilitating the reuse of software components. Inheritance is one of the principles that helps us implement polymorphism, but polymorphism, in principle, can be implemented without inheritance.
Using the Code
Let's write a couple of classes that describe different animals and they are inherited from the general class, and that is, we create an abstract
class, Animal
. Let's take a look at the code for our example, and the code is here:
abstract class Animal
{
public abstract void Voice();
}
class Cat : Animal
{
public override void Voice()
{
Console.WriteLine("meow");
}
}
class WhiteBear : Animal
{
public override void Voice()
{
Console.WriteLine("Bugaga");
}
}
class Horse : Animal
{
public override void Voice()
{
Console.WriteLine("Hee-haw");
}
}
Everything seems to be fine. We created a base abstract
class Animal
and then overridden the method Voice()
to create sounds in the descendants (Cat
, WhiteBear
, Horse
). Next, let's add some functionality for moving in our classes. To do this, we go into our abstract
class and create another virtual method and add this method to all child classes. The code is given below:
abstract class Animal
{
public abstract void Voice();
public abstract void Walk();
}
class Cat : Animal
{
public override void Voice()
{
Console.WriteLine("meow");
}
public override void Walk()
{
Console.WriteLine("I'm walking");
}
}
class WhiteBear : Animal
{
public override void Voice()
{
Console.WriteLine("Bugaga");
}
public override void Walk()
{
Console.WriteLine("I'm walking");
}
}
class Horse : Animal
{
public override void Voice()
{
Console.WriteLine("Hee-haw");
}
public override void Walk()
{
Console.WriteLine("I'm walking");
}
}
Yes, now our animals can move. In abstact
class Animal
, we made the method Walk()
and this method was overloaded in classes Cat
, WhiteBear
, Horse
. Then, we understand that a polar bear can also swim, and moreover, a horse can also, but a cat cannot do it. Let's add a new method in our class Animal
. Below is the code:
abstract class Animal
{
public abstract void Voice();
public abstract void Walk();
public abstract void Swim();
}
Now we need to inherit the method in all classes, which does not triple us. Alternatively, we can make the implementation of each method in a separate class. The code is as follows:
class Program
{
abstract class Animal
{
public abstract void Voice();
}
abstract class WalkingAnimal : Animal
{
public abstract void Walk();
}
abstract class SwimmingAndWalkingAnimal : WalkingAnimal
{
public abstract void Swim();
}
class Cat : WalkingAnimal
{
public override void Voice()
{
Console.WriteLine("meow");
}
public override void Walk()
{
Console.WriteLine("I'm walking");
}
}
class WhiteBear : SwimmingAndWalkingAnimal
{
public override void Swim()
{
Console.WriteLine("I'm Swimming");
}
public override void Voice()
{
Console.WriteLine("Bugaga");
}
public override void Walk()
{
Console.WriteLine("I'm walking");
}
}
class Horse : SwimmingAndWalkingAnimal
{
public override void Swim()
{
Console.WriteLine("I'm Swimming");
}
public override void Voice()
{
Console.WriteLine("Hee-haw");
}
public override void Walk()
{
Console.WriteLine("I'm walking");
}
}
Here, we tried to make a classification, but this way is not optimized for us and it can make more cons for us than pros. This is just one example of how adding only one method in the base class leads to changes in the entire functionality. And this is just an example from textbooks, not a real development, and we had to fence additional abstract
classes to implement floating animals. It's good that C# doesn't have multiple inheritance. For trying to optimize our code, we can try use interfaces of classes. Let's write all the characteristics in the interfaces. The code is as given below:
class Program
{
interface IAnimal
{
void Sound();
}
interface IWalkingAnimal
{
void Walk();
}
interface ISwimmingAnimal
{
void Swim();
}
class WhiteBear : IAnimal, IWalkingAnimal, ISwimmingAnimal
{
public void Sound()
{
Console.WriteLine("Bugaga");
}
public void Swim()
{
Console.WriteLine("I'm Swimming");
}
public void Walk()
{
Console.WriteLine("I'm walking");
}
}
static void Main(string[] args)
{
WhiteBear whiteBear1 = new WhiteBear();
whiteBear1.Sound();
whiteBear1.Walk();
whiteBear1.Swim();
}
However, our code is still not optimal. There is a duplicate code problem. But it can be noted that for each type of animal
, each action must be described, which forces us to write the same code.
Using Strategy Pattern
One method for solving the problem of adding new methods to existing code is to use the strategy method. Let's optimize our code and get rid of duplication. The code is here:
class Program
{
interface IAnimal { }
interface IAction
{
void Action();
}
interface IWalk
{
void Walk();
}
interface ISwim
{
void Swim();
}
class SwimAction : IAction
{
void IAction.Action()
{
Console.WriteLine("I'm swimming");
}
}
class WalkAction : IAction
{
void IAction.Action()
{
Console.WriteLine("I'm walking");
}
}
class WhiteBear : IAnimal, IWalk, ISwim
{
IAction walkAction;
IAction swimAction;
public WhiteBear()
{
walkAction = new WalkAction();
swimAction = new SwimAction();
}
public void Swim()
{
swimAction.Action();
}
public void Walk()
{
walkAction.Action();
}
}
static void Main(string[] args)
{
WhiteBear whiteBear1 = new WhiteBear();
whiteBear1.Swim();
whiteBear1.Walk();
Console.ReadKey();
}
Here, we have implemented the Strategy pattern. We will customize specific navigation strategies to the base interface. The result is in image 1.
Image 1 - Result of Strategy pattern
Using Contain Methods in Interfaces
In C# 8, it became possible for interfaces to contain methods by default. This helps us write code without duplication and will allow us to add new methods without a single problem. Let's create an application that supports C# 8 and higher. The code is as follows:
class Program
{
interface IAnimal { }
interface ISwim
{
void Swim()
{
Console.WriteLine("I'm swimming");
}
}
interface IWalk
{
void Walk()
{
Console.WriteLine("I'm walking");
}
}
class WhiteBear : ISwim, IWalk { }
static void Main(string[] args)
{
WhiteBear whiteBear1 = new WhiteBear();
ISwim swim = (ISwim)whiteBear1;
swim.Swim();
IWalk walk = (IWalk)whiteBear1;
walk.Walk();
}
}
The result is shown in image 2:
Image 2 - Result of implementation of functionality in interfaces
It is worth noting that this method is suitable for applications written in C# 8 and above.
Using Traits
Moreover, we have a more interesting solution. There, we can use traits. Let's look at the code.
abstract class Animal { }
interface IAnimal<T> where T : Animal { }
class Walk : Animal { }
class Swim : Animal { }
class WhiteBear : IAnimal<Walk>, IAnimal<Swim> { }
static class AnimalStatic
{
public static void WalkAction(this IAnimal<Walk> trait)
{
Console.WriteLine("I'm walking");
}
public static void SwimAction(this IAnimal<Swim> trait)
{
Console.WriteLine("I'm swimming");
}
}
class Program
{
static void Main(string[] args)
{
WhiteBear whiteBear1 = new WhiteBear();
whiteBear1.SwimAction();
whiteBear1.WalkAction();
}
}
We will have the same result. The whole point here lies in the use of the static
class. The result is shown in image 3.
Image 3 - Result of using traits
Conclusion
In this post, I showed you the problem of inheritance and solution to this problem. You can use these three solutions in your application and easily add a new method in your abstract
class.
History
- 15th March, 2021: Initial version