Just because you are using object oriented programming, it does not mean that you are doing object oriented programming. In order to do that, we need to follow certain principles while designing our application.
S.O.L.I.D principles are those guidelines, which help us to create a clean, maintainable, independently testable solution. Please note that these are not hard and fast rules which you need to follow in any case. But these are the standard principles which allow our objects easy to change and easy to test.
So, let’s dig into it in detail.
'S' (Single responsibility principle)
As the name suggests, a class should be solely responsible for one thing and only one thing. If we need to change the class, there should be one and only one reason to change. So, when you design a class, think about what that class should be doing. If you find that the class is taking care of multiple things, then plan to break that class into different classes.
SRP allows you to have modules loosely coupled. (Coupling means, what are the dependencies between two or more components (objects) in your application. If your application has high coupling, it means that the independent components are highly dependent on each other. If we change one ,there is a very high possibility that we may break another.)
Ex- 1
- public class BankAccount {
- public BankAccount() {}
- public string AccountNumber {
- get;
- set;
- }
- public decimal AccountBalance {
- get;
- set;
- }
- public decimal CalculateInterest() {}
- }
In the above given example, class BankAccount is holding Accountdetails but it is also doing interest calculation. so, this class has two different responsibilities which is against the SRP's principle.
In order to follow SRP, we need to break this class into two different classes. 1. Only to hold BankAccount details 2. Only to calculate intrest.
'O' - Open/Closed principle(OCP)
According to this principle, classes should be open for extension but closed for modification. Let me explain what it means and how to implement this.
- class BankAccount {
- public string accountNumber {
- get;
- set;
- }
- public double accountBalance {
- get;
- set;
- }
- public double CalculateIntrest() {
- return something;
- }
- }
The above code looks clean and there is no problem in that. Now, consider a scenario where we have been asked to calculate the interest based on certain criteria e.g accounttype , account balance etc.
Now, one option we have is, we will keep changing the logic whenever the bank adds any new type of account or makes any changes in the existing one. The problem in this solution is that even if we need to add a new account type, we will be adding risk to the existing account type as well, because they are using a common code.
Additionally, since the code base is the same, the testing of this change request will be extra additional efforts, since we need to test existing as well as new account type.
By following this principle, we would like to do below.
- public abstract class BankAccount {
- public abstract double CalculateIntrest();
- }
- class SavingAccount: BankAccount {
- public override double CalculateIntrest() {
- return something;
- }
- }
- class FixedDeposits: BankAccount {
- public override double CalculateIntrest() {
- return something;
- }
- }
So now, we have a class BankAccount which has a method CalculateIntrest() and all child classes are getting inherited from this call but all sub classes are free to have their own defined business rule. Even if they want to change the rule or need to update their interest rates based on any attribute they can do it, without impacting others classes or existing application. And also, if any new account type is introduced in future they can extend new class from BankAccount.
'L' -LSP (Liskov substitution principle)
As per the definition of LISKOV principle, it says the parent should easily replace the child object. Now let’s continue using the same example after changing the parent class from abstract class to normal class and create a new class – checkingAccount.
Checking account is not eligible for any interest. But as per our above implementation, if we create below class and implement CalculateIntrest() as below, it is violating the principle. As CalculateIntrest() method is having its own implementation.
- public class BankAccount {
- public virtual double CalculateIntrest() {
- return something;
- }
- }
- class CheckinDeposits: BankAccount {
- public string accountNumber {
- get;
- set;
- }
- public override double CalculateIntrest() {
- throw new Exception("Not allowed");
- }
- }
The above example is violating the principle of LSP. The solution is,
- interface BankAccount {
- double DepositMoney();
- double WithDrawMoney();
- }
- interface IntrestCalculator {
- double CalculateIntrest();
- }
- class CheckingAccount: BankAccount {
- public string accountNumber {
- get;
- set;
- }
- public double DepositMoney() {
- return something;;
- }
- public double WithDrawMoney() {
- return something;
- }
- }
- class SavingAccount: BankAccount, IntrestCalculator {
- public string accountNumber {
- get;
- set;
- }
- public double DepositMoney() {
- return something;
- }
- public double WithDrawMoney() {
- return something;
- }
- public double CalculateIntrest() {
- return something;
- }
- }
“I” - ISP (Interface Segregation principle)
Consumer of an interface should not be forced to use all of its method, if the client does not want to or does not need them. Which means , if you have a BIG FAT interface, try to see can that interface be broken into different interfaces? If you find it yes , then break it. Please note – breaking the interfaces does not mean that in any case you need to break it. If you think all the methods available in that interface are related and all client need it, then no need to over complicate it.
Example
- interface ILog {
- void log();
- }
- interface IDBLog {
- void closeConnection();
- }
In the above interfaces, if we put both methods together, then closeConnection() might only be useful if we are using DB logging. So better to break that into separate interface.
"D" - (Dependency Inversion Principle)
As per this principle, high level module should not be directly dependent on low level module. If we need to set the dependency of a low-level module inside a high level module, it should be via abstraction. I found a very nice explanation of this in the below blog -
https://www.codeproject.com/Articles/615139/An-Absolute-Beginners-Tutorial-on-Dependency-Inver
Again, as I mentioned in the beginning, S.O.L.I.D principles are the guidelines but not hard and fast rules. So while designing solutions or developing any new application or enhancing existing applications, follow these principles to make a maintainable solution.