Basic software design principles

Aro Militosyan
21 min readFeb 6, 2023

--

DRY, KISS, YAGNI, SOLID

Hello everyone, I will try to explain about the principles of the software, we will understand what each of them represents and what it is intended for. Software programmers use their own acronyms, some of which are basic principles that provide depth and meaning to a software program. We’ll look at some important acronyms worth knowing and learning about. This article will help you learn their basic concepts, uses, and advantages of abbreviations.

DRY principle (Don’t Repeat Yourself)

First of all, let’s start with the DRY (Don’t repeat yourself) principle.DRY translates as “Don’t Repeat Yourself”. Don’t Repeat Yourself (DRY) is a very popular acronym used by many programmers.Copy-pasting code in multiple places in the same project creates unnecessary complexity. Debugging becomes more difficult, as well as testing.

Let’s create two functions simple_arith and complex_arith as follows.

def simple_arith(num1, num2, operator):
if operator == '+':
return {"result": num1 + num2}
elif operator == '-':
return {"result": num1 - num2}
elif operator == '*':
return{"result": num1 * num2}
elif operator == '/':
return {"result": num1 / num2}


def complex_arith(num1, num2, num3, operator):
if operator == '+':
return {"result": num1 + num2 + num3}
elif operator == '-':
return {"result": num1 - num2 - num3}
elif operator == '*':
return {"result": num1 * num2 * num3}
elif operator == '/':
return {"result": num1 / num2 / num3}

The functions are quite simple. You can go ahead and test them by adding the following line to the end of your .py file.

print(simple_arith(1, 2, '+'))

Everything is fine, but if you try an operator that is not supported, the program crashes with an error.

print(simple_arith('1', 2, '+'))
Traceback (most recent call last):
File "/home/aro/design_principles/DRY.py", line 22, in <module>
print(simple_arith('1', 2, '+'))
File "/home/aro/design_principles/DRY.py", line 3, in simple_arith
return {"result": num1 + num2}
TypeError: can only concatenate str (not "int") to str

In this case, the fix is pretty easy, but the point is that you have to copy and paste it into simpe_arith and complex_arith. However, if we do that, we’ll be duplicating our code. This is bad practice.

For example, this way.

def simple_arith(num1, num2, operator):

try:
int(num1)
except ValueError:
return {'message': 'Invalid input'}

try:
int(num2)
except ValueError:
return {'message': 'Invalid input'}

if operator == '+':
return {"result": num1 + num2}
elif operator == '-':
return {"result": num1 - num2}
elif operator == '*':
return{"result": num1 * num2}
elif operator == '/':
return {"result": num1 / num2}

def complex_arith(num1, num2, num3, operator):

try:
int(num1)
except ValueError:
return {'message': 'Invalid input'}

try:
int(num2)
except ValueError:
return {'message': 'Invalid input'}

try:
int(num3)
except ValueError:
return {'message': 'Invalid input'}

if operator == '+':
return {"result": num1 + num2 + num3}
elif operator == '-':
return {"result": num1 - num2 - num3}
elif operator == '*':
return {"result": num1 * num2 * num3}
elif operator == '/':
return {"result": num1 / num2 / num3}

Note how much code repetition there is. The try-except block repeats 5 times.

Before moving on to the solution, we need to consider one important concept in Python. in particular, higher-order functions.

Passing functions to functions

Before we get into the decorator functions, we need to simplify one approach first. In Python, it is possible to pass a function as an argument to another function.This is due to the fact that Python functions are actually higher-order functions.

Consider the following code.

def foo(arg):
arg()

def foo1():
print('foo1')

foo(foo1)

What do you think the result will be? .The variable arg is a reference to the function foo1, so it acts as a function. So foo1 will be printed.

Decorator functions

A decorator function is nothing but a function that takes a function as an argument. The basic structure is similar to the following.

def decorator(fun):
def wrapper(*args, **kwargs):

return fօօ(*args, **kwargs)
return wrapper

As you can see, the function (foo) is passed as an argument to the decorator function. The wrapper function is then called with *args and **kwargs as arguments.The *args variable is a tuple that contains the arguments passed to the original foo function, while **kwargs is a dictionary that contains the keyword arguments passed to the foo function.

We are getting closer to our DRY principle little by little.

If we use the @ sign before the function definition, the decorator function will wrap around the function being defined. We can remove the error checking logic in simple_arith and add @decorator like this.

@decorator
def simple_arith(num1, num2, operator):

if operator == '+':
return {"result": num1 + num2}
elif operator == '-':
return {"result": num1 - num2}
elif operator == '*':
return{"result": num1 * num2}
elif operator == '/':
return {"result": num1 / num2}

Whenever simple_arith is called, the decorator will be called first, with simple_arith as an argument. Arguments num1, num2, operator will appear as Tuple in *args variable.

Now we can put the logic-checking Try-except in the decorator function like this.

def decorator(fօօ):
def wrapper(*args, **kwargs):

nums = args[:-1]
for arg in nums:
try:
int(arg)
except ValueError:
return {'message': 'Invalid input'}

tu=tuple(map(int, nums))
tu = tu + (args[-1],)
return fօօ(*tu, **kwargs)

return wrapper

We remove the last argument (which is the operator) and store the result in nums. Then we loop over and try to cast to an integer.If it fails, we know the input is wrong, and we can catch the error and notify the user that the input is wrong.

If the input is correct, we cast the numbers from string to int, just to make sure the program doesn’t crash if an input like “1” is passed, we use the map function.

tu=tuple(map(int, nums))

However, we must also attach it to the operator contained in args[-1] , like this.

tu = tu + (args[-1],)

When this is done, we return foo with the modified arguments.

return fօօ(*tu, **kwargs)

Now here comes the miracle. Suppose we want the same logic to be applied to the complex_arith function. What are we doing? We just wrap it up with @decorator like this.

@decorator
def complex_arith(num1, num2, num3, operator):

if operator == '+':
return {"result": num1 + num2 + num3}
elif operator == '-':
return {"result": num1 - num2 - num3}
elif operator == '*':
return {"result": num1 * num2 * num3}
elif operator == '/':
return {"result": num1 / num2 / num3}

Now we can call both simple_arith and complex_arith with non-desired input data, and in both cases the decorator will be called first considering the input data.

Let’s call it as follows.

print(simple_arith("11",5 ,'+'))
print(complex_arith("4",5,"2" ,"*"))

The result will be the following.

/usr/bin/python3.10 /home/aro/design_principles/DRY.py 
{'result': 16}
{'result': 40}

Summary

Decorator functions are useful for creating DRY code that avoids code repetition. They are used when the same logic needs to be applied to multiple functions. Because the types *args and **kwargs are used, we are not limited to functions with one, two, or three arguments.

KISS Principle (Keep it simple, Stupid!)

KISS stands for Keep It Simple, Stupid. The essence of this principle is to make your code simpler. Unnecessary complexity should be avoided. Simple code is easier to read and understand.

You must remove duplicate code, you must remove redundant functions, you must not use redundant variables and methods, you must use variable names and methods that make sense and correspond to their actions.

When you are working on code that contains a piece of code that is not needed or could be simpler, you should consider modifying (refactoring) it.

Summary

What are the benefits of following the KISS principle?

You will be able to solve problems faster,

You will be able to create higher quality code,

The code base will be more flexible, it will be easier to expand, modify or transform when new requirements appear.

Make everything as simple as possible, but not easier.

YAGNI Principle( You Aren’t Gonna Need It)

YAGNI has what is known as the “You Aren’t Gonna Need It” principle, taken from eXtreme Programming, which states that you shouldn’t build functionality in advance, or rather, until you need it.

This principle is similar to the KISS principle in that both aim for a simpler solution. The difference between them is that YAGNI focuses on removing redundant functionality and logic and KISS focuses on complexity.

Summary

Always implement things when you actually need them, don’t implement things when you just anticipate or think you will need them.

S.O.L.I.D Principle

Finally we reached the principles of SOLID. The SOLID principles are an amalgamation of five different software design principles, as listed below.

1) Single Responsibility Principle (SRP)
2) Open/Closed Principle (OCP)
3) Liskov Substitution Principle (LSP)
4) Interface Segregation Principle (ISP)
5) Dependency Inversion Principle (DIP)

This idea was put forward by Robert Martin, also known as “Uncle Bob”.

The goals of applying the SOLID principle are as follows:

Make the code more understandable,

Facilitates code reusability,

Facilitates testing,

Flexible to adapt to script changes,

Single-responsibility principle

S is opened and translated as single-responsibility principle.

There should never be more than one reason for changing a class. That is, each class should have only one responsibility. Basically, a class should have only one purpose, if the functionality of the class needs to change, there should be one reason to do so.

Code:
Let’s imagine that the car dealership sells 3 types of cars. The user can.

Request a car

Test the car

Buy a car

The code is as follows.

class Car:
prices={'BMW': 100000, 'Audi': 200000, 'Mercedes': 300000}
def __init__(self, name):

if name not in self.prices:
print("Sorry, we don't have this car")
return

self.name = name
self.price = self.prices[name]

def testDrive(self):
print("Driving {}".format(self.name))

def buy(self, cash):
if cash < self.price:
print("Sorry, you don't have enough money")
else:
print("Buying {}".format(self.name))

if __name__ == '__main__':
car = Car('BMW')
car.buy(100000)

If the requested machine is not available, the user receives an error. Finally, the user can test drive the car and also buy it. The car can be bought only by offering the full amount.

If you notice, the Car class has many responsibilities, it has to manage the information about the car, as well as the financial costs of buying a car. Thus, it violates the single-responsibility principle.

Now let’s imagine that the car dealership wants to make two changes.

Begins offering customers to pay in installments instead of the full amount up front.

And change the price of the car

Now there are two reasons to change the car class. There should only be one reason for a class to change.

We can easily fix this by creating a separate class called Financials and making the changes listed above.

class Car:
prices={'BMW':200000, 'Audi': 200000, 'Mercedes': 300000}
def __init__(self, name):
if name not in self.prices:
print("Sorry, we don't have this car")
return

self.name = name
self.price = self.prices[name]

def testDrive(self):

print("Driving {}".format(self.name))

class Finances:
def buy(car, cash):

if cash == car.price:
print("Buying {}".format(car.name))
elif cash > car.price/3:
print("Buying {} on installments".format(car.name))
else:
print("Sorry, you don't have enough money")

if __name__ == '__main__':
car = Car('BMW')
Finances.buy(car, 100000)

Note that each of the classes, Car and Financials, has only one reason to change.

Summary:

Let’s try to understand whether we have a problem in the combination of machine transaction and financial functions in the same class. However, it is easier to work with code divided into small parts. Notice that the code is now more loosely coupled than before because we split it into two classes. Each of the classes now has a clearly defined task. The Car class deals only with the car, and the Financials class deals only with financial matters.

The Open-Closed principle

Software may change constantly, it is never complete, there is almost always something to fix or improve.

So the question is how do we create software that adapts to change and is stable at the same time?

The Open-Closed principle is a way to answer this question.

The open-closed principle is in some ways quite related to the single responsibility principle. The essence of this principle is as follows.

Software components (classes and functions) should be open to extension but closed to modification.

The idea is quite simple. The specific class logic in your code should not change. Rather, if an additional feature is to be introduced, the code should be extended rather than modified.

CODE:
Imagine we have the following code.

class Car:
def __init__(self, name, price):
self.name = name
self.price = price

def buy(self, cash):
if cash == self.price:
print("Buying {}".format(self.name))
else:
print("Sorry, you don't have enough money")

def buy_with_discount(self, cash):
if int(0.8*self.price) == int(cash):
print("Buying {} with 20%% discount".format(self.name))

if __name__ == '__main__':
car = Car('BMW', 100000)
car.buy_with_discount(80000)

We are using the same theme as in the previous principle. It is possible to buy a car with the buy function, and it is also possible to buy a car with a 20% discount with the buy_with_discount function.

The cash amount provided to the buy_with_discount function must be exactly 80% of the car’s price in order to buy it.

Now, suppose a car dealer wants to offer some discounts to loyal members of the dealership, VIPs, or acquaintances and friends.😃

As you can see, business needs have changed and code changes are required.

To satisfy the query, we can simply write another if buy_with_discount function that will calculate a 30% discount.

if int(0.7*self.price) == int(cash):
print("Buying {} with 30%% discount".format(self.name))

But this is a wrong approach for the open-closed principle. Because the Car class logic should not change. Instead, it should be expanded. A common way to extend functionality in OOP (Object Oriented Programming) is to use polymorphism, also known as inheritance.

Let’s see how we can use inheritance in Python to solve this problem correctly.

Consider the following code.

class Car:
def __init__(self, name, price):
self.name = name
self.price = price

def buy(self, cash):
if cash == self.price:
print("Buying {}".format(self.name))
else:
print("Sorry, you don't have enough money")

class Discount():
def __init__(self, discount):
self.discount=discount

def buy_with_discount(self, car):
print("Buying {} with {}% discount".format(car.name, self.discount))

class Discount20(Discount):
def __init__(self):
super().__init__(20)

def buy_with_discount(self, cash, car):
if int(0.8*car.price) == int(cash):
return super().buy_with_discount(car)

class Discount30(Discount):
def __init__(self):
super().__init__(30)

def buy_with_discount(self, cash, car):
if int(0.7*car.price) == int(cash):
return super().buy_with_discount(car)

if __name__ == '__main__':
car = Car('BMW', 100000)
Discount20().buy_with_discount(80000, car)
Discount30().buy_with_discount(70000, car)

The first thing we do is separate the discount logic from the normal purchase logic by creating a Discount class. Next, we define the buy_with_discount function, which simply prints that the car has been purchased.

Next, we need to implement the logic of the 20% discount. But remember that we are not allowed to modify the Discount class as per principle. The only thing we can do is expand. How do we scale it up? . Creating a new class and having the new class inherit from the Discount class.

We define a new class Discount20 and inherit it from the Discount class by passing Discount as an argument in the class definition.

The most interesting thing is, let’s say we want to offer a 30% discount. How do we achieve this? We simply create a new class called Discount30 and repeat the code, changing only the values from 20 to 30 and from 0.8 to 0.7.

The code can now run and you will see that we have bought two cars. One with 70% discount, the other with 80% discount.

Summary:

We understood that we should use the open-closed principle so that we do not change the code, but only expand it.

The Liskov Substitution Principle

Before delving into this principle and how it fits with the last two principles.

Let’s look at the definition.

The principle of Liskov’s replacement is as follows: it is necessary for the child classes to be able to replace the parent classes. The purpose of this principle is that child classes can be used instead of the parent classes from which they are derived without crashing the program.

Let’s say you have a subclass that inherits from the base class. Let’s say you make some changes to the subclass, such as changing the arguments, value types, and possibly the return type. In this case, you would violate the LSP principle, because replacing an instance of a superclass with an instance of a subclass would change the principle. Code that depends on the base class may expect a return of type str, for example, but when you replace the instance with a subclass, it returns an int. This can lead to code crashes or other errors.

In general, the parameters and return type of a function in a subclass must be unchanged.

Code:
Let’s look at a simple example.

class Car:
def __init__(self, name):
self.name = name
self.gears = ["N", "1", "2", "3", "4", "5", "6", "R"]
self.speed = 0
self.gear = "N"

def changeGear(self, gear):
if (gear in self.gears):
self.gear = gear
print("Car %s is in gear %s" % (self.name, self.gear))

def accelerate(self):
if (self.gear == "N"):
print("Error: Car %s is in gear N" % self.name)
else:
self.speed += 1
print("Car %s is accelerating" % self.name)

class SportsCar(Car):
def __init__(self, name):
super().__init__(name)
self.turbos = [2, 3]

def accelerate(self, turbo):
if (self.gear == "N"):
print("Error: Car %s is in gear N" % self.name)
else:
if (turbo in self.turbos):
self.speed += turbo
print("Car %s is accelerating with turbo %d" % (self.name, turbo))

if __name__ == '__main__':

car = Car('BMW')
car.changeGear("1")
car.accelerate()


autoCar = SportsCar('Audi')
autoCar.changeGear("1")
autoCar.accelerate()

The Car class defines a car. We initialize it with name, number of gears, speed and current gear position. The ChangeGear function allows you to change gears, while the accelerate function increases the car’s speed by 1. If the transmission is in neutral position N, we do not change the gear, instead we inform the user.

Let’s say we want to model a SportsCar as well. We inherit it from the base class Car. During initialization, we also define a turbos variable, which is a list indicating what turbo levels the car supports.

Because of the behavior change, we need to define a new accelerate function that takes turbo as a parameter. The speed is increased by the turbo amount instead of 1 so that the sports car can go faster.

The problem is, if we try to replace the instance of Car with an instance of SportsCar, it will cause the code to crash, because accelerate in SportsCar expects a turbo argument.

According to the LSP principle, replacing the above should not result in an error and the code should function normally.

Let’s consider two solutions.

Solution 1:

A simple solution would be to replace the turbos variable with a turbo variable with a fixed value. And removing the turbo parameter from the accelerate function like this.

class Car:
def __init__(self, name):
self.name = name
self.gears = ["N", "1", "2", "3", "4", "5", "6", "R"]
self.speed = 0
self.gear = "N"

def changeGear(self, gear):
if (gear in self.gears):
self.gear = gear
print("Car %s is in gear %s" % (self.name, self.gear))

def accelerate(self):
if (self.gear == "N"):
print("Error: Car %s is in gear N" % self.name)
else:
self.speed += 1
print("Car %s is accelerating" % self.name)

class SportsCar(Car):
def __init__(self, name):
super().__init__(name)
self.turbo = 2
#Այս կետում փոխարինեցինք turbos-ը ֆիքսված արժեք ունեցող turbo փոփոխականով

def accelerate(self):
if (self.gear == "N"):
print("Error: Car %s is in gear N" % self.name)
else:
self.speed += self.turbo
print("Car %s is accelerating with turbo %d" % (self.name, self.turbo))

if __name__ == '__main__':

car = Car('BMW')
car.changeGear("1")
car.accelerate()

car = SportsCar('BMW')
car.changeGear("1")
car.accelerate()

As you can see, the function arguments are now exactly the same as the base class function arguments. This means we can replace the Car class code in the __main__ function with SportsCar without the code crashing.

However, if you notice that in this case we have a fixed turbo value, what if we want the user to set the turbo value as was possible before? It is required to use abstract class.

Solution 2 Abstract class:

The problem is how we modeled the machines. In order for the above code to conform to the LSP and have turbo selection functionality, we need to create an abstract base class Car.

Then we create two separate subclasses that inherit from it: SportsCar and RegularCar. The diagram below shows.

The code will be as follows.

from abc import ABC, abstractmethod

class Car(ABC):
@abstractmethod
def __init__(self, name):
self.name = name
self.gears = ["N", "1", "2", "3", "4", "5", "6", "R"]
self.speed = 0
self.gear = "N"

def changeGear(self, gear):
if (gear in self.gears):
self.gear = gear
print("Car %s is in gear %s" % (self.name, self.gear))

def accelerate(self):
if (self.gear == "N"):
print("Error: Car %s is in gear N" % self.name)
else:
self.speed += 1
print("Car %s is accelerating" % self.name)

class RegularCar(Car):
def __init__(self, name):
super().__init__(name)

class SportsCar(Car):
def __init__(self, name):
super().__init__(name)
self.turbos = [2, 3]

def turboAccelerate(self, turbo):
if (self.gear == "N"):
print("Error: Car %s is in gear N" % self.name)
else:
if (turbo in self.turbos):
self.speed += turbo
print("Car %s is accelerating with turbo %d" % (self.name, turbo))

if __name__ == '__main__':

car = RegularCar('BMW')
car.changeGear("1")
car.accelerate()

autoCar = SportsCar('Audi')
autoCar.changeGear("1")
autoCar.turboAccelerate(2)

We turn Car into an abstract class using the ABC module. The __init__ function is assigned @abstractmethod to express an abstract function.

This ensures that the Car object cannot be instantiated. Can only be created from classes that inherit from Car.

We create two classes that inherit from Car.

RegularCar showing a regular car without a turbo.

SportsCar showing a sports car with turbos.

RegularCar has no additional changes. In the SportsCar class, we can make the changes we want. We define an array of turbos and a turboAccelerate function that contains an additional turbo parameter.

In this case, changing the function parameter in SportsCar does not break the LSP. Why, since the base class of Car is abstract, it cannot be instantiated. If it cannot be instantiated, it cannot be overridden in subclasses.

Summary:

We learned what Liskov substitution principle is for. We saw an example that violates the principle and there are two ways to solve it. LSP helps make code flexible and prevents problems when old code is extended by new code. LSP ensures that code behavior remains the same in both new and old code bases, resulting in less code crashes.

The Interface Segregation Principle

In this post, we’ll explore the I of the SOLID acronym, which represents the principle of interface isolation.

Customers should not be forced to depend on methods they do not use. The source: Agile Software Development, Robert C. Martin

The principle is similar to the single responsibility principle in that classes should only implement methods that they actually use.

Imagine we have a subclass that implements all the methods in the base class. But for a particular method, the function body throws an exception because we don’t want that method to be callable at all. That one particular method contradicts the behavior definition of the class and technically shouldn’t be implemented. This would be a violation of the ISP principle.

You might wonder why a subclass should implement a method it doesn’t need. See, this principle is about interfaces. When an interface is inherited, all existing methods must be implemented.

Before moving on to the next topic, let’s talk a little about interfaces and duck typing .

Interfaces, abstract classes and duck typing in Python

Interfaces are software constructs that define and enforce what methods an object of a class should have. There is no class interface in Python.

To create an interface in Python, duck typing is used, that is, we use an abstract class to represent the interface. In other words, if the defined class behaves like an interface, then we can use it like an interface.

The key point here is that we treat our abstract class as an interface (having empty method bodies and declaring methods abstract).

Code:

The interface can be considered a completely abstract (abstract) class. Therefore, we mark all methods of the abstract class with @abstractmethod.

Consider the following code.


from abc import ABC, abstractmethod


class Car(ABC):
@abstractmethod
def __init__(self, name):
"""Please implement intialization of a car"""

@abstractmethod
def accelerate(self):
"""Please implement accelerating of a car"""

@abstractmethod
def turboAccelerate(self, turbo):
"""Please implement turboaccelerating of a car"""


class RegularCar(Car):
def __init__(self, name):
self.name = name
self.speed = 0

def accelerate(self):
self.speed += 1
print(f"Car {self.name} is accelerating" )

def turboAccelerate(self, turbo):
raise Exception("Regular car has no turbo")


class SportsCar(Car):
def __init__(self, name):
self.name = name
self.speed = 0

def accelerate(self):
self.speed += 1
print(f"Car {self.name} is accelerating")

def turboAccelerate(self, turbo):
self.speed += turbo
print(f"Car {self.name} is accelerating with turbo {turbo}")


if __name__ == '__main__':
car = RegularCar('BMW')
car.accelerate()

autoCar = SportsCar('Audi')
autoCar.turboAccelerate(2)

###This line will throw an exception
###This is an ISP violation because the class contains an unused function
car.turboAccelerate(2)

We define the abstract base class Car. Again, this can be considered an interface because all three methods are defined as abstract.

Next step, we create two subclasses: RegularCar and SportsCar. Both of these classes inherit the methods defined in Car . However, notice that RegularCar does not have a turbo, so we throw an exception if turboAccelerate is called on a RegularCar object.

We can inspect the code in the __main__ function and see that the call to turboAccelerate does indeed throw an exception. Everything else works as expected.

Now you might be wondering why define the turboAccelerate function for RegularCar if it’s not supposed to be used anyway.

This is the principle of ISP. We should not force RegularCar to implement turboAccelerate if it is not supported.

Also, the second problem is code maintenance. Imagine we need to change something in the turboAccelerate function. Suppose we need to pass an additional parameter to the function. If we make this change to the interface, we also need to make a change to RegularCar, even though the function just throws an exception. In other words, we have to make useless changes just to compile the code.

Let’s try to fix the code.

Solution

from abc import ABC, abstractmethod


class NoTurbo(ABC):
@abstractmethod
def __init__(self, name):
"""Please implement intialization of a car"""

@abstractmethod
def accelerate(self):
"""Please implement accelerating of a car"""


class WithTurbo(NoTurbo):
@abstractmethod
def turboAccelerate(self):
"""Please implement turboaccelerating of a car"""


class RegularCar(NoTurbo):
def __init__(self, name):
self.name = name
self.speed = 0

def accelerate(self):
self.speed += 1
print(f"Car {self.name} is accelerating")


class SportsCar(WithTurbo):
def __init__(self, name):
self.name = name
self.speed = 0

def accelerate(self):
self.speed += 1
print(f"Car {self.name} is accelerating")

def turboAccelerate(self, turbo):
self.speed += turbo
print(f"Car {self.name} is accelerating with turbo {turbo}")


if __name__ == '__main__':
car = RegularCar('BMW')
car.accelerate()

autoCar = SportsCar('Audi')
autoCar.turboAccelerate(2)

So. Our goal was to remove turboAccelerate from RegularCar. But we can’t do that in code because an interface contains a method, and all interface methods must be implemented in a subclass.

Therefore, the solution is to create two interfaces. One is called NoTurbo and the other is WithTurbo. We just make turboAccelerate a function in WithTurbo and not in NoTurbo. A normal car uses NoTurbo, while a turbo car uses WithTurbo.

Problem solved…

As you can see now, RegularCar and SportsCar don’t implement any useless functions that they won’t need.

Summary:

In this post, you learned about the principle of interface separation. Basically, the principle says that a class should not be forced to implement functions that it does not use.

Dependency inversion principle:

We have finally reached the last of the principles. Principle 5 is called the Dependency Inversion Principle. The definition has two parts…

Top-level modules should not depend on lower-level modules. Both must depend on abstractions.

Abstractions should not depend on details. Details must depend on abstractions

Time for some code and a concrete example.

Code:
Consider the following example where we are modeling a robot.

class Apple:
def eat(self):
print(f"Eating Apple. Transferring {5} units of energy to brain...")

class Robot:
def get_energy(self):
apple = Apple()
apple.eat()

if __name__ == '__main__':
robot = Robot()
robot.get_energy()

The Robot class has only one function get_energy. We implement the Apple that the robot can get power from.

Now at some point the robot will get tired of eating apples. So we add the Chocolate class to give the robot more options to eat.

class Apple:
def eat(self):
print(f"Eating Apple. Transferring {5} units of energy to brain...")

class Chocolate:
def eat(self):
print(f"Eating Chocolate. Transferring {10} units of energy to brain...")

class Robot:
def get_energy(self, eatable: str):
if eatable == "Apple":
apple = Apple()
apple.eat()
elif eatable == "Chocolate":
chocolate = Chocolate()
chocolate.eat()

if __name__ == '__main__':
robot = Robot()
robot.get_energy("Apple")

However, notice what happens with the get_energy method. As a parameter we need to pass a string that indicates what the robot should eat. For example, if we introduce an additional parameter to the eat method in Chocolate, the get_energy code will be broken. These problems are caused by tight coupling and strong dependencies, and this is a violation of the principle of dependency inversion.

Let’s look at the following diagram.

Solution

To solve this problem, we need to introduce the abstract method by modifying the UML diagram as follows.

And the code will look like this.

from abc import ABC, abstractmethod

class Eatable(ABC):
@abstractmethod
def eat(self):
return NotImplemented

class Apple(Eatable):
def eat(self):
print(f"Eating Apple. Transferring {5} units of energy to brain...")

class Chocolate(Eatable):
def eat(self):
print(f"Eating Chocolate. Transferring {10} units of energy to brain...")

class Robot:
def get_energy(self, eatable: Eatable):
eatable.eat()

if __name__ == '__main__':
robot = Robot()
robot.get_energy(Apple())

We’re building an Eatable interface that’s implemented by both Apple and Chocolate.

We change the get_energy method argument so that it expects an argument of type Eatable instead of str. All edible classes implement the Eatable interface, we are sure that the code will not crash if Chocolate or Apple are changed.

Summary:

So we studied the principle of dependence inversion. The principle essentially states that higher-level modules should not depend on lower-level modules. Instead, both must depend on abstraction. We need to create higher-level modules regardless of implementation specifics in lower-level modules.

We have finally reached the end!

I hope you enjoyed this article.

Thank you for

--

--