SOLID principles
1. Introduction to SOLID principles
We'll start by exploring the
reasons they came about and why we should consider them when
designing software. Then, we'll outline each principle alongside some example
code to emphasize the point.
2. The Reason for SOLID Principles
The SOLID principles were first conceptualized by
Robert C. Martin in his 2000 paper, Design Principles
and Design Patterns. These concepts were later
built upon by Michael Feathers, who introduced us to the SOLID acronym. And in
the last 20 years, these 5 principles have revolutionized the world of
object-oriented programming, changing the way that we write software.
So, what is SOLID and how does it help us write better
code? Simply put, Martin's and Feathers' design
principles encourage us to create more maintainable, understandable, and
flexible software. Consequently, as our
applications grow in size, we can reduce their complexity and
save ourselves a lot of headaches further down the road!
The following 5 concepts make up our SOLID principles:
- Single
Responsibility
- Open/Closed
- Liskov Substitution
- Interface Segregation
- Dependency
Inversion
While some of these words may sound daunting, they can
be easily understood with some simple code examples. In the following sections,
we'll take a deep dive into what each of these principles means, along with a
quick .net example to illustrate each one.
3. Single Responsibility
Let's kick things off with the single responsibility
principle. As we might expect, this principle states that a class should only have one responsibility. Furthermore, it
should only have one reason to change.
How does this principle help us to build better software? Let's see a few of its benefits:
- Testing –
A class with one responsibility will have far fewer test cases
- Lower
coupling – Less functionality in a single
class will have fewer dependencies
- Organization –
Smaller, well-organized classes are easier to search than monolithic ones
Take, for example, a class to represent a simple book:
public
class Book {
private String name;
private String author;
private String text;
//constructor, getters and setters
}
In this code, we store the name, author, and text
associated with an instance of a Book.
Let's now add a couple of methods to query the text:
public
class Book {
private String name;
private String author;
private String text;
//constructor, getters and setters
// methods that directly relate to the book properties
public String replaceWordInText(String word){
return text.replaceAll(word, text);
}
public boolean isWordInText(String word){
return text.contains(word);
}
}
Now, our Book class
works well, and we can store as many books as we like in our application. But,
what good is storing the information if we can't output the text to our console
and read it?
Let's throw caution to the wind and add a print
method:
public
class Book {
//...
void printTextToConsole(){
// our code for formatting and printing the text
}
}
This code does, however, violate the single
responsibility principle we outlined earlier. To fix our mess, we should
implement a separate class that is concerned only with printing our texts:
public
class BookPrinter {
// methods for outputting text
void printTextToConsole(String text){
//our code for formatting and printing the text
}
void printTextToAnotherMedium(String text){
// code for writing to any other location..
}
}
Awesome. Not only have we developed a class that
relieves the Book of its
printing duties, but we can also leverage our BookPrinter class
to send our text to other media.
Whether it's email, logging, or anything else, we have
a separate class dedicated to this one concern.
4. Open for Extension, Closed for Modification
Now, time for the ‘O' – more formally known as
the open-closed principle. Simply put, classes should be open for extension, but closed for modification. In doing so, we stop
ourselves from modifying existing code and causing potential new bugs in
an otherwise happy application.
Of course, the one exception to the rule is when fixing bugs in existing code.
Let's explore the concept further with a quick code
example. As part of a new project, imagine we've implemented a Guitar class.
It's fully fledged and even has a volume knob:
public
class Guitar {
private String make;
private String model;
private
int volume;
//Constructors, getters & setters
}
We launch the application, and everyone loves it.
However, after a few months, we decide the Guitar is
a little bit boring and could do with an awesome flame pattern to make it look
a bit more ‘rock and roll'.
At this point, it might be tempting to just open up
the Guitar class and add a flame pattern – but
who knows what errors that might throw up in our application.
Instead, let's stick to the
open-closed principle and simply extend our Guitar class:
public
class SuperCoolGuitarWithFlames extends Guitar {
private String flameColor;
//constructor, getters + setters
}
By extending the Guitar class
we can be sure that our existing application won't be affected.
5. Liskov Substitution
Next up on our list is Liskov substitution,
which is arguably the most complex of the 5 principles. Simply put, if class A is a
subtype of class B, then we
should be able to replace B with A without disrupting the behavior of our program.
Let's just jump straight to the code to help wrap our
heads around this concept:
public
interface Car {
void turnOnEngine();
void accelerate();
}
Above, we define a simple Car interface
with a couple of methods that all cars should be able to fulfill – turning
on the engine, and accelerating forward.
Let's implement our interface and provide some code for the methods:
public
class MotorCar implements Car {
private Engine engine;
//Constructors, getters + setters
public void turnOnEngine() {
//turn on the engine!
engine.on();
}
public void accelerate() {
//move forward!
engine.powerOn(
1000);
}
}
As our code describes, we have an engine that we can
turn on, and we can increase the power. But wait, its 2019, and Elon Musk has
been a busy man.
We are now living in the era of electric cars:
public
class ElectricCar implements Car {
public void turnOnEngine() {
throw
new AssertionError(
"I don't have an engine!");
}
public void accelerate() {
//this acceleration is crazy!
}
}
By throwing a car without an engine into the mix, we
are inherently changing the behavior of our program. This is a blatant violation of Liskov substitution and is a bit
harder to fix than our previous 2 principles.
One possible solution would be to rework our model
into interfaces that take into account the engine-less state of our Car.
6. Interface Segregation
The ‘I ‘ in SOLID stands for interface segregation,
and it simply means that larger
interfaces should be split into smaller ones. By doing so, we can ensure that
implementing classes only need to be concerned about the methods that are of
interest to them.
For this example, we're going to try our hands as
zookeepers. And more specifically, we'll be working in the bear enclosure.
Let's start with an interface that outlines our roles
as a bear keeper:
public
interface BearKeeper {
void washTheBear();
void feedTheBear();
void petTheBear();
}
As avid zookeepers, we're more than happy to wash and feed our beloved bears. However, we're all too aware of the dangers of petting them. Unfortunately, our interface is rather large, and we have no choice than to implement the code to pet the bear.
Let's fix this by
splitting our large interface into 3 separate ones:
public
interface BearCleaner {
void washTheBear();
}
public
interface BearFeeder {
void feedTheBear();
}
public
interface BearPetter {
void petTheBear();
}
Now, thanks to interface segregation, we're free to
implement only the methods that matter to us:
public
class BearCarer implements BearCleaner, BearFeeder {
public void washTheBear() {
//I think we missed a spot...
}
public void feedTheBear() {
//Tuna Tuesdays...
}
}
And finally, we can leave the dangerous stuff to the
crazy people:
public
class CrazyPerson implements BearPetter {
public void petTheBear() {
//Good luck with that!
}
}
Going further, we could even split our BookPrinter class
from our example earlier to use interface segregation in the same way. By
implementing a Printer interface
with a single print method, we
could instantiate separate ConsoleBookPrinter and OtherMediaBookPrinter classes.
7. Dependency Inversion
The principle of Dependency Inversion refers to the decoupling of
software modules. This way, instead of high-level modules depending on
low-level modules, both will depend on abstractions.
To demonstrate this, let's go old-school and bring to
life a Windows 98 computer with code:
public
class Windows98Machine {}
But what good is a computer without a monitor and
keyboard? Let's add one of each to our constructor so that every Windows98Computer we instantiate comes pre-packed
with a Monitor and a StandardKeyboard:
public
class Windows98Machine {
private
final StandardKeyboard keyboard;
private
final Monitor monitor;
public Windows98Machine() {
monitor =
new Monitor();
keyboard =
new StandardKeyboard();
}
}
This code will work, and we'll be able to use the StandardKeyboard and Monitor freely
within our Windows98Computer class.
Problem solved? Not quite. By declaring
the StandardKeyboard and Monitor with the new keyword, we've tightly coupled these 3 classes together.
Not only does this make our Windows98Computer hard to test, but we've also lost the ability to switch out our StandardKeyboard class with a different one should the need arise. And we're stuck with our Monitor class, too.
Let's decouple our machine from the StandardKeyboard by adding a more general Keyboard interface and using this in our class:
public
interface Keyboard { }
public
class Windows98Machine{
private
final Keyboard keyboard;
private
final Monitor monitor;
public Windows98Machine(Keyboard keyboard, Monitor monitor) {
this.keyboard = keyboard;
this.monitor = monitor;
}
}
Here, we're using the dependency injection pattern
here to facilitate adding the Keyboard dependency
into the Windows98Machine class.
Let's also modify our StandardKeyboard class
to implement the Keyboard interface
so that it's suitable for injecting into the Windows98Machine class:
public
class StandardKeyboard implements Keyboard { }
Now our classes are decoupled and communicate through
the Keyboard abstraction. If we want, we can easily
switch out the type of keyboard in our machine with a different implementation
of the interface. We can follow the same principle for the Monitor class.
Excellent! We've decoupled the dependencies and are
free to test our Windows98Machine with
whichever testing framework we choose.
Dependency Injection in C#
Why do we need the Dependency Injection in C#?
The Dependency Injection is a design pattern that allows us to develop loosely coupled software components. In other words, we can say that this design pattern is used to reduce the tight coupling between the software components. As a result, we can easily manage future changes and other complexity in our application.
Before understanding the Dependency Injection Design Pattern using C#, first, we need to understand what is tight coupling and what is loose coupling in software development. So let’s understand these two concepts first.
What is Tight Coupling in Software Design?
Tight coupling means classes and objects are dependent on each other. That means when a class is dependent on another concrete class, then it is said to be a tight coupling between these two classes. In that case, if we change the dependent object, then we also need to change the classes where this dependent object is used. If your application is a small one, then it is not that difficult to handle but if you have a big enterprise-level application, then its really very difficult to handle to make these changes.
What is Loose Coupling in Software Design?
Loosely coupling means two objects are independent of each other. That means if we change one object then it will not affect another object. The loosely coupled nature of software development allows us to manage future changes easily and also allows us to manage the complexity of the application.
What is Dependency Injection Design Pattern in C#?
The Dependency Injection Design Pattern in C# is a process in which we are injecting the object of a class into a class that depends on that object. The Dependency Injection design pattern is the most commonly used design pattern nowadays to remove the dependencies between the objects.
Dependency Injection (DI) is a design pattern used to implement IoC. It allows the creation of dependency objects outside of a class and provides those objects to a class in different ways. Using DI, we move the creation and binding of the dependent objects outside of the class that depends on them.
Dependency Injection pattern involves 3 types of classes:
1. Client Class: The Client class (dependent class) is a class that depends on the service class.
2. Service Class: The Service class (dependency) is a class that provides service to the client class.
3. Injector Class: The Injector class injects the service class object into the client class.
For better understanding, please have a look at the following diagram.
As you can see above in the above diagram, the injector class creates an object of the service class and injects that object to a client class. In this way, the Dependency Injection pattern separates the responsibility of creating an object of the service class out of the client class.
Different Types of Dependency Injection in C#?
The injector class injects the dependency object to a class in three different ways. They are as follows.
Constructor Injection: When the Injector injects the dependency object (i.e. service) through the client class constructor, then it is called as Constructor Injection.
Property Injection: When the Injector injects the dependency object (i.e. service) through the public property of the client class, then it is called as Property Injection. This is also called as the Setter Injection.
Method Injection: When the Injector injects the dependency object (i.e. service) through a public method of the client class, then it is called as Method Injection. In this case, the client class implements an interface that declares the method(s) to supply the dependency object and the injector uses this interface to supply the dependency object (i.e. service) to the client class.
Here, in this article, we will discuss how to inject the dependency object through the constructor. In the next article, I am going to discuss the Method and Property Dependency injection in C# with examples.
Constructor Dependency Injection in C#:
Let us understand the Constructor Dependency Injection in C# with an example. Let’s create a console application. to do so select, File => New => Project and then select the console application as shown below.
Now create 3 classes Employee.cs, EmployeeDAL.cs and EmployeeBL.cs as shown below
Employee.cs
namespace DependencyInjectionExample
{
public class Employee
{
public int ID { get; set; }
public string Name { get; set; }
public string Department { get; set; }
}
}
EmployeeDAL.cs
namespace DependencyInjectionExample
{
public class EmployeeDAL
{
public List<Employee> SelectAllEmployees()
{
List<Employee> ListEmployees = new List<Employee>();
//Get the Employees from the Database
//for now we are hard coded the employees
ListEmployees.Add(new Employee() { ID = 1, Name = "Pranaya", Department = "IT" });
ListEmployees.Add(new Employee() { ID = 2, Name = "Kumar", Department = "HR" });
ListEmployees.Add(new Employee() { ID = 3, Name = "Rout", Department = "Payroll" });
return ListEmployees;
}
}
}
EmployeeBL.cs
namespace DependencyInjectionExample
{
public class EmployeeBL
{
public EmployeeDAL employeeDAL;
public List<Employee> GetAllEmployees()
{
employeeDAL = new EmployeeDAL();
return employeeDAL.SelectAllEmployees();
}
}
}
In the above example, in order to get the data, the EmployeeBL class depends on the EmployeeDAL class. In the GetAllEmployees() method of the EmployeeBL class, we create an instance of the EmployeeDAL (Employee Data Access Layer) class and then invoke the SelectAllEmployees() method. This is tight coupling because the EmployeeDAL is tightly coupled with the EmployeeBL class. Every time the EmployeeDAL class changes, the EmployeeBL class also needs to change.
Let us see how to use the constructor injection to make these classes loosely coupled.
Modify the EmployeeDAL.cs file as shown below
namespace DependencyInjectionExample
{
public interface IEmployeeDAL
{
List<Employee> SelectAllEmployees();
}
public class EmployeeDAL : IEmployeeDAL
{
public List<Employee> SelectAllEmployees()
{
List<Employee> ListEmployees = new List<Employee>();
//Get the Employees from the Database
//for now we are hard coded the employees
ListEmployees.Add(new Employee() { ID = 1, Name = "Pranaya", Department = "IT" });
ListEmployees.Add(new Employee() { ID = 2, Name = "Kumar", Department = "HR" });
ListEmployees.Add(new Employee() { ID = 3, Name = "Rout", Department = "Payroll" });
return ListEmployees;
}
}
}
As you can see, first we create one interface i.e IEmployeeDAL with the one method. Then that interface is implemented by the EmployeeDAL class. So the point that I need to keep focus is when you are going to use the dependency injection design pattern in c#, then the dependency object should be interface based. In our example, the EmployeeDAL is the dependency object as this object is going to be used by the EmplyeeBL class. So we created the interface and then implement that interface.
Modify the EmployeeBL.cs file as shown below
namespace DependencyInjectionExample
{
public class EmployeeBL
{
public IEmployeeDAL employeeDAL;
public EmployeeBL(IEmployeeDAL employeeDAL)
{
this.employeeDAL = employeeDAL;
}
public List<Employee> GetAllEmployees()
{
Return employeeDAL.SelectAllEmployees();
}
}
}
In the above example, we created one constructor which accepts one parameter of the dependency object type. The point that you need to keep focus is, the parameter of the constructor is of the type interface, not the concrete class. Now, this parameter can accept any concrete class object which implements this interface.
So here in the EmployeeBL class, we are not creating the object of the EmployeeDAL class. Instead, we are passing it as a parameter to the constructor of the EmployeeBL class. As we are injecting the dependency object through the constructor, it is called as constructor dependency injection in C#.
Let’s us see how to use EmployeeBL class in our Main method of Program class:
namespace DependencyInjectionExample
{
class Program
{
static void Main(string[] args)
{
EmployeeBL employeeBL = new EmployeeBL(new EmployeeDAL());
List<Employee> ListEmployee = employeeBL.GetAllEmployees();
foreach(Employee emp in ListEmployee)
{
Console.WriteLine("ID = {0}, Name = {1}, Department = {2}", emp.ID, emp.Name, emp.Department);
}
Console.ReadKey();
}
}
}
Now run the application and you will see the output as expected as shown below.
Advantages of Constructor Dependency Injection
1. The Constructor Dependency Injection Design Pattern makes a strong dependency contract
2. This design pattern support testing as the dependencies are passed through the constructor.