DEV Community

Cover image for Inheritance Vs Delegation
Kinanee Samson
Kinanee Samson

Posted on

Inheritance Vs Delegation

If you are an OOP extremist then you would religiously stick to the tenets that define what OOP is, When we want to model relationship in data, we often pick a hierarchical structure, sticking to classical Inheritance, but recently my use of TypeScript and other Object Oriented Programming Language has taught me other ways to get around having to inherit from other classes. It makes more sense in some situation to handle functionality of a class to another, take for instance we want to implement an alarm system? Or we want to use an even higher level of abstraction.

In this article we are going to be exploring what each is in detail then we will also look at situations where it makes more sense to use one approach over the other. You should know that there is no ultimate solution to things, it all comes down what we are willing to settle for and how well it fits the situation at hand.

These principles follow through irrespective of the language you use, it could be python, dart or Typescript and you could still achieve the same result, it all lies in how we visualize the relationships that exists between our classes. So in no particular order let's dive in.

Inheritance

Inheritance in classical OOP is a term used to describe a situation where class B which is called the child class will inherit the methods and propertied declared on class A which is called the parent class. The idea behind Inheritance is to avoid code duplication, there's no point to create a new class again with the same properties and methods as one you already have? it could lead to all sorts of problems when you have to modify the code. A good aspect of inheritance is that it leans towards POLYMORPHISM, a child class is not tied to the parent class is a rigid manner.

The child class can have it's own implementation of methods declared on the parent class, however for maintenance sake it is advised that there should be some form of consistency between both implementation. The child class could also be doing something entirely different from what's it's parent is doing, mimicking what we see in reality. The child class could also maintain it's own custom attributes and methods, they could also be private to that class.

void main() {
  Person sam = Person('sam', 23);
  Admin superAmdin = Admin('supes', 900000, '2');
  sam = Person('Kalashin', 23);
  print(sam.getProfile());  // {name: Kalashin, age: 23}
  print(superAmdin.getProfile()); // {name: supes, age: 900000}
  superAmdin.haveBreak(); // supes is having some good break
}

class Person {
  String name;
  int age;

  Person(this.name, this.age);

  Map<String, String> getProfile() {
    return {'name': name, 'age': '$age'};
  }
}

class Admin extends Person {
  String level;
  Admin(String name, int age, String level)
      : level = level,
        super(name, age);

  haveBreak() {
    print('$name is having some good break');
  }
}

Enter fullscreen mode Exit fullscreen mode

In the code block above we created a simple Person that class served as the parent class because later we created another class Admin that inherited from Person, you can also see how we customise the constructor of the Admin, and we can give an admin level, if we wanted to we could override the implementation of that method. There are several types of inheritance which we will discuss briefly. Let's look at the use case for this first, if you were implementing a custom authentication this would be a good situation, the Admin could share the same logic for authentication with the admin. If need be, the authentication of the admin could change to match it's need without breaking the code.

Single Inheritance

This type of inheritance occurs when a child class inherits from only one parent class, the example we treated above is a fine example for single inheritance.

Multiple Inheritance

Multiple Inheritance occurs where a child class is inheriting from more than one parent class, this is practise occurs if you apply the Interface segregation Principle, and as such, you might need to inherit multiple small behavioural classes to tie certain features to a particular class. It is quite unfortunate that dart doesn't support multiple inheritance however we can create a super class that can inherit from another class, creating a hierarchical structure.

void main() {
  Car Toyota = Car('16 HP Engine');
  Boat dummyBoat = Boat('Fish Engine');

  Toyota.reverse();
  dummyBoat.dropAnchor();
}

class Accelerate extends Break {
  accelerate() {
    print('im accelerating');
  }
}

class Break {
  canBreak() {
    print('im can break.');
  }
}

class AutoMobile extends Accelerate {
  AutoMobile(String engine);
}

class Boat extends AutoMobile {
  Boat(String engine) : super(engine);

  dropAnchor() {
    print('im dropping my anchor');
  }
}

class Car extends AutoMobile {
  Car(String engine) : super(engine);
  reverse() {
    print('i can reverse');
  }
}

Enter fullscreen mode Exit fullscreen mode

Multiple inheritance in dart leads to hierarchical inheritance. in hierarchical inheritance class b will inherit from class a and in turn class c inherits from class b. this ensures a strict relationship tree can be modelled. You often hear the term "this is a type of". This is because class c can call methods that class a has defined in it because class c parent class, class b is a child of class a. That is not demonstrated above but is totally okay. We have briefly considered inheritance, let's see how delegation works.

Delegation

Delegation is a term used to describe a situation where a class receiver will delate another class sender to evaluate some of it's method or properties, if looked at blindly you will tend to think that there exists some kind of hierarchical relationship between the receiver and the sender, this is not the case and if your code is modelled in this format you should be refactoring to an Inheritance approach. Delegation is designed to lean your code towards a behavioural approach and as such, the relationship between both parties should be arbitrary to enable your code maintain flexibility.

void main() {
  Ringer ElecticBell = Ringer();

  Alarm fireAlarm = Alarm(ElecticBell, 'fire alarm');

  fireAlarm.Notify(); // fire alarm is ringing
}

class Ringer {
  ring(Alarm alarm) {
    print('${alarm.type} is ringing');
  }
}

class Alarm {
  String type;
  Ringer ringer;
  Alarm(this.ringer, this.type) {}

  Notify() {
    ringer.ring(this);
  }
}

Enter fullscreen mode Exit fullscreen mode

You can see in the example above when we call ringer.ring() we pass in this which represent an instance of that class, the ring() function is expecting an alarm as an argument because it needs to log out the type type of the alarm system. The Alarm delegates the computation of the type property to the ringer and as such the ringer has access to it. If it were to be a function we could also compute result of that function inside the sender which in this instance is the Ringer class, while the receiver is the Alarm class. We can create another type of alarm system, say an intruder alert alarm, although it would still very much make use of a Ringer but this time it's own type is logged out. If they were function calls we could get very different results.

void main() {
  Ringer ElecticBell = Ringer();

  SuperAlarm intruderAlarm = SuperAlarm(ElecticBell, 'intruder alarm', 7);

  intruderAlarm.Notify();     // Threat level - 7
                            // intruder alarm is ringing
}

class SuperAlarm extends Alarm {
  int threatLevel;
  SuperAlarm(Ringer ringer, String type, int threatLevel)
      : this.threatLevel = threatLevel,
        super(ringer, type);

  @override
  Notify() {
    print('Threat level - $threatLevel');
    return super.Notify();
  }
}

Enter fullscreen mode Exit fullscreen mode

You see how we can customize the ring function to get different results, as we extend our classes. Note that the Ringer is not tightly coupled to the Alarm class, rather what it is interested in is an instance of an alarm, this allows us to call the ring method on the Ringer from anywhere as long as we have an instance of an alarm. But the Alarm needs a Ringer before it is created. This leads us from the "is a type of " relationship to the "has a" type of relationship. Here we can see that every alarm has a ringer right?

Which one of them makes more sense to you? Does it seem more proper for an alarm to inherit the properties of the ringer and say "an alarm is a rigner"? or an alarm should delegate information about itself to it's ringer and say that "an alarm has a ringer?" I would love to hear your thoughts below, thank you.

Discussion (0)