In object-oriented programming, S.O.L.I.D. is a mnemonic acronym for five design principles intended to make software designs more understandable, flexible and maintainable.
S.O.L.I.D. Concepts
Let us initially briefly describe each one of the principles, before our attempt to elaborate on each one of them with Python examples.
- Single Responsibility Principle: Any class should only have a single responsibility, meaning that only changes that are happening in one part of the software should affect the class specification
- Open-Closed Principle: Any software entity (Classes, methods,etc…) should be open for extension, but closed for modification
- Liskov Substitution Principle: Objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program (Design by Contract)
- Interface Segregation Principle: Many client-specific interfaces are better than one general-purpose interface
- Dependency Inversion Principle: We should depend on abstractions and not concretions
Single Responsibility Principle
The Principle requires that a class should have only one job (responsibility). If a class has more than one responsibilities then it will become coupled and that leads us to make changes to other responsibilities in the case that any of those need to be modified.
Assume the below class which is used both to perform some database operations and to access class attributes.
class Post: def __init__(self, body): self.body = body def get_body(self): pass def save(self, post): pass
In this scenario and in the case that we need to make any change that affects database methods -such as the save method at line 8- we should modify and recompile the class that handles Post properties to comply with the changes.
This can be solved by splitting the class into two, one that handles Post’s properties and one that handles Post’s database methods.
class Post: def __init__(self, body): self.body = body def get_body(self): pass
class PostDB: def save(self, post): pass
Ideally to solve the specific problem we should implement the Facade Pattern for which we will talk about in an upcoming post.
Open-Closed Principle
Classes, modules, functions should be open for extension, and not for modification.
Assume that we have an Employee class that has as attributes the employee type (stuff, manager or VP) and the salary and implements a give bonus method that simply returns the 10% of the employee’s salary.
class Employee: def __init__(self, employee_type, salary): self.employee_type = employee_type self.salary = salary def give_bonus(self): return self.salary * 0.1
In the case that we wanted to change the bonus for the manages and make it double than the rest employees, then we could do something like this:
class Employee: def __init__(self, employee_type, salary): self.employee_type = employee_type self.salary = salary def give_bonus(self): if self.employee_type == "manager": return self.salary * 0.2 return self.salary * 0.1
This although does not satisfy the Open-Closed principle. For any different bonus, we want to give to any of the employee types instead of adding the logic to the existing class, we should create a new class that will extend our base class -in this scenario the Employee class. Each one of the new classes will have their own implementation of the requirement and this will cover the Open-Closed principle.
class Employee: def __init__(self, employee_type, salary): self.employee_type = employee_type self.salary = salary def give_bonus(self): return self.salary * 0.1
class Manager(Employee): def give_bonus(self): return super().give_bonus() * 2
And in the case we want to have double the manager bonus for the VPs:
class VP(Manager): def give_bonus(self): return super().give_bonus() * 2
Liskov Substitution Principle
The Liskov Substitution principle was introduced by Barbara Liskov in her conference keynote “Data abstraction” in 1987. Barbara Liskov and Jeannette Wing formulated the principle succinctly in a 1994 paper as follows:
Let φ(x) be a property provable about objects x of type T. Then φ(y) should be true for objects y of type S where S is a subtype of T.
Robert Martin made the definition sound more smoothly and humane in 1996:
Functions that use pointers of references to base classes must be able to use objects of derived classes without knowing it.
Assume the below Animal class which simply has as attributes name and age, and has the fly and walk methods.
class Animal: def __init__(self, name, age): self.name = name self.age = age def eats(self): return "Eating" def fly(self): return "Flying" def walk(self): return "Walking"
We can easily extend this class to make an Eagle and a Dog class.
class Eagle(Animal): def lays_eggs(self): return "Lays eggs in nest"
class Dog(Animal): def barking(self): return "Woof"
The problem here is that with this implementation we can make a dog fly. In Animal class we violate the Liskov Substitution Principle, due to the fly method in this particular example. Since a dog cannot fly we should not be able to call the fly method for dogs.
In order to solve this and also comply with the Liskov Substitution Principle we could do something like:
class Animal: def __init__(self, name, age): self.name = name self.age = age def eats(self): return "Eating"
class FlyingAnimal(Animal): def fly(self): return "Flying" def walk(self): return "Walking"
class WalkingAnimal(Animal): def walk(self): return "Walking"
class Eagle(FlyingAnimal): def lays_eggs(self): return "Lays eggs in nest"
class Dog(WalkingAnimal): def barking(self): return "Woof"
Liskov Substitution Principle is fundamental to good object-oriented software design because it emphasizes one of its core aspects, the polymorphism.
We need to carefully think of how we will implement new classes so we can comply with the Liskov Substitution Principle. This will also assist us in complying with the Open-Closed Principle.
Interface Segregation Principle
According to Robert Martin’s Agile Software Development: Principles, Patterns, and Practices, the principle is defined as,
Clients should not be forced to depend on methods that they do not use.”
In other words, classes should not have access to behaviors that they do not use.
In object-oriented terms, an interface is represented by the set of methods an object exposes. This is to say that all the messages that an object is able to receive or interpret constitute its interface, and this is what other clients can request. The interface separates the definition of the exposed behavior for a class from its implementation.
Think that we want to be able to parse an event from several data sources, in different formats (XML, JSON and other).
class EventParser(): def parse_JSON(): raise NotImplemented def parse_XML(): raise NotImplemented def parse_other(): raise NotImplemented
But what happens if a particular class only needs the XML method? It would still carry the parse_JSON() and the method parse_other() from the interface, and since it does not need it, it will have to pass. This creates coupling and forces clients of the interface to work with methods that they do not need.
class XMLApiParser(EventParser): def parse_XML(): return "Parsing XML" def parse_JSON(): pass def parse_other(): pass
It is better to have as small interfaces as possible and for the specific example, we should split Event Parser into three different interfaces.
class JSONEventParser(): def parse_JSON(): raise NotImplemented
class XMLEventParser(): def parse_XML(): raise NotImplemented
class OtherEventParser(): def parse_other(): raise NotImplemented
With this our class that wants to use the XML parser will only implement the parse_XML method and nothing more.
class XMLApiParser(XMLEventParser): def parse_XML(): return "Parsing XML"
Dependency Inversion Principle
Dependency inversion principle is one of the principles on which most of the design patterns are build upon. Dependency inversion talks about the coupling between the different classes or modules.
As per Wikipedia:
A. High-level modules should not depend on low-level modules. Both should depend on abstractions.
B. Abstractions should not depend on details. Details should depend on abstractions.
Assume that we want to implement a supervisor class for a software house and add to each instance the employees that are supervised by him/her.
class Supervisor(): def __init__(self): self.developers=[] self.designers=[] self.testers=[] def add_developer(self,dev): self.developers.append(dev) def add_designers(self,design): self.designers.append(design) def add_tester(self,tester): self.testers.append(tester)
And the classes for each type of employee.
class Developer(): def __init__(self): print "developer added"
class Designer(): def __init__(self): print "designer added"
class Tester(): def __init__(self): print "tester added"
Everything about the lower layer is exposed to the upper layer, so abstraction is not mentioned. That means Supervisor must already know about the type of workers that he/she can supervise.
Now if another type of employee comes under the supervisor (e.g Product Owner) then the whole class needs a redesign.
By applying the Dependency Inversion Principle we can solve this problem and the code will conclude at something like:
class Employee(): def work(): pass
class Developer(Employee): def __init__(self): print "developer added" def work(): print "coding"
class Designer(Employee): def __init__(self): print "designer added" def work(): print "designing"
class Tester(Employee): def __init__(self): print "tester added" def work(): print "testing"
class Supervisor(): def __init__(self): self.employees=[] def add_employee(self,a): self.employees.append(a)
Now if any other kind of the employee is added it can be simply be added to Supervisor without the need to make the Supervisor explicitly aware of it.
Creating this abstraction between Supervisor and Employee allows us to maintain and extend our code easily.
The code now is truly decoupled and as mentioned there are many design patterns where this is the basic idea and we extend on it.
If you want to study more on S.O.L.I.D. Principles I would suggest reading Clean Code by Robert C. Martin.