SOLID Principles
Five separate principles come together to define the SOLID principles.
S stands for the Single Responsibility principle.
O stands for the Open Close principle.
L stands for the Liskov Substitution principle.
I stands for the Interface Segregation principle.
D stands for the Dependency Inversion principle.
Note that these are the software design best practice principles. How strict does one wants to adhere to these principles in their application is one’s choice. There is always a tradeoff between over-engineering and following best practices. Being familiar with these principles helps a developer to write better code.
Single Responsibility Principle
A class should have only one responsibility i.e. there should be only one reason to change a class. Furthermore giving a class multiple responsibilities is more likely to introduce bugs later when a change is made. Another reason to have a class follow single responsibility is to avoid a whole load of merge conflicts (and thus creating a possibility of bad merges) with other developers who happen to work on a different feature but end up touching the same class. Extracting functionality into its specific class also enables code reusability.
Note that single responsibility does not mean that a class should do only one job. It means everything it does should be related to avoid bloated classes.
Open Closed Principle
A class should be open for extension but closed for modification. In order to add new functionality or feature to an application already in production, the existing classes or methods should not be edited. Any changes in the methods may result in failing unit tests and has the potential to cause issues with the existing functionality itself as thorough regression testing will be required to ensure nothing already working in production will fail upon the release.
One way to achieve this is to use the Decorator pattern. Create a new implementation of the same interface and control the implementation to use at runtime. The only pitfall here is that the method that needs to be extended is included in the Interface and is public. So, if you are using a feature flag, you could control which implementation to serve at run-time.
Extension methods can also be used to extend the functionality of a class. The disadvantage here is that it cannot be controlled at run time.
In summary, think of this principle as how could you add another piece of functionality to your application without affecting the already working production code.
Again, think off if the requirements have changed and instead of modifying the old code, you end up extending the functionality and the old code never gets used.
Liskov Substitution Principle
In essence, it states that a child class should be able to do everything a parent class can. Thus, substituting the parent class object with the child class object should not throw any exceptions.
Interface Segregation Principle
Interfaces are contracts that the class need to implement. If the interface is bulky and some of the methods mentioned would not be implemented, then this interface can be segregated into separate smaller interfaces. So in essence, the classes should only implement interfaces that they intend to use.
Again, there is a trade off here. Don’t go too far and end up having one method per interface. That is not a good design.
Dependency Inversion Principle
In the simplest form it states that the high level modules should not depend upon the low level modules. They should both depend upon an abstraction.
Using dependency inversion makes the code a lot easier to test as one can use a Mock for the dependency.