SOLID Principles

➡ SRP (Single Responsibility Principle)

The single responsibility principle states that a class should have responsibility over a single part of the functionality of the application. The class should be cohesive - designed around a set of related functions.

Example

Problem:
The class CardGame has too many responsibilities: showing the UI, game logic, managing the deck of cards.

Solution:
Each class has a single responsibility, and will contain methods related to eachother.


Design patterns

  • Adapter - responsibility is to adapt a class to a target interface.
  • Decorator - divide a class that implements many possible variants of behavior into several smaller classes.
  • Facade - responsibility is to be an intermidiary between the subsystem and the client.
  • Factory - responsibility is to instantiate a class.
  • MVC - Each part of the program (Model, View, Controller) is designed around one part of the program's functionality. Model - system data, View - presentation and interaction with user, Controller - flow of data between view and model.
  • State - organize the code related to particular states into separate classes.


➡ OCP (Open Closed Principle)

The open closed principle states that a class should be open for extension, but close for modification. When adding new functionality, minimal changes should occur in existing code. To code for this principle, we should use abstraction and different subclasses.

Example

Problem:
The class company needs two different methods to add the two different types of employees. The same for removing the employees. Because the employees are of different classes, they cannot be in the same list, so the class needs two different ArrayLists with different parameters.


We want to add a new kind of employee: Consultant. In this case, we need to add two new methods to add and remove this type of employee, make a new list, and modify the code of the getNumberOfEmployees() method to account for this new list. This violates the Open Closed Principle.

Solution:

In this case, the class Company only needs one ArrayList with parameter Employee, one method to add and one to remove, and if we want to add any type of employee, it only needs to extend the Employee abstract class and no existing code needs to be changed. 

Design patterns

  • Adapter - introduce new types of adapters into the program without breaking the existing client code, as long as they work with the adapters through the client interface.
  • Factory - introduce new variants of an abstract class/ interface without breaking existing client code. We only need to (slightly) modify the factory or the Enum if we use one (which does violate the OCP, but supports OCP for the rest of the application).
  • MVC - Since the three parts are separated, it is easy to introduce new functionality in one part without changing the others. For example, we can add a new View without having to change the Model or the Controller
  • Observer - introduce new observer subclasses without having to change the observable code.
  • Strategy - introduce new strategies without changing the context.

➡ LSP (Liskov Substitution Principle)

Liskov Substitution Principle states that a superclass object should be replaceable with a subclass without losing any functionality. In other words, when a class implements an interface or extends a parent class, it should only add functionality, never limit.

Example

Problem:
The class Duck does add new functionality to the superclass Bird, but the class Ostrich limits it, by throwing an exception.

Solution:
In this case, the class FlyingBird contains the method fly( ), and Ostrich doesn't limit the functionality of the class Bird. This design respects the LSP.

Design patterns

  • Decorator - adds functionality to the class it wraps without limiting it.
  • State - the state design pattern might violate the LSP, since most of the time different states have different behaviours in terms of usable methods and methods that throw exceptions.
  • Strategy - each algorithm in the Strategy pattern has the same functionality and can be swapped at runtime.

➡ ISP (Interface Segregation Principle)

The Interface Segregation Principle states that no code should be forced to depend on methods it does not use.

Example

Problem:
We have two kinds of coffee machine: a Basic machine that brews filter coffee and an Espresso machine that brews espresso coffee.
We might have something like this:

The problem here is that the BasicCoffeeMachine class inherits the brewEspresso( ) method, and the EspressoMachine inherits the brewFilterCoffee( ) method, which are respectively not used. That violates the Interface Segregation principle. We can override these methods to throw an exception if the client tries to use it. This would also violate the Liskov Substitution Principle.

Solution:

In this design, the methods are moved to different interfaces. If we have a new machine that uses filter coffee, it can implement the FilterCoffeeMachine interface. And if we have a new brew method, we can add a new interface that extends the CoffeeMachine interface. The classes now use all the methods they inherit, which respects the ISP.

Design patterns

  • Observer - the Observer interface has only one method, update( ), which is always used in the observer pattern. This respects the ISP.
  • Strategy - all the concrete strategies implement and use all functionalities of the Strategy interface.

➡ DIP (Dependency Inversion Principle)

The Dependency Inversion Principle states that high-level modules should not depend on low-level modules. Both should depend on the abstraction. The abstractions should not depend on details, but the details should depend on abstractions.

Example

Problem:
Take this code for example:

public class Copy {
    public String readFromKeyboard(KeyboardReader keyboardReader) {
        return keyboardReader.read( );
    }
    public void writeToPrinter(PrinterWriter printWriter, String text) {
        return printWriter.writeToPrinter(text);
    }
}


UML:  

This principle violates the Dependency Inversion Principle because the high-level class Copy depends on the low-level classes. This design is not flexible. For example, to add a new way of writing, like printing to a file, we need to alter the code of the Copy class, which violates the OCP, adding a new method to print to a file.

Solution:

public class Copy {
    public String read(Reader r) {
        return r.read( ); 
    }
    public void writeToPrinter(Writer w, String text) {
        return w.write(text);
    }
}


By inserting a level of abstraction we don't need to change the Copy class when we add new implementations of the Reader or Writer implementations.

Design patterns

    • Adapter - client talks to abstraction (target interface) and adapter implements abstraction (target interface)
    • Decorator - the decorator extends / implements the same abstraction as the class it wraps. The client uses that abstraction.
    • Factory - client and concrete class made by factory both depend on abstraction
    • State - client and concrete states depend on state interface
    • Strategy - context and concrete strategies depend on strategy abstraction
    • Template method - the classes that use the template method depend on the abstraction where the method is

    Comments