S O L I D Python

Tasnuva Zaman
Nerd For Tech
Published in
9 min readJul 1, 2021

--

“If builders built buildings the way programmers wrote programs, then the first woodpecker that came along would destroy civilization”. (Gerald Weinberg)

Hi builders, the buildings should be SOLID, pure SOLID ;) .

S O L I D

In software engineering we write efficient algorithms for an efficient application. Once the algorithms got in place the most vital and crucial thing would be to ensure that the application is designed with best designing and architectonic practices. SOLID is said to be one of those best designing practices.

The term SOLID describes a collection of design principles for better coding style that was invented by Uncle Bob! Oh do you know who Uncle Bob is? Well, he is none other than Robert C. Martin, you are right, the father of clean code.

“ Walking on water and developing software from a specification are easy if both are frozen. “ — Edward V. Berard

source: unsplash. credit goes to @templecerulean

When you are following SOLID you are actually writing simpler, more easily understandable, maintainable and expandable code which going to save a lot of time of your team mates. Specially when a group of people are working together on a larger code base with hundred of thousands lines SOLID turns into the necessity. Let’s jump to the topic right now.

The acronym SOLID actually covers 5 vital principles:

  • The Single-Responsibility Principle (SRP)
  • The Open-Closed Principle (OCP)
  • The Liskov Substitution Principle (LSP)
  • The Interface Segregation Principle (ISP)
  • The Dependency inversion Principle (DIP)

Let’s start with Single-Responsibility principle

Single-Responsibility principle

Single-Responsibility principle or SRP states that each class or function should handle a single task. That means if you define a class to load data, you should not use the same class to update your db. Each function will have a single responsibility and hence a single reason to change. So, if your class or function loads data, modifies and also plots them, all before returning its results stop there right now. Robert C. Martin explains this as:

“A class should have only one reason to change”.

So, just because you can add everything you want into your class or function doesn’t mean that you should.

Let’s have an example of PhoneBook. Our PhoneBook contains only name and number of person. It’s primary responsibilities will be managing the contact numbers. Hence, the functionalities of our class will be adding new contact, deleting existing contact, change a contact number also search a number by name. So our PhoneBook class may look like this:

Now, let’s say we have another requirement of saving all data of contacts to a database , so we are adding another function to save contacts to a db , also we want to save data to a csv file . Let’s add functionality of storing data to csv file also.

Wait! are we breaking the principle of single responsibility? Yes we are!

while adding the extra functionalities of storing data to db and csv we are actually breaking the principle of single responsibility. Because, by adding these functionalities we are burdening our class with extra responsibilities apart from it’s primary responsibilities. In future if there require any changes related to saving data to db those can change our class PhoneBook also. Hence the class is prone to change apart from it’s primary responsibilities.

The Single Responsibility Principle simply asks us not to add additional responsibilities to a class so that we don’t have to modify a class unless there is a change to its primary responsibility.

Let’s fix the violation. All we need to do is define separate classes that would handle database storing and saving to csv . we will pass the contact objects to those classes.

Open-Closed Principle:

Open-Closed Principle also called Meyer’s open-closed principle was first conceptualized by Berterd Meyer in 1988 in his book Object Oriented Software Construction. Uncle Bob mentioned this as “the most important principle of object-oriented design”. the open–closed principle states:

software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification

that is, such an entity can allow its behavior to be extended without modifying its source code.

  • A module will be said to be open if it is still available for extension. For example, it should be possible to add fields to the data structures it contains, or new elements to the set of functions it performs.
  • A module will be said to be closed if [it] is available for use by other modules. This assumes that the module has been given a well-defined, stable description (the interface in the sense of information hiding).

This principle ensures that a class is defined to do what it is supposed to do.

If we want to add any further features we can do it by creating new entities that extends the existing class’s features and add more features to itself. Thus preventing frequent and trivial changes to a well-established low-level class.

Let’s say we have an application for H&M. One of the features of our system is to apply discounts based on apparel type. Nice, so in the below example we have a class CalculateDiscount which has a property to hold the type of cloths. It has a function that calculates the discount based on the type of cloths and returns the discounted amount.

This design clearly breaches the Open-Closed principle as it will need modification if:

  1. a new apparel type is to be included in discount list
  2. the discount amount for any apparel changes.

To avoid this violation what we can do is create a class with a abstract method apply_discount . Let’s look at the example:

Here, we have class CalculateDiscount with a single abstract method apply_discount We have individual classes for individual apparel which extends CalculateDiscount , so each sub class needs to implement apply_discount by it’s own which avoids the constraints of modification of base class. We can add new products and change the discounted amount if needed without changing base class.

Liskov Substitution Principle

Liskov Substitution Principle or LSP states that:

“Objects in a program should be replaceable with instances of their subtypes without altering the correctness of the program”

Liskov substitution principle (LSP) is a particular definition of a subtyping relation, called (strong) behavioral subtyping, that was initially introduced by Barbara Liskov in a 1988. The principle states that:

if S is a subtype of T, then objects of type T may be replaced with objects of type S (i.e., an object of type T may be substituted with any object of a subtype S) without altering any of the desirable properties of the program (correctness, task performed, etc.)

Let’s we have a base class Animal which indicates the type of animals. The Cat class is inherited from Animal. Similarly the Cat class can also be inherited by other classes.

suppose we will have to find out all the cat’s with white color. let’s define a function to get all cats with white color and run the program:

Note: As there is no standard specification to add properties of the Animal. You can implement it as a dictionary and your team mate can use as tuple, hence it can be implemented as multiple way

The output will look like this:

Here, we can see while looping through the list of animals, we are breaking liskov substitution principles as we cannot replace Super type Animal’s objects with objects of Subtype Cat in the function written to get white cats.

The better approach could be introduce setter and getter methods in the Superclass Animal using which we can set and get Animal's properties without leaving that implementation to individual comfort.

So, we can comply Liskov’s substitution principle using getter and setter as shown below:

Interface Segregation Principle

The Interface Segregation Principle states that,

“No client should be forced to depend on methods it does not use”.

Robert C Martin introduced Interface Segregation Principle while he was consulting for Xerox. This principle suggests smaller interfaces known as “role interfaces”. ISP splits larger interfaces into smaller role interfaces. So client will depend on the methods relevant to them.

Note: this example is derived from this blog posted by Hiral Amodia

Let’s say we are designing an application for different communication devices.
Most common feature of a communication device are:
— a) make calls, b). send SMS and c). browse the Internet. So, Let’s create an interface named CommunicationDevice and add the respective abstract methods for each of these features such that any implementing class would need to implement these methods.

So far so good. But now say that we want to implement a traditional Landline phone, which is a communication device also, so we create a new class LandlinePhone using the same CommunicationDevice interface. This is exactly when we face the problem due to a large CommunicationDevice interface we created. In the class LandlinePhone, we implement the make_calls() method, but as we also inherit abstract methods send_sms() and browse_internet() we have to provide an implementation of these two abstract methods also in the LandlinePhone class even if
these are not applicable to this class LandlinePhone. We can either throw an exception or just write pass in the implementation, but we still need to provide an implementation.

Let’s fix the violation by segregating the interfaces. Instead of creating a large interface, we will create smaller role interfaces for each method. The respective classes would only use related interfaces.

Dependency Inversion Principle

The dependency inversion principle is a specific form of loosely coupling software modules. The principle states that:

  1. High-level modules should not depend on low-level modules. Both should depend on abstractions (e.g., interfaces).
  2. Abstractions should not depend on details. Details (concrete implementations) should depend on abstractions.

When our application is largely composed of modules, we need to follow dependency injection. All our modules should be loosely coupled and independent.

traditional layers pattern (source wikipedia)
dependency inversion pattern (source wikipedia)

Interestingly if our code is following open-closed principle and Liskov substitution principle it is aligned to be compliant with dependency inversion principle also. Because

  1. by following open-closed principle we have created interfaces to provide different high-level implementation
  2. Liskov substitution ensure that we can replace the low-level class objects with high-level class objects without causing any adverse effect on the application.

Let’s assume we have a class Player and a class ClubMembership which holds the membership of different student in different clubs. Now we define a high-level class MembershipReportOfBarcelona which will hold all the players of club Fc Barcelona with their net worth.

As you can see, we are directly accessing members of ClubMembership class from our high level class MembershipReportOfBarcelona . And by doing so we are breaching the principle of dependency inversion.

How? Suppose we need to modify the implementation of members from list to dict or tuple or any other data type in future. In that case, our high level class MembershipReportOfBarcelona will break as it is dependent on
implementation of a low level class ClubMembership. This is absurd!

Let’s fix this fundamental violation asap:

The first and foremost thing to stick with dependency inversion principle is ensuring that any of our high-level class is not dependent on implementation of any low-level class.

So, like open-closed principle we have created a new interface FindClubMembership with a abstract method find_players_of_club which will look up all players of a club. Any class which will inherit this FindClubMembership class will have to implement this abstract method by itself.

In our case our ClubMembership class has extended the interface so we have implemented the look up part here and the method will yield the result. The process which was done in our MembershipReportOfBarcelona class is now
responsibility of ClubMembership class. We did it through the interface FindClubMembership. By doing this we have eliminated the dependency of our high-level class MembershipReportOfBarcelona from low-level class ClubMembership and
transferred the dependency to our interface FindClubMembership. As a result any changes of low-level class implementation is not going to impact our high-level class anymore.

--

--