Inheritance
If you know basic OOP, you know what Inheritance is. When one class extends another, the child class inherits the parent class and thus the child class has access to all the variables and methods on the parent class.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
class Duck: speed = 30 def fly(self): return "Flying at {} kmph".format(self.speed) class MallardDuck(Duck): speed = 20 if __name__ == "__main__": duck = Duck() print(duck.fly()) mallard = MallardDuck() print(mallard.fly()) |
Here the MallardDuck
extends Duck
and inherits the speed
class variable along with the fly
method. We override the speed
in the child class to suit our needs. When we call fly
on the mallard duck, it uses the fly
method inherited from the parent. If we run the above code, we will see the following output:
1 2 |
Flying at 30 kmph Flying at 20 kmph |
This is inheritance in a nutshell.
Composition
Let’s first see an example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
class GmailProvider: def send(self, msg): return "Sending `{}` using Gmail".format(msg) class YahooMailProvider: def send(self, msg): return "Sending `{}` using Yahoo Mail!".format(msg) class EmailClient: email_provider = GmailProvider() def setup(self): return "Initialization and configurations" def set_provider(self, provider): self.email_provider = provider def send_email(self, msg): print(self.email_provider.send(msg)) client = EmailClient() client.setup() client.send_email("Hello World!") client.set_provider(YahooMailProvider()) client.send_email("Hello World!") |
Here we’re not implementing the email sending functionality directly inside the EmailClient
. Rather, we’re storing a type of email provider in the email_provider
variable and delegating the responsibility of sending the email to this provider. When we have to send_email
, we call the send
method on the email_provider
. Thus we’re composing the functionality of the EmailClient
by sticking composable objects together. We can also swap out the email provider any time we want, by passing it a new provider to the set_provider
method.
Composition over Inheritance
Let’s implement the above EmailClient
using inheritance.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
class EmailClient: def setup(self): return "Initializions and configurations!" def send_email(self, msg): raise NotImplementedError("Use a subclass!") class GmailClient(EmailClient): def send_email(self, msg): return "Sending `{}` from Gmail Client".format(msg) class YahooMailClient(EmailClient): def send_email(self, msg): return "Sending `{}` from YMail! Client".format(msg) client = GmailClient() client.setup() client.send_email("Hello!") # If we want to send using Yahoo, we have to construct a new client yahoo_client = YahooMailClient() yahoo_client.setup() yahoo_client.send_email("Hello!") |
Here, we created a base class EmailClient
which has the setup
method. Then we extended the class to create GmailClient
and YahooMailClient
. Things got interesting when we wanted to start sending emails using Yahoo instead of Gmail. We had to create a new instance of YahooMailClient
for that purpose. The initially created client
was no longer useful for us since it only knows how to send emails through Gmail.
This is why composition is often favoured over inheritance. By delegating the responsibility to the different composable parts, we form loose coupling. We can swap out those components easily when needed. We can also inject them as dependencies using dependency injection. But with inheritance, things get tightly coupled and not easily swappable.