Solid Principles in Software Engineering
We always want reliable software while we use item-oriented programming. Design is the most important phase in all software development methodology. The impact of good design makes the other stages of SDLC stress and hassle-free.
There are no standard guidelines for design quality and it heavily depends on developers expertise and experience.
The SOLID principle is an acronym coined for five object-oriented design principles. These principles can help create a robust system which is easier to maintain and extend over time.
These are the OOPS concepts but can be applied for any other design principles as well. Some of the SOLID principles can be debatable and should be evaluated before applying in project-specific scenarios
What makes a poor software?
These five factors result in poor architecture coming from Poor design and make the software poor.
- Scalability-how good the software to add/ modify requirements.
- Rigidity-how difficult to measure the changing the software.
- Fragility-how frequently the software brakes when it is changed.
- Immobility- The inability to reuse software or its components from one project to another.
- Viscosity-The inability to preserve the design of the system which can degrade if a proper solution is not Incorporated pertaining to any changes in the system requirement.
Requirement mismatch- solid principle When a software design has flowed it produces damage.
- Produces bugs
- Put stress on the system as individual system components will have more responsibilities.
- More dependency and tight coupling.
- More duplicate codes.
In order to make good software design -We can do the following tasks.
- Identifies correct architecture. (MVC,3 tier, MVVP, MVP, Layered etc.)
- Incorporate design principles.
- Follow a good design pattern to build the software based on requirements.
History of SOLID Principle
In February 2001- a set of software professionals designed the Agile Manifesto.(read more on agile here).
SOLID principles are one of the classic design principles that encounter most of the software design problems.
Agile says the followings –
- Individuals and interactions over processes and tools.
- Working software over comprehensive documentation.
- Customer collaboration over contract negotiation.
- Responding to change over following a plan.
Few Other Development models are:
Apart from the traditional development process, in all development model software gets better with each iteration.
The objective of iterations are-
- A process is set up of repeating a set of operations until a specific result is achieved.
- Typically build something quickly for review.
- Review generates great feedback.
- Feedbacks creates revision in Design, coding.
- Interestingly, stakeholders often do not know what they want, so iterations will help them to figure out the actual requirement.
Peter Blake came up with a concept called “Form follows Fiasco” that says function should be the utmost importance in any design. The object-oriented design provides – flexibility, scalability, maintainability and reusability.
To get a good software package, we must have the followings:-
- Lower coupling
- Substantial cohesion
- Potent encapsulation
Using these rules /ideas collectively, we will be able to generate improved high-quality code. Sound ideas assist software developers to reach scalability, prevent code break, increase reusability.
To make the software stable, the concept was launched by Micheal Feathers for five principles that were described by Robert C Martin (known as Uncle Bob)in the early 2000s.
Details of Solid
SOLID principle reduces tight coupling. Tight indicates a set of classes are highly dependent on each other. In programming, such tight coupling is referred to as poor coding. On the other hand, if the design and coding have been done using a loose coupling.
SOLID principles are one of the classic design principles that encounter most of the software design problems.
SOLID is an acronym formed by the name of 5 basic design principles to design better code. The principles were later named by Micheal Feathers who changed their order to form SOLID.
The code is referred to as good coding. Loose coupling introduces reusability, flexibility, maintainability. Overall it provides a stable code.
As per SOLID, it is always better to have many smaller interfaces than very few fatter interface. When we break down interfaces, we offer composition in place of inheritance.
This also triggers decoupling over coupling. We can use composition by separating roles and Responsibilities. Decoupling provides derivative classes instead of a huge Monolith.
Effective Software designs
Requirement mismatch- solid principle When a software design has flowed it produces damage.
- Produces bugs
- Put stress on the system as individual system components will have more responsibilities.
- More dependency and tight coupling.
- More duplicate codes.
In order to make good software design -We can do the following tasks.
- Identifies correct architecture. (MVC,3 tier, MVVP, MVP, Layered etc.)
- Incorporate design principles.
- Follow a good design pattern to build the software based on requirements.
History of SOLID Principle
In February 2001- a set of software professionals designed the Agile Manifesto.(read more on agile here).
SOLID principles are one of the classic design principles that encounter most of the software design problems.
Agile says the followings –
- Individuals and interactions over processes and tools.
- Working software over comprehensive documentation.
- Customer collaboration over contract negotiation.
- Responding to change over following a plan.
Few Other Development models are:
Apart from the traditional development process, in all development model software gets better with each iteration.
The objective of iterations are-
- A process is set up of repeating a set of operations until a specific result is achieved.
- Typically build something quickly for review.
- Review generates great feedback.
- Feedbacks creates revision in Design, coding.
- Interestingly, stakeholders often do not know what they want, so iterations will help them to figure out the actual requirement.
Peter Blake came up with a concept called “Form follows Fiasco” that says function should be the utmost importance in any design. The object-oriented design provides – flexibility, scalability, maintainability and reusability.
To get a good software package, we must have the followings:-
- Lower coupling
- Substantial cohesion
- Potent encapsulation
Using these rules /ideas collectively, we will be able to generate improved high-quality code. Sound ideas assist software developers to reach scalability, prevent code break, increase reusability.
To make the software stable, the concept was launched by Micheal Feathers for five principles that were described by Robert C Martin (known as Uncle Bob)in the early 2000s.
Details of Solid
SOLID principle reduces tight coupling. Tight indicates a set of classes are highly dependent on each other. In programming, such tight coupling is referred to as poor coding. On the other hand, if the design and coding have been done using a loose coupling.
SOLID principles are one of the classic design principles that encounter most of the software design problems.
SOLID is an acronym formed by the name of 5 basic design principles to design better code. The principles were later named by Micheal Feathers who changed their order to form SOLID.
The code is referred to as good coding. Loose coupling introduces reusability, flexibility, maintainability. Overall it provides a stable code.
As per SOLID, it is always better to have many smaller interfaces than very few fatter interface. When we break down interfaces, we offer composition in place of inheritance.
This also triggers decoupling over coupling. We can use composition by separating roles and Responsibilities. Decoupling provides derivative classes instead of a huge Monolith.
Effective Software designs
Adaptive code S-Single Responsibility Principle
This principle states that an object/ class should have one and the reason to change. If there is more than one motive for changing a class, that class is assumed to have more than one responsibility. That means it has high coupling and it may break unexpectedly if there is any change in requirements.
Single responsibility principle was initially introduced by Tom De Marco in1979 in his book -structured Analysis and systems specification.
As described by Robert Martin “A class should have one and only one reason to change”. This principle relates to Cohesion which is a measure of how closely two things core related. The programmer should maximize Cohesion so that each class does only one thing. This, in turn, ensures that the behaviour of a class is not accidentally changed.
What is responsibility?
Responsibility is defined as a reason for the change. When some requirements get changed, we need to refactor some underlying code. The more requirements a class covers, the more responsibilities a class has. That means the same class needs more refactoring.
Single responsibility principle says,” Do one thing and do it well”. Just because we can add everything to our class that does not mean that we will put every responsibility into a single class.
If a class implements multiple responsibilities, it is no longer independent of those. More refactoring brings more complexity, more side effects and it ready a lot of work. Similarly, if an object has more than one responsibility, hence there is more than one reason to change. In that case, it is violating SRP.
If the application changes later in a way that affects any of the responsibilities(DB management or class properties), then the other portion needs to be refactored, recompiled to adopt the new changes. This phenomenon creates domains effect, touch one card if affects all other cards in the line.
The single responsibility principle is all about coupling and cohesion.
Coupling
If a class has more than one responsibility it is then coupled. Coupling is defined as the degree to which a module, class, construct, is tied directly to others. The degree of coupling defines how dependent the classes are.
Our target is to make a class with zero couplings. It indicates that the class does not depend on anything. But remember coupling is not bad if designed correctly.
Cohesion-
Cohesion is a measure in which two or more subsystems work together to obtain better results than each part individually.
When we partially implement the interfaces or base class functionalities and leave the unimplemented methods or properties to throw Not Implemented Exception we violate SRP.
Thin interfaces should be cohesive ( operations that logically belong together). This actually prevents us from ending up with one -the interface-per method most of the time.
Factors affecting SRP:
The below factors mainly affect the SRP:
- Persistence
- Validation
- Notification
- Error handling
- logging
- Class instantiation/ selection
- Formatting
- Parsing
- Mapping
How the single responsibility principle is violated?
When coders need to add features or change some of the existing behaviour they try to implement everything into a single existing class. This makes the code lengthy, complex. Later point of time If further modifications need to be incorporated, the refactoring efforts goes high.
Example of SRP Violation
Public abstract class Indian Bank Account { double gtBalance(){} void do Deposit(double amount){} void do withdraw(double amount){} void add Interest(double amount){} void do transfer(double amount ,BANK Account to Account){} }
Let’s say when we started the project, we had one and only banking type Called savings account. With the new set of requirements, the new ask Is to set up a current account along with savings account.
Now look at from developers perspective he cannot use the above class as current account as the fundamental difference of interest calculation among the two accounts.
Secondly, if he tries to tweak that code, in that case, the class will have now two different and distinct reasons to change.
How to resolve?
We need to introduce layers in the application to break the base class into smaller classes and modules.
As per Steve Fenton “When designing our classes, we should aim to put related features together, so, whenever they tend to change they change for the same reason. And we should try to separate features if they will change for different reasons.”
Refactor the code for SRP friendliness
Public abstract Class IndianBankAccount { //all except add Interest } public class CurrentAccount implements IndianBankAccount{ } public class SavingsAccount implements IndianBankAccount { void add Interest(double amount){} }
The SRP is one of the easy to principle but the hardest to get right. The main motto of SRP is –Divide and win.
One more SRP validation
In the below code, the single responsibility principle (SRP) is being violated because the Insert Record class has multiple responsibilities of creating database connection and inserting the records.
The method with multiple responsibilities- violating SRP:-
public void insertRecord (CreaditCard e) { String StrConnectionString = ""; SqlConnection objCon= new SqlConnection (Str connectionStr) SqlParameter [ ] myParameters = null; SqlCommand objCommand = new SqlCommand("InsertQuery", objCon); objCommand.Parameters.AddRange (myParameters); objCommand.ExecuteNonQuery( ); }
The above code design if not very efficient as does not allow reuse. In the below code, the above code can be refactored to implement SRP to increase reuse. Though generally this principle is widely accepted, one of the arguments against SRP is that depending on the future requirements the code can be refactored for reuse.
Refactor the code for SRP friendliness
Public void InsertRecord (CreditCard c) { SqlConnection objCon= Getconnection( ); SqlParameter[ ] myParameters= GetParameters( ); SqlComand objCommand = GetSqlCommand(objCon,"InsertQuery", myParameters); objCommand.ExecuteNonQuery( ); } Private SqlCommand GetSQLCommand(SqlConnection objCon,String insertQuey, SqlParameter[ ] param) { SqlCommand objcommand=new SqlCommand(InsertQuery,objCon); objComand,Parameters.AddRange (Param); return objCommand; } Private SqlParameter[ ] getParameters( ) { // create parameter array from values } Private SqlConnection GetConnection( ) { String StrConnectionString=""; return new SqlConnection(StrConnectionString); }
Popular SRP violation
SRP Violation classic example Design Principles- SOLID principles In SRP, we must split up the code based on the social structure of the users, who are using it.
Solution (Instead of clubbing all into the same class segregate the responsibilities into different classes.)
SRP friendliness How to apply the SRP principle?
Apply SRP- Solid Principle How to understand if we are violating SRP principles?
- While designing the class, think what is the responsibility of the class/ component; then try to document the answer. If the answer contains “and”, “or”, “but”, “if”, then we need to break the class /component.
- Check the cohesiveness of the class. If it is low, then possibly it has violated SRP.
- Check for the class constructor arguments, if the count is more than two/three and any other methods have several parameters, then the module /class is violating
- Check for the class/module implementation details. If it is too long, possibly it is violating SRP.
- A single responsibility principle is often driven by business needs. So understanding the business along with the technology can help.
Advantages of Single Responsibility Principle
- A stronger cohesion in the class.
- Lesser coupling in dependency classes
- High cohesion.
- Better encapsulation.
- Improved readability
- Reduce the count of bugs.
- Decreased complexity.
- Less error-prone
- More robust
- Better testable
- Better maintainable
- Better extendable.
- Speed up the development process.
- Better Organization-Small and well-organized classes are easier to find and refactor than a huge one.
- Easy Testing- A class with one reason (responsibilities)will surely have the least number of test cases less number of good test cases will be able to concentrate more on the functional ability of the UAT.
- If it is a model class, the class should represent one actor /entity. In the same way, if we write a service or manager class, They should only content method calls.
Disadvantages of Single Responsibility Principle
- Needs change over time and software is repurposed
- A class or any class should be highly coupled internally.
- A change in a specific are should affect all dependent classes.
Wrong interpretation of the SRP:-
We should not interpret that a class should only perform one task. Interestingly, SRP does not mean a class will have only one function/method.
Open and Closed Principle
Open closed principle was introduced by Bertrand Meyer, He worked on Inheritance to resolve the dilemma of the principle. As per Bertrand, once the class is completed, if coding should only be altered to fix the errors or bugs. But if we need to add/change features, a separate class needs to be created.
The open-closed principle states “Software entities (classes, modules, functions etc) should be open for extension, but closed for modification i.e immutable ” that is such an entity can allow its behaviour to be modified without altering its source code.
Open close principle advocates polymorphism in a controlled way. This enables classes to polymorphically substitute for other classes.
When a single change in a program results in a cascade of changes to dependant modules that actually depicts an unreliable software causing from a bad design. This design made the software fragile, rigid, unpredictable and unreliable.
This open-closed principle says these modules should never be changed in general but when requirements change, you add new codes to support the new requirements.
Open closed principle is very similar to open source library, when we deploy an open-source library, we can extend our classes using the library to build better software but we do not change the behaviour of the already present library.
In this principle, any software entity can allow it’s functionality to be modified without altering its source code.
As per Robert C Martin open-closed principle have two main features-
- Open for Extension-It is very much possible to extend the behaviour of the module as the requirements of the application change (Change the behaviour of the module)
- Closed for Modification:- Extending the behaviour of the module does not result in the changing of the source code or binary code of the module.
There are various instances when the consuming code may want to change the behaviour of the class. Open/closed principle suggests extending or changing the behaviour of the class without changing the class. This can be done by applying the OOPs (Object Oriented Programming) concept of abstractions and polymorphism resulting in delivered class to extend the behaviour for the new feature.
In many practical situations with the requirements changing drastically, it does not make sense to have (N) layers of hierarchy in the code because the modification was asked N times. The design decision should be based on how frequently the base class is being referenced. This principle is extremely useful in frameworks, libraries where there are numerous dependencies and any change to the behaviour of the existing classes can cause a big impact on the system.
electricity-plug The electric adapter is closed to any kind of further modification. But it gives provision to attach more electrical equipment. We can also use an extension board that can extend it further.
Polymorphic open/close principle
This principle uses abstract interfaces to implement the open-close principle. Once the base implementations of several types can be created and they are polymorphically substituted for each other.
In this principle, a class is open for the extension when it does not depend directly on concrete implementations. Rather it depends on abstract base classes or interfaces. It depends on how the dependencies are Implemented at runtime.
Open closed principle actually arranges encapsulation in an effective way yet it is open to be extensible.
So, finally, the open-close principle exposes only the moving parts that need to be changed but hides everything else.
As per Evic Elliott,-“Closed for modification means that we should not introduce breaking changes to existing functionality because that would force you to refactor a lot of existing code”.
How to extend a class using OCP?
There are three ways we can extend a class using OCP. They are as follows:-
- Inheritance
- Composition
- Proxy implementation (a type of composition)
Role of Inheritance in OCP?
Inheritance provides the ability to a subclass to get an overridable behaviour (function). Note-The base version is not abstract one rather a fully codified instance. This base method is often referred to as the template method. The derived or overridden one is called delegation. Inheritance gives stronger coupling than delegation.
The open-close principle is important as the code base of the project is often refactored in the whole delivery process.
Features of the open-closed principle:
- Modules should never change.
- The behaviour of modules can be extended by adding code.
- Encapsulation is the key hence avoid any use of global and public variables.
In case, a parent company of a library releases classes, and a developer needs to do some modifications, in that case, the developer is allowed to extend the existing class created and released by a parent company. But the developer is not supposed to update/modify the class directly.
Popular Open close principle violation
Open close violation The below code is refactored to implement open-close principle to calculate the area of rectangle and circle inheriting the base class. This is helpful as for new change in requirements (say calculating the area of the cylinder) in future can be handled without changing the existing code or classes making the design more reprehensive to changes and leaving no room for introducing any bugs in the existing functionality.
//open for modification but closed for extension public double calculationArea(Object[ ] shapes) { double area=0; for each (var shape in shapes) { If (shape is Rectangle) { Rectangle rect= (Rectangle) shape; Area+= rect.windth*rect.height; } else { Circle cir =(circle) shape; area+ = cir.Radius*cir.Radius*Math.PI; } } Return area; }
The above code is refactored to implement the open-closed principle. The solution to adopt the open-close principle
The solution to open close principle public abstract class GetDimensionsForShape { public abstract double calculateArea( ); } //Inheriting from Get Dimensions shape the Rectangle and Circle classes now looks like- public class Rectangle : GetDimensionsForShape{ public double Width {get, set} public double Height {get, set} public override double CalculateArea ( ) { return Width*Height; } } Public class Circle: GetDimensionsForShape { Public double Radius {get, set}; Public override double CalculateArea( ){ Return Radius*Radius*Math.PI; } }
The easiest and better way to implement OCP is to use a template or strategy pattern.
Is sealed class contradicts OCP?
A sealed class restricts users to inherit the class (from inheriting a class)the keyword instructs the compiler that the class is sealed and it cannot be extended. we cannot derive any class from a sealed class.[in java, the concept has been implemented using a final keyword]
So for sealed classes, we cannot use Inheritance. What we can do is to make an interface by injecting all dependencies into it? In that case, we can plug in new features by removing the collaborators/dependencies.
The class extension should happen in any of the following:-
- Inheriting from the class.
- Overwriting the required method of the class.
- Extending certain behaviour of the class.
One more example of Open close principle
Say our student class looks below-
Class Student { Student(string type){ } getstudentType( ){ } }
Let us assume that each student belongs to one specific category. like science, commerce, Arts. So, we want to iterate through a list of students and check their category.
Student stud[ ]={new student ('Arts'),new student('science'),.....} public void studentType( ){ for(a:stud){ if(a.type=='Arts') log('ARTS) if (a.type= ='Science') log('Science') .... .... } }
Such class design does not abide by the open-close principle as if cannot be opened for any extension for new kind of students if the organisation adds new streams. Say tomorrow, if we want to add ‘social science’ as a separate stream. Then we have to touch several places of the code -like-
Student stud[ ]={new student('Arts','science','commerce','social science'}; public void studenType( ){ for(a:stud)} if........ // . . . if (a.type=='social science') log('social science') }
Now for every type of student, a new logic needs to be added into the student Type( ) function. Since it is a very simple example,it looks simple. But once the application grows big and complex, these refactoring also becomes tough.
How to resolve this case?
Class Student { gettype( ) //.... } Class Science extends student{ gettype ( ){ return 'science'; } } Class Commerce extends student{ gettype ( ){ return'commerce'; } } ...... ....... Class SocialScience extends student{ gettype ( ){ return'social science'; //...} }
Disadvantages if we do not follow the open-close principle:
- Class or method modification (refactoring becomes a challenge as we need to understand thousand lines of codes.
- Modifications /refactoring may end up adding multiple sub-tasks.
- Refactoring means mostly adding new logic. To test these logics, we need to write more test cases. More test cases may lead to unnecessary failures, bugs, defects.
Liskov Substitution Principle:
“Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it”- Robert C Martin.
New derived classes extend the base classes without changing their behaviours. Functions that at use pointers or references to base classes must be able to use objects of the derived without knowing it.
This principle is an extension of the open-closed principle. If a system is using a base class, the reference to that base class can be replaced with the derived class without changing the functionality of the system.
If X is a subtype of Y, then objects of type Y may be replaced with objects of type X ( objects of type X may substitute objects of type Y) without altering any of the desirable properties of that program(like – correctness, the task performed etc)
Liskov substitution principle is also referred to as strong behavioural subtyping. This principle talks about the use of Inheritance (“Is-a” relationship).
We have an instance of the class car that our program uses to perform a break or honk action. This instance should be replaced by an instance of the class Ford if Ford is a subclass of the car.
Liskov Substitution Principle Consider an electric bulb. As long as the bulb type is the same (like-holder/push type/screw type).The same type bulbs can be replaced.
This replacement happens, keeping the type the same. Other than functionality nothing really changes.
They work perfectly as no modification in functionality happens.
Instead of a working set of classes that are tightly coupled to each other, We want to work with a standard interface.
The simplified form of LSP is as follows
If the supertype (class)has a method that accepts a superclass type parameters, Is subclass should accept as an argument a superclass type or subclass type.
Liskov Substitution Principle (LSP) has certain Violations, most typically being in circle ellipse problem. The below code violates the Liskov Substitution principle. Since only ‘Project Task’ raises an exception when the task status is closed, the program needs to be changed if ‘Project Task’ is based as a substitution for ‘Task Status’.
public Class TaskStatus{ public status Task_Status (get,set;) } public virtual void CloseTask( ) { Task_Status = TaskStatus.Closed; } public project Task: Task Status{ public override void CloseTask ( ) } if (TaskStatus == TaskStatus.Started) Throw newException ("Can not close a started Project Task"); base.CloseTask( ); } }
The above Code is modified below to support the Liskov substitution principle. By stipulating that a call of TaskClose() is valid only in the state when canCloseTask ( ) returns true. We can fix the Liskov Substitution Principle violation by applying the precondition to ProjectTask and TaskStatus.
public Class TaskStatus { Public Status Task_Status (get;set) } public virtual boolean CanCloseTask( ) { return true; } public virtual void closeTask( ) { Task_Status=Task_Status.closed; } }
public ProjectTask : TaskStatus{ public override boolean canCloseTask ( ) { return Task_Status! = Task_Status.started; } Public override void closeTask ( ) { If (Task_Status == Task_Status.started) throw new Exception ("Can not close a started Project Task"); base.closeTask ( ); } }
Another LSP violation example
Inheritance like a square is a rectangle that breaks LSP. The example that does not break LSP. Let us consider a print job, a copy job etc, they are all jobs, they are not different but just properties on the job.
LSP Violation Square is a rectangle (“IS-A” relationship holds good). How they violate the rule?
LSP Violation in real-time The square does not require height and width fields as they are automatically inherited. The setHeight ( ) and setWidth( ) are not proper for square.
How to resolve LSP?
We can override setHeight ( ) and setwidth ( ) method. Liskov substitution principle makes sure that any class that is a sub or child class of a parent class should be usable in a place of its parent class without any issues. We will implement here the same.
public Class Square extends Rectangle{ public void setHeight(int h){ this.height=h; this.width=h; } public void setwidth(int w){ this height=w; this width=w; } }
The easiest form of Liskov’s substitution
- Preconditions cannot be put in a subtype to enforce.
- Postconditions cannot dilute in the subtype.
- Non-changing attributes of a subtype should be preserved in a subtype.
Note: Liskov substitution mainly applied where classes have an “IS-A” relationship among each other. This is necessary but apart from this, they need to exhibit”Is- substitutable for relationship.
How to make sure our code follows the Liskov substitution Principle?
If our programming language has support for type hinting for the return types in the interface methods, we can use this feature to avoid issues of different return types.
LSP principle confirms that the abstraction is correct and they help us to get a code that is easily reusable and class hierarchies that are very easily understood that is very easily understood. As per Robert C Martin -” a violation of LSP is a latent violation of OCP”.
How to stop violating LSP?
We generally use inheritance while using LSP. We need to double-check the inheritance tree. The child class object should not really break the functionality while replacing the parent class object. So child class must inherit from a proper parent class.
An application that actually follows LSP, a client consuming a polymorphic interface should not care about the concrete implementation it is getting.
Disadvantages if we do not follow LSP?
- The object may provide wrong results.
- The Child class may throw an exception about the unsupported method(UnsupportedMethodException)
- A lot many runtime errors and exception, bugs, defects arises.
- LSP violations are difficult to debug.
Interface Segregation Principle:
This principle states that clients should not depend on the interface that they don’t use. Each interface should have a single responsibility and should not be overloaded. Interfaces should be kept small and cohesive and exposed only when required.
Clients should not be forced to depend upon interfaces that they don’t use. A change in the unrelated to interface can result in an inadvertent change in the client code. This shows in an inadvertent coupling between all the clients. Interface segregation platform (ISP) suggests that clients should not know about them as a single class. Instead, clients should know about abstract base classes that have cohesive.
ISP talks about the disadvantages of implementing a huge interface. The Interface segregation principle says do not add additional functionality to an already existing interface by adding new methods. Rather creates a set of interfaces and we need to implement the new interfaces.
By using interface segregation we make sure that implementing classes only need to be concerned about the methods that are of interest to these classes.
Split interfaces which are very large into smaller specific ones -clients will only have to know about the methods that are of interest to them. Class as with “fat” interfaces are not cohesive. This principle talks about the disadvantages of ‘fat’ interfaces. There are objects with non-cohesive functionalities, but they should be known to clients by their cohesive interfaces.
Interface Segregation Principle says we need to make many fine-grained clients specific interfaces. Classes should be more specific instead of generic. This intern says that code should be modular. Each modular class should contain only the minimum necessary logic to complete the desired behaviour. Similar logic goes for interfaces as well.
In well-designed software code, We must avoid interface pollution ( fat interfaces ). A class should never implement an interface that the class does not go to use. ISP says “Do not depend on things you don’t need”
Real life examples of Interface segregation Principle example:-
- FocusListener
- KeyListener
- MouseMotionListener
- MouseWheelListener
- TextListener
- WindowFocusListener
Example of ISP violation
ISP Violation Both square and cube are shapes as is A relationship hold but square does not need to have volume ( ) method. But as the “is A” relationship holds, the square gets it by default.
The solution to make ISP friendly
We can break down a “fat” interface into smaller interfaces with cohesive sets of responsibilities.
ISP friendly This principle says we must prefer to many client interfaces rather than one general interface. Each specific interface will have its own responsibility. This principle helps in reducing the side-effects and frequency of required changes.
Another real example
interface GetBankCustomerData{ public customer GetBankCustomer(Guid AccountID) public void SetBankCustomer (Customer customer); public customer GetBankCustomerForReport(Guid AccountID); }
The above interface is polluted by having three method declarations when a report data access class implements this. The report class needs only the GetCustomerForReport( ) method but for the other two methods, it will have to implement something, at least it will throw Exception for the first two methods. So, in this case, we have to segregate this interface into two, which is in line with the interface segregation principle.
interface GetBankCustomerData : BankReportDataAccess { public customer GetBankCustomerData(Guid AccountID); public void SetBankCustomer(Customer customer); } interface BankReportDataAccess { public Customer getCustomerForReport (Guid AccountID); }
Another example of ISP violation
Let us create an interface-
This interface drives the Car, Truck Bus. Individual classes like the Car, Truck, Bus implements the IVehicle interface must define the methods available in the interface.
interface IVehicle{ driveCar( ); driveTruck( ); driveBus( ); }
The Car class looks like
Class Car implements IVehicle{ driveCar( ){//...} driveTruck( ){//...} driveBus( ){//...} }
The Car class looks like
Class Truck implements IVehicle{ driveCar( ){//...} driveTruck( ){//...} driveBus( ){//...} }
The Bus class looks like
Class Bus implements IVehicle{ driveCar( ){//...} driveTruck( ){//...} driveBus( ){//...} }
Now if we look at the implementations of individual classes, the other methods ( not related to the particular class) is actually of no use.
Now if requirement changes and we need to add one more class called Train then simply adding a method driveTrain ( ) will not work.interface IVehicle{ driveCar( ); driveTruck( ); driveBus( ); driveTrain(); }
Individual classes need to implement the new method. This is a huge and error-prone task. Most importantly, this change is of no use to the existing classes.
More clients ( Bus, Train, Truck, Car) should not be forced to depend on methods that they do not need or use. Isp also advocates that interfaces should perform only one job(Like SRP) any extra grouping of tasks should be abstracted away to another interface. Now if we segregate the behaviours into different interfaces-ISP recommends that we must keep interfaces as small as possible.
interface IVehicle{ drive(); } interface ICar extends IVehicle{ driveCar(); } interface IBus extends IVehicle{ driveBus(); } interface ITrack extends IVehicle{ driveTruck(); } interface ICar extends IVehicle{ driveCar(); } interface ITrain extends IVehicle{ driveTrain(); } Class Bus Implements IBus(){ driveBus(){....} } Class Truck Implements ITruck(){ driveTruck(){....} } Class Car Implements ICar(){ driveCar(){....} } Class Train Implements ITrain(){ driveTrain(){....} }
Disadvantages if we do not follow the Interface Segregation Principle:-
- Developers may deliver big fat interfaces as a result clients will implement unnecessary methods.
- Client interface may perform some unrelated actions.
Dependency Inversion Principle
High-level modules should not depend upon low-level modules. Both should depend upon abstraction. Abstraction should not depend upon details. Details should depend upon abstraction- Robert Martin.
So during design, we should decouple high-level modules from low-level modules, introducing an abstraction layer between the high-level classes and low-level classes. If a class has dependencies on other classes, it should rely on the dependencies interfaces rather than their concrete types.
The idea is to isolate one class behind a boundary formed by the abstraction it depends upon. If all the details behind those abstraction changes, then our class is still safe. This helps keep coupling low and makes our design easier to change. Dependency generates risks. Risks always trigger cost.
Reusing high-level modules are hard as they depend on details. Alternatively, low-level modules can be used as functions or libraries.
DIP is often resolved by injecting dependency)dependencies of a class through the class’s constructor as an input parameter.
Dependency Inversion principle:- High-level modules or classes in general talks about rules or logic in an application and low-level modules /classes tells about the details of the low-level implementation.
Dependency Inversion Principle In real life, if you look at our desktop CPU. It provides several parts (USB, Serial, VGA etc) to attach external devices. Now, We cannot attach any device to a random port. The CPU will not allow its operation. But if we put in a correct port normal operation can happen.
Pictorial simplification of Dependency Inversion Principle
Dependency Inversion principle simplification By using dependency inversion, we can decouple the software modules. After decoupling, the classes actually communicate through the abstraction.
An example of a DIP violation
DIP Violation Let us have a PasswordRecovery class that has multiple functions to recover someone’s password.
The recoveryOption() method looks like:
public String recoveryOption( ){ string option=""; ..... ..... return option; }
With this design, once we change the database engine later, we have to change the class’s code. As per dependency inversion principle the password Reminder class can connect to the database without knowing the engine.
How to make it DIP friendly?
DIP Friendly Difference between Inversion and Dependency Injection?
Dependency Inversion principle advocates depending on abstractions rather on the concrete implementation. Dependency inversion can be achieved via abstraction. Abstraction provides the ability to extend the behaviour of a class without changing any code.
Say we have several classes like square, rectangle, circle, Hexagon etc. To implement the open-close principle, we can introduce an interface called shape. Now if we need to put dependency injection, we can inject a shape instance instead of a lower level class.
Dependency Injection (DI) and inversion of control (IOC) are actually almost the same term. This is a part of the pattern named as IOC. Martin Fowler renamed it as DI. DI allows us to create loosely coupled code.It also helps us reducing tight coupling between various software components.
The following example considers a code to track the user actions on a website-
public Class LogUserActions { Public void Addlog ( ) { // adds user actions to a log file } } public Class logManager{ LogUserActions log = new LogUserActions( ) ; public LogManager (LogUserActions log){ this.log = log; } public void Log( ){ this.log.Addlog ( ) ; } }
In case there is a change to log the user actions in the database as well then new low-level class needs to be written to insert the data to the database as below. The issue with this approach is new for change to LogManager class which will require the entire functionality to be tested again.
public class LogUserActionaID { Public void Addlog ( ) // insert the user activity details in Database } }
Using the Dependency Universal Principle (DIP) we can put across the abstract class interface, between high level and low-level classes. This example is rewritten below using DIP by creating an abstraction. MyManage Log and Manager as a high-level class.
public interface MyManagelog { Void Addlog() ; } Public Class LogManager { MyManage log; public LogManager(MyManageLog log)} this.log= log; } public void Insert() { this.log.Addlog(); } } public Class LogUserActionsInDB: MyManageLog { Public void Addlog ( ) { //Insert the user activity details in Database } } public Class LogUserActions: MyManageLog{ public void AddLog( ){ //Adds user actions to a file } }
With this solution, we can implement new low-level classes without changing the code for the high-level class. The high-level class does not depend on the low-level classes. Thus achieving greater flexibility.
The implementation of this principle should not be applied to all classes. If class functionality is not likely to change then this principle need not be applied.Coupling is not always bad. Without coupling the software actually won’t do anything. We need to understand the software required to understand where the DIP can be applicable.
The issues if we do not follow the Dependency Inversion Principle:-
- Developers may bury into refactorings as there is a chain of modification for a change in the module.
- No concurrent development is possible.
- Testing of the module becomes tough.
Can SOLID be applied to Microservices?
What is a microservice?
Effective features of Microservices:-
- A microservice is independently deployable.
- A microservice is a unique piece of code, possessing modular architecture. This runs on a separate process in a separate machine.
- Microservice can communicate with the outside world.
- Microservices should be short( but necessary to cover its functionalities).
Microservice should follow one by one approach that effectively means each service on its own.
The possible drawback may be sharing of utility functions are not allowed. That intern means possible duplicates of the same resource across our system.
But the major plus point is each service is isolated with the codebase. The Service will have its own interfaces, class hierarchy and internal dependencies.
When we apply SOLID principles into microservices code sharing and code reusability becomes easy. It effectively means we can maintain a single repository with a single build system to compile and test the latest code.
So effectively we can implement SOLID principles to micro Services.
However, versioning a single service is a huge task.
Alternatives to SOLID principles
The below-written principles can be used as an alternative to SOLID
- Agile
- YAGNI-(You Are not Gonna Need It.)-YAGNI says that we should not introduce any code to resolve a future problem. Just Implement what actually is needed at this time. This will keep the software up to date, lean, clean. SRPThis will also save extra money and effort.
- KISS- (keep it simple, stupid)-These principle talks about how to make software simple by avoiding unnecessary complexity.The more the simple the software design will be more robust, easier to understand, easier to maintain, easier to extend.
- DRY principle-( Don’t Repeat Yourself)-This principle says that we should not write the same code/configuration/any programmatic block in multiple places. This reduces the sync issues due to changes in requirements.
- GRASP(General Responsibility Assignment software Principle)
- The principle of opportunity cost– The value has two different aspects -benefits and costs. This principle provides choices to make in order to provide greater benefit with the lowest cost
- The principle of least effort:- This principle is also known as Zipf’s law. The shortest, sweetest and effortless path that needs to be needed to be taken. The least effort is a variant of least work. To achieve this the aim is to start strong.
- The principle of least Astonishment -This principle talks about the solution or approach should not surprise a Subject Matter Expert (SME).
- LOD(Low Of Demeter principle)
In my views, they are not competing directly with SOLID. They can be used in conjunction with SOLID to produce better software.
Advantages to the SOLID principle
As per Mark Seemann-“If you take the SOLID principles to their extremes, you arrive at something that makes functional programming look quite attractive.” However, here are some benefits of the SOLID principles.
- Easy to maintain
- Easy to extend
- Easy to explain
- Easy to understand
- Easy to implement
- Testable
- Things are where they are expected to be.
- Classes are in narrow.
- Codes can be easily adjusted.
Disadvantages of the SOLID principle
Like any other principles of software engineering, SOLID is also not flawless. The most criticisms are:
- SOLID principles are vague.
- SOLID leads to very Complex code.
- SOLID is very much idealistic.
- SOLID is very much market gimmick.
- SOLID focuses too much on dependencies.
Conclusion
Design principles are great for guiding us making several important decisions based on proven and mature advice. Markets are very competitive nowadays. We need to provide the product to the shelf in a tight budget and a shorter timeline.
As per Donald Knuth -” premature optimization is the root of all evil, there are only cost and no benefit”.In SOLIDly designed system, the system itself is easily dissectible, the parts are easily replaceable.
Software design principles represent a whole lot of guidelines that help us to construct a solid foundation.
The antipatterns and wrong understanding of design principle can introduce STUPID code.
S Singleton T Tight Coupling U Untestability P Premature optimization. I Indescriptive naming D Duplication of Code. SOLID actually helps to avoid these issues from software modules. SOLID is a guiding principle but not like a mathematical rule. They help but may fail in some cases. There are many principles out there. We have to choose the correct one.