SOLID ADVISE FOR SOFTWARE DEVELOPMENT

SOLID ADVISE FOR SOFTWARE DEVELOPMENT

Written by

By Ranjgith

Published on

Dec 26, 2021

8 min read

SOLID Principles were introduced by Robert C. Martin - The Uncle Bob in the year 2000 on his paper  Design Principles and Design pattern. Uncle Bob is one of the weird naming schemes in computer science in line with the Java (The Coffee☕), Python (the snake🐍) and Gang of Four 🧑🏻‍🤝‍🧑🏼🧑🏼‍🤝‍🧑. However the name was coined by Michael Feathers later.

Uncle Bob gives us some solid piece of advice for software development through these S.O.L.I.D Principles which makes software designs more understandable, easy to reuse ♻, scale and very much easier to maintain for longer period. As a software engineer these principles are essential knowledge.

A quick overview of the principles –

  • S – Single responsibility principle
  • O – Open/Closed principle
  • L – Liskov substitution  principle
  • I – Interface segregation principle
  • D – Dependency inversion principle

Lets breakdown this solid piece of advise one by one and try to understand why these are very much important in a software development perspective with an example –

The code example contains a sales system, which will handle the payment options just to understand these 5 principles practically –

S – Single responsibility!!👨

As the name suggest, we need our classes and methods to hold a single responsibility, this enables them to have high cohesion and ensures that we can reuse them later on —

    ....
    def add_item(self, name, quantity, price):
    	....
    def total_price(self):
    	....
    def pay(self, payment_type, security_code):
    	if payment_type == 'debit':
        	...
        elif payment_type == 'credit':
        	...
        else:
        	...

Naïve solution

With this context and the above example, we see that the Order class has too many responsibilities – add_item and total_price can be part of the Order, but definitely payment method shouldn't be part of Order class as it breaks the single responsibility principle – one way of solving this problem is extracting the pay method and creating a separate class out of it – by doing so we can ensure that we can add/create different payment options like Bitcoin or whatever with minimal code changes. So after the changes, the class looks something like this –

	...
	def pay_credit(self, Order, security_code):
        	...
	def pay_debit(self, Order, security_code):
        	...

class Order:
    ....
    def add_item(self, name, quantity, price):
    	....
    def total_price(self):
    	....

Solving Single responsibility principle

Here, we separated the single pay method to pay_credit and pay_debit methods and passed order object as one of the arguments so that the payment can be processed. After these changes, we made sure that both classes have their own single responsibility.

If you could see this solution, we have increased cohesion with single responsibility but we have introduced coupling, which we can deal with when we dive into other principles ...

O – Open😁/Closed😊 principle

This means we need to write code which is open for extension which ensures scalability with new functions but closed for any modification – we should not change the existing code in order to achieve something

Lets get back to the changes which we made and see where the problem arrives –

	...
	def pay_credit(self, Order, security_code):
        	...
	def pay_debit(self, Order, security_code):
        	...

Break in Open/Close Principle

Here, if we need to add more payment options like UPI payments / Bitcoins we need to edit the payment processor class which violates this principle –  what we can do is define a class structure and define child-classes for each new payment options. Let's refactor this —

	...
	def pay(self, Order, security_code):
        	...
class DebitPaymentProcessor(PaymentProcessor):
	...
	def pay(self, Order, security_code):
        	...
class CreditPaymentProcessor(PaymentProcessor):
	...
	def pay(self, Order, security_code):
        	...

Inheritance solving the Open/Close principle!

So, after this change, we do not violate the open-close principle anymore as we can create new payment option without any changes to current code! wonderful -  isn't it?

L – Liskov substitution principle🪃

This principle states that – if you have objects in your code, you should be able to replace that object with instances of their  sub-classes (child classes) without altering the correctness of the code.

Lets, assume I have to add UPI payment option to my payment options which requires upi_id instead of security_code –

	...
	def pay(self, Order, security_code):
        	...
class DebitPaymentProcessor(PaymentProcessor):
	...
	def pay(self, Order, security_code):
        	...
class CreditPaymentProcessor(PaymentProcessor):
	...
	def pay(self, Order, security_code):
        	...
class UPIPaymentProcessor(PaymentProcessor):
	...
	def pay(self, Order, upi_id): # New class!!
        	...

Adding UPI Payment Processor with new argument type breaking the Liskov substitution principle

One way to solve this is removing the security code from the pay method and add that to the initializer method so that all other classes have security code and the upi payment processor gets an upi_id. So, the solution looks something like this –

	...
	def pay(self, Order):
        	...
class DebitPaymentProcessor(PaymentProcessor):
	...
    def __init__(self, security_code):
    	self.security_code = security_code
    def pay(self, Order):
        	...
class CreditPaymentProcessor(PaymentProcessor):
	...
    def __init__(self, security_code):
    	self.security_code = security_code
    def pay(self, Order):
        	...
class UPIPaymentProcessor(PaymentProcessor):
	...
    def __init__(self, upi_id):
    	self.upi_id = upi_id
    def pay(self, Order): 
        	...

Solved Liskov Substitution principle!

This way, we send in the security code when we initialize the class for Credit and Debit Payment Processor and send in the upi_id to the UPI Payment Processor

I – Interface Segregation 🤌

Interface segregation means that overall it is better if you have several specific interfaces instead of one general interface.

In our current solution we can add a new functionality called the two factor authentication –

	...
    def auth_sms (self, code):
    	...
    def pay(self, Order):
        ...
class DebitPaymentProcessor(PaymentProcessor):
	...
    def __init__(self, security_code):
    	self.security_code = security_code
    def auth_sms (self, code):
    	...
    def pay(self, Order):
        	...
class CreditPaymentProcessor(PaymentProcessor):
	...
    def __init__(self, security_code):
    	self.security_code = security_code
    def auth_sms (self, code):
    	...
    def pay(self, Order):
        	...
class UPIPaymentProcessor(PaymentProcessor):
	...
    def __init__(self, upi_id):
    	self.upi_id = upi_id
    def pay(self, Order): 
        	...

Naïve solution for Adding more features

Here UPI Payments does not support the two factor authentication – whenever we see one or more subclasses which does not support the parent methods, it is recommended to go with interfaces – what we can do is, create a child class for the Payment Processor which extends the functionality with two factor authentication and derive the respective classes from them as like below –

	...
    def pay(self, Order):
        ...
class PaymentProcessor_SMS(PaymentProcessor):
	...
    def auth_sms (self, code):
    	...
class DebitPaymentProcessor(PaymentProcessor_SMS):
	...
    def __init__(self, security_code):
    	self.security_code = security_code
    def pay(self, Order):
        	...
class CreditPaymentProcessor(PaymentProcessor_SMS):
	...
    def __init__(self, security_code):
    	self.security_code = security_code
    def pay(self, Order):
        	...
class UPIPaymentProcessor(PaymentProcessor):
	...
    def __init__(self, upi_id):
    	self.upi_id = upi_id
    def pay(self, Order): 
        	...

The right way!

Instead of using classes and child classes through inheritance we can also use compositions which will make more sense here in our example – instead of a child class called PaymentProcessor_SMS, we could create a SMS Authorizer class and move the responsibility of the two factor authorization to that class and send the objects to the required classes –

	...
    def auth_sms (self, code):
    	...
    def auth_status(self):
    	...
    	return status
_______________________________________________________________
class PaymentProcessor():
	...
    def pay(self, Order):
        ...
_______________________________________________________________
class DebitPaymentProcessor(PaymentProcessor_SMS):
	...
    def __init__(self, security_code, SMS_Auth):
    	self.security_code = security_code
    def pay(self, Order):
        	...
class CreditPaymentProcessor(PaymentProcessor_SMS):
	...
    def __init__(self, security_code, SMS_Auth):
    	self.security_code = security_code
    def pay(self, Order):
        	...
class UPIPaymentProcessor(PaymentProcessor):
	...
    def __init__(self, upi_id):
    	self.upi_id = upi_id
    def pay(self, Order, SMSAuth): 
        ...

Composition in work!!

Note that after every change we make the way we call these methods and how we initialize the class changes every slightly, also we can note that we can reduce the big inheritance tree by using composition to separate different kinds of behavior in our code!

D – Dependency inversion 🛫

Dependency inversion means that we want our classes to depend on abstractions and not on concrete subclasses – this helps in decoupling.

In our current implementation this is a big problem where we need the help of SMS_Auth() for running our payment processors, so to solve this, we can create an abstract authorizer class which we can pass to the payment processors, Also if we need any other authorizer type – such as a not a bot check, we should be able to do that without breaking the Open-close principle.. So, let's rewrite the code into something like below –

class Authorizer():
	def auth_status(self):
    	...
    	return status
class SMSAuth(Authorizer):
	...
    def auth_sms (self, code):
    	...
_______________________________________________________________
class PaymentProcessor():
	...
    def pay(self, Order):
        ...
_______________________________________________________________
class DebitPaymentProcessor(PaymentProcessor_SMS):
	...
    def __init__(self, security_code, Authorizer):
    	self.security_code = security_code
    def pay(self, Order):
        	...

Final change (❁´◡`❁)

Now, we have a common Authorizer object as part of Debit Payment Processor which ensures dependency inversion and ensures scalability for different authorizers

Takeaways ‍🎈

Quick summary of the SOLID advise from uncle Bob –

  1. S –  Single responsibility principle – Have single responsibility to methods, classes and functions
  2. O – Open/Closed principle – Our code should be open to scale and closed to modifications
  3. L – Liskov substitution  principle – Implement the solution such that the objects of child classes are replaceable with the parent objects
  4. I – Interface segregation principle – Try not to have access to methods which you don't need from parent, use interfaces
  5. D – Dependency inversion principle – High level modules should not depend on low level modules, they should depend on abstractions

Conclusion

I hope these examples have given a gist of what SOLID design principles mean and how they can be easily added to our development to write a better code!  As far as it is important to know about these design principle as a software developer, as time goes on we tend to get more comfortable in applying these to our workflow ❤️

4 Likes
instagram-comment0 Comments