SOLID Design Patterns

Introduction:

SOLID is short for 5 Design Patterns/ Principles (S-O-L-I-D) of Coding which are very commonly used in every software system. All the frameworks and existing solutions are based on these principles hence its very important to understand them before you even start designing or writing your piece of code. These are the guidelines to write clean code which is easy to maintain and extend without creating lots of bugs.

  1. S - Single Responsibility 
  2. - Open Closed Principle
  3. L - Liskov Substitution
  4. - Interface Segregation
  5. D- Dependency Inversion/Injection

S- Single Responsibility

This pattern states that every class/piece of code should have single responsibility and lots of tasks should not be aggregated into a single class. for example if a class Book stores information about a book, it should not be responsible about printing the book or logging information about it onto the console. we can divide the task of printing or logging into a separate class. For example:

class Book{ 
 public int Id {get; set;} 
 public string Name {get;set;} 
 public string Author {get;set;} 
 public string Text {get;set} 
 public List<int> FindWordsinText(string words) 
 { 
 //search the book's text property for the words 
 } 
 }

class Printing
{
    public void PrintonConsole(string text) {
}
public void PrintinFile(string text, string fileName)
{

}
}

The book class here is only responsible to manage the properties of Book class where as Printing is a class which can print the content of passed string onto the target destination output source.

This is Separation of concerns that we usually talk about in Object Oriented programming

O- Open Closed Principle

It states that Classes should be open for extension but closed for modification. The classes should be designed in such a way that other than bug fixing, the additional modified behaviour should be implemented in the derived or extension classes. Exisiting code when released should not be modified for adding new behaviour. This causes more bugs and can break existing systems. This can better be understood by example.

If I have a car class and I want to add new features to the car class, the better approach is that I drive a new car from existing car. 

public class Car
{
public string Model {get; set;}
public string Make{get;set;}
public int Year {get; set;}
public int Milage {get; set;}

}

public class ElectricCar : Car
{
public int Power {get;set;}
public int MaxSpeed {get;set;}
}


instead of modifying the behavior of Car and breaking existing code using Car, adding a new class to extend the functionality of electric car, we can drive a class from the base class. This also applies to interfaces as Interface are the contract. You will see this in the Interface segregation principle as well.


L- Liskov Substitution

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() {
        
    }
public void feedTheBear() {
        
    }
}

And finally, we can leave the dangerous stuff to the crazy people:

public class CrazyPerson implements BearPetter {
public void petTheBear() {
        
    }
}

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.

D - 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.

Now 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.



Comments

Popular posts from this blog

How to prepare for AZ-204 Developing for Microsoft Azure Certification Exam

Data Annotations in ASP.NET Core