Solid Principles (Java)

In all the below examples they are not complete but basically show you in principle how to break a specified principle and what you should have done.

Single Responsibility Principle (Java)

The Single Responsibility Principle (SRP) basically means that a class should have one and only one responsibility, it should be encapsulated within the class, the class should not be a GOD object.

Breaking the SRP rule If you notice in the Journal we have the entries array the addEntry and removeEntry methods, toString and count all off which relates to the Journal, however at a later date the persistence code is added which really has nothing to do with the Journal and thus breaking the Single Responsibility Principle rule.
package uk.co.datadisk.solidPrinciples;

import java.io.FileNotFoundException;
import java.io.PrintStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;

class Journal {
    private final List<String> entries = new ArrayList<>();

    private static int count = 0;

    public void addEntry(String text){
        entries.add("" + (++count) + ": " + text);
    }

    public void removeEntry(int index){
        entries.remove(index);
    }

    @Override
    public String toString() {
        return String.join(System.lineSeparator(), entries);
    }

    // VERY BAD
    // Now we break the Single Responsibility Principle
    // Should create a separate persistence class
    public void save(String filename) throws FileNotFoundException {
        try (PrintStream out = new PrintStream(filename)){
            out.println(toString());
        }
    }

    public void load(String filename){}
    public void load(URL url){}
}

public class Main_Single_Responsibility_Principle {

    // Looking at Single Responsibility Principle

    public static void main(String[] args) throws Exception {
        Journal j = new Journal();
        j.addEntry("I learned Java");
        j.addEntry("I leaned Design Patterns");
        System.out.println(j);
    }
}
Following the SRP rule We break out the persistence code into its own class, we can even make this generic to handle all types of data. Thus the Journal class is only responsible for the Journal and Persistence class only deals with persisting the data, both now follow the Single Responsible Principle.
package uk.co.datadisk.solidPrinciples;

import java.io.FileNotFoundException;
import java.io.PrintStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;

class Journal {
    private final List<String> entries = new ArrayList<>();

    private static int count = 0;

    public void addEntry(String text){
        entries.add("" + (++count) + ": " + text);
    }

    public void removeEntry(int index){
        entries.remove(index);
    }

    @Override
    public String toString() {
        return String.join(System.lineSeparator(), entries);
    }
}


// We create a Persistence class that can used 
class Persistence {

    constructor......

    public void save(List<T> j, String filename) throws FileNotFoundException {
        try (PrintStream out = new PrintStream(filename)){
            out.println(j.toString());
        }
    }

    public void load(String filename){}
    public void load(URL url){}
}

public class Main_Single_Responsibility_Principle {

    // Looking at Single Responsibility Principle

    public static void main(String[] args) throws Exception {
        Journal j = new Journal();
        j.addEntry("I learned Java");
        j.addEntry("I leaned Design Patterns");
        System.out.println(j);
        
        Persistence p = new Persistence("Journal", j)
    }
}

Open-Closed Principle

The Open-Closed Principle (OCP) should be open for extension (extending) but closed for modification, the reason for this is that if a class has been tested and you know it works don't change it. This class could be at any clients location or in a library and thus making changes could cause issues. It uses interfaces instead of superclasses to allow different implementations which you can easily substitute without changing the code that uses them.

Breaking the OCP rule The original ProductFilter was created and tested, then we started to make changes again and again thus breaking the Open-Closed Principle, remember Open for extending but closed for modification.
enum Color { RED, GREEN, BLUE }

enum Size { SMALL, MEDIUM, LARGE, YUGE }

class Product {
    // Normally private with getters and setters but to cut down on code
    public String name;
    public Color color;
    public Size size;

    public Product(String name, Color color, Size size) {
        this.name = name;
        this.color = color;
        this.size = size;
    }
}

// THIS IS A BAD IDEA, ONCE CREATED AND TESTED YOU SHOULD NEVER ADD TO THIS CLASS
class ProductFilter {
    public Stream<Product> filterByColor(List<Product> products, Color color) {
      return products.stream().filter(p -> p.color == color);
    }

    // Depending on the number of Filters this could get out of hand
    // BAD - BREAKS THE OPEN-CLOSED PRINCIPLE

    // Added this method after above
    public Stream<Product> filterBySize(List<Product> products, Size size) {
        return products.stream().filter(p -> p.size == size);
    }

    // Added another method after one above, etc
    public Stream<Product> filterBySizeAndColor(List<Product> products, Size size, Color color) {
        return products.stream().filter(p -> p.size == size && p.color == color);
    }
}

public class Main_Open_Closed_Principle {

    // Looking at Open-Closed Principle and Specification

    public static void main(String[] args) {
        Product apple = new Product("Apple", Color.GREEN, Size.SMALL);
        Product tree = new Product("Tree", Color.GREEN, Size.LARGE);
        Product house = new Product("House", Color.BLUE, Size.LARGE);

        List<Product> products = Arrays.asList(apple, tree, house);

        // BAD SOLUTION
        ProductFilter pf = new ProductFilter();
        System.out.println("Green products (old): ");
        pf.filterByColor(products, Color.GREEN).forEach(p -> System.out.println(" - " + p.name + " is " + Color.GREEN));
    }
}
Following the OCP rule You use interfaces and inheritance to create different types of filters thus keeping to the Open-Closed Principle.
enum Color { RED, GREEN, BLUE }

enum Size { SMALL, MEDIUM, LARGE, YUGE }

class Product {
    // Normally private with getters and setters but to cut down on code
    public String name;
    public Color color;
    public Size size;

    public Product(String name, Color color, Size size) {
        this.name = name;
        this.color = color;
        this.size = size;
    }
}

interface Specification<T> {
    boolean isSatisfied(T item);
}

// If we are asked to create more filters we simply create new specification classes that will be passed to the filter

// This class does one thing checks the color
class ColorSpecification implements Specification<Product> {

    private Color color;

    public ColorSpecification(Color color) {
        this.color = color;
    }

    @Override
    public boolean isSatisfied(Product item) {
        return item.color == color;
    }
}

// This class does one thing checks the size
class SizeSpecification implements Specification<Product> {

    private Size size;

    public SizeSpecification(Size size) {
        this.size = size;
    }

    @Override
    public boolean isSatisfied(Product item) {
        return item.size == size;
    }
}

// We can check for both size and color
class AndSpecification<T> implements Specification<T> {

    private Specification<T> first, second;

    // We could pass a color and size specification
    public AndSpecification(Specification<T> first, Specification<T> second) {
        this.first = first;
        this.second = second;
    }

    @Override
    public boolean isSatisfied(T item) {
        // if both are true then return the item
        return first.isSatisfied(item) && second.isSatisfied(item);
    }
}

// We have only one filter class that can handle multiple specifications (the filters)
interface Filter<T> {
    Stream<T> filter(List<T> items, Specification<T> spec);
}

class BetterFilter implements Filter<Product> {
    @Override
    public Stream<Product> filter(List<Product> items, Specification<Product> spec) {

        // if true in the lambda then returns the item
        return items.stream().filter(p -> spec.isSatisfied(p));
    }
}

public class Main_Open_Closed_Principle {

    // Looking at Open-Closed Principle and Specification

    public static void main(String[] args) {
        Product apple = new Product("Apple", Color.GREEN, Size.SMALL);
        Product tree = new Product("Tree", Color.GREEN, Size.LARGE);
        Product house = new Product("House", Color.BLUE, Size.LARGE);

        List<Product> products = Arrays.asList(apple, tree, house);

        // GOOD SOLUTION
        // Now we can create filters to our hearts content without modifying existing classes (we create new ones)
        BetterFilter bf = new BetterFilter();
        System.out.println("Green products (new): ");
        bf.filter(products, new ColorSpecification(Color.GREEN)).forEach(p -> System.out.println(" - " + p.name + " is " + Color.GREEN));

        System.out.println("Large Blue item");
        bf.filter(products, new AndSpecification<>(
                new ColorSpecification(Color.BLUE),
                new SizeSpecification(Size.LARGE)
        )).forEach(p -> System.out.println(" - " + p.name + " is " + Color.BLUE + " and large"));
    }
}

Liskov Subsitution Principle

The Liskov Subsitution Principle (LSP) is that objects of a superclass shall be replaceable with objects of its subclasses without breaking the application. That requires the objects of your subclasses to behave in the same way as the objects of your superclass. So any functions that use references to base classes must be able to use objects of the derived class without knowing it, for example you inherit a class method that starts to break things or behaves oddly.

Breaking the LSP rule This is seems to be the default example show when explaining LSP rule breaking, Square is a Rectangle so based on LSP rule it should behave like a Rectangle and as you can see for this example it does not.
package uk.co.datadisk.solidPrinciples;

class Rectangle {

    protected int width, height;

    public Rectangle() {
    }

    public Rectangle(int width, int height) {
        this.width = width;
        this.height = height;
    }

    public int getWidth() {
        return width;
    }

    public void setWidth(int width) {
        this.width = width;
    }

    public int getHeight() {
        return height;
    }

    public void setHeight(int height) {
        this.height = height;
    }

    public int getArea() { return width * height; }

    @Override
    public String toString() {
        return "Rectangle{" +
                "width=" + width +
                ", height=" + height +
                '}';
    }
}

class Square extends Rectangle {

    public Square() {}

    public Square(int size){
        width = height = size;
    }

    // The below setter methods break the LSP rule.
    // These methods won't work for derived class

    @Override
    public void setWidth(int width) {
        super.setWidth(width);
        super.setHeight(width);
    }

    @Override
    public void setHeight(int height) {
        super.setHeight(height);
        super.setWidth(height);
    }
}

public class Main_Liskov_Substitution_Principle {

    public static void main(String[] args) {

        // The useIt function would be able to work with all derived classes of Rectangle
        Rectangle rc = new Rectangle(2,3);
        useIt(rc);

        // Using the square class get get odd behaviour
        Square sq1 = new Square(5);
        useIt(sq1);

        // The same with using a Rectangle odd behaviour as we have violated LSP.
        // Remember a Square is a Rectangle so this should work based on LSP rule.
        Rectangle sq2 = new Square(5);
        useIt(sq2);
    }

    static void useIt(Rectangle r){
        int width = r.getWidth();

        // Now  violate the principle, it sets both height and width
        r.setHeight(10);

        System.out.println(r);
        System.out.println("Expect an area of " + (width * 10) + ", got " + r.getArea() + "\n");
    }
}

Output
------------------------------------
Rectangle{width=2, height=10}
Expect an area of 20, got 20

Rectangle{width=10, height=10}
Expect an area of 50, got 100                 // something not right here

Rectangle{width=10, height=10}
Expect an area of 50, got 100                 // something not right here
Following the LSP rule So to fix the above example you can either add a method to check its a square and plan accordingly or better still use a factory pattern which I will be explaining in another section.
package uk.co.datadisk.solidPrinciples;

class Rectangle {

    protected int width, height;

    public Rectangle() {
    }

    public Rectangle(int width, int height) {
        this.width = width;
        this.height = height;
    }

    public int getWidth() {
        return width;
    }

    public void setWidth(int width) {
        this.width = width;
    }

    public int getHeight() {
        return height;
    }

    public void setHeight(int height) {
        this.height = height;
    }

    public int getArea() { return width * height; }

    @Override
    public String toString() {
        return "Rectangle{" +
                "width=" + width +
                ", height=" + height +
                '}';
    }

    // could replace the Square class by using the below
    public boolean isSquare(){
        return height == width;
    }
}


// We could implement a Factory class to create Rectangles or Squares
class RectangleFactory {
    public static Rectangle newRectangle(int width, int height){
        return new Rectangle(width, height);
    }

    public static Rectangle newSquare(int side) {
        return new Rectangle(side, side);
    }
}

Interface Segregation Principle

The Interface Segregation Principle (ISP) states that no client should be forced to depend on methods it does not use, you should split interfaces that are very large into smaller and more specific ones so that clients will only have to know about the methods that are of interest to them. There is a design pattern that you can also use here called the decorator pattern which I will cover in another section.

Breaking the ISP rule When the interface is created many methods are added so if you implement this interface you will inherit all the methods even if you don't need then and thus break the ISP rule.
package uk.co.datadisk.solidPrinciples;

class Document {}

// Too many methods in this interface, try to put the absolute minimum into an interface
interface Machine {
    void print(Document d);
    void fax(Document d);
    void scan(Document d);
}

// If we implement Machine we get methods we really don't need fax, scan, etc
class OldFashionedPrinter implements Machine {

    @Override
    public void print(Document d) {
        // WILL BE USED
    }

    @Override
    public void fax(Document d) {
        // DON'T NEED
    }

    @Override
    public void scan(Document d) {
        // DON'T NEED
        
        // You might be tempted to throw an exception but a bad idea as you have to change code
        // to capture the exception
    }
}

public class Main_Interface_Segregation_Principle {

    // Interface Segregation Principle
    public static void main(String[] args) {
		// DO STUFF
    }
}
Following the ISP rule To fix the above example and follow the ISP rule we break up the original interfaces into many interfaces and then even combine them to make more specific interfaces, this then means that you only need to implement what is needed and not additional methods that you don't need.
// Breakup the methods into appropriate interfaces
interface Printer {
    void print(Document d);
}

interface Scanner {
    void scan(Document d);
}

interface Fax {
    void fax(Document d);
}

// We can even use other interfaces to create specific interface types.
interface PhotoCopier extends Printer, Scanner {}

class Document {}

// Only implement what we need
class MultiFunctionPrinter implements Printer, Scanner, Fax {
    @Override
    public void print(Document d) {
        // WILL BE USED
    }

    @Override
    public void fax(Document d) {
        // WILL BE USED
    }

    @Override
    public void scan(Document d) {
        // WILL BE USED
    }
}

// Only implement what we need
class OldFashionedPrinter implements Printer {

    @Override
    public void print(Document d) {
        // WILL BE USED
    }
}

Dependency Inversion Principle

The Dependency Inversion Principle (DIP) requires that High-level modules, which provide complex logic, should be easily reusable and unaffected by changes in low-level modules, which provide utility features. To achieve that, you need to introduce an abstraction that decouples the high-level and low-level modules from each other, by decoupling components it allows for easier testing, most often this is solved by using dependency injection which is different than dependency inversion.

Breaking the DIP rule The Research constructor breaks the DIP rule as its dependent on the Relatioships, also in the method findAllChildrenOf we use a list but what if this type changed as well, the code breaks
package uk.co.datadisk.solidPrinciples;

import org.javatuples.Triplet;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

enum Relationship {
    PARENT, CHILD, SIBLING
}

class Person {
   // made public and simple to keep code to a minimum
   public String name;

    public Person(String name) {
        this.name = name;
    }
}

// low-level module (just provides a list (storage)), no business logic
// This class also meets the Single Responsibility Principle (SRP)
class Relationships {
    private List<Triplet<Person, Relationship, Person>> relations = new ArrayList<>();

    // Getter Method
    public List<Triplet<Person, Relationship, Person>> getRelations() {
        return relations;
    }

    // Setter Method
    void AddParentAndChild(Person parent, Person child){
        relations.add(new Triplet<>(parent, Relationship.PARENT, child));
        relations.add(new Triplet<>(child, Relationship.CHILD, parent));
    }

    public List<Person> findAllChildrenOf(String name) {
        return relations.stream()
                .filter(x -> Objects.equals(x.getValue0().name, name) && x.getValue1() == Relationship.PARENT)
                .map(Triplet::getValue2)
                .collect(Collectors.toList());
    }
}

// high-level module can perform operations on a low-level module
// When developers use this module they don't really care about the low-level module that this is 
// using (Relationships)
class Research {
    // the constructor is dependent on the Relationships, remember the DSP rule is that high-level 
    // modes should not depend on low-level modules - WE ARE BREAKING THE DIP RULE
    // also we are dependent on a list to be returned what would happen if the code changed and 
    // the Collection type was changed

    public Research(Relationships relationships){
        List<Triplet<Person, Relationship, Person>> relations = relationships.getRelations();
        relations.stream().filter(p -> p.getValue0().name.equals("Paul") && p.getValue1() == Relationship.PARENT)
                .forEach(ch -> System.out.println("Paul has a child called " + ch.getValue2().name));
    }
}

public class Main_Dependency_Inversion_Principle {

    // Note: this is not dependency injection

    // A. High-level modules should not depend on low-level modules.
    // Both should depend on abstractions (interface or abstract class).

    // B. Abstractions should not depend on details.
    // Details should depend on abstractions.

    public static void main(String[] args) {

        // Setup the data
        Person parent = new Person("Paul");
        Person child1 = new Person("Dominic");
        Person child2 = new Person("Jessica");

        Relationships relationships = new Relationships();
        relationships.AddParentAndChild(parent , child1);
        relationships.AddParentAndChild(parent, child2);

        // We never had to change a thing to implement the new reachSearchBrowser
        new Research(relationships);
    }
}
Following the DIP rule To fix the examle above we use an interface as the abstraction that will be applied to the low-level module, the retrieval of the list is now handled by the low-level module, which means you can even change the type without affecting any other code as long as we return a list the data storage can be a Triplet, Array, Class, etc. The Research class is changed so it no longer is dependent on the Relationships class as long as a list is retruned it does not care what the underlying storage is.
// This is the abstraction that needs to be applied to the low-level module
interface RelationshipBrowser {
    List<Person> findAllChildrenOf(String name);
}

// low-level module (just provides a list (storage))
class Relationships implements RelationshipBrowser {
    private List<Triplet<Person, Relationship, Person>> relations = new ArrayList<>();

    // Getter Method
    public List<Triplet<Person, Relationship, Person>> getRelations() {
        return relations;
    }

    // Setter Method
    void AddParentAndChild(Person parent, Person child){
        relations.add(new Triplet<>(parent, Relationship.PARENT, child));
        relations.add(new Triplet<>(child, Relationship.CHILD, parent));
    }

    // try to code information retrieval within the same class, thus changing a collection type (or anything else for that matter) at a later
    // date you can make sure you don't affect anyone else's code because we are still supplying the same data back
    @Override
    public List<Person> findAllChildrenOf(String name) {
        return relations.stream()
                .filter(x -> Objects.equals(x.getValue0().name, name) && x.getValue1() == Relationship.PARENT)
                .map(Triplet::getValue2)				// use a reference method Triplet::getValue2
                .collect(Collectors.toList());
    }
}

// high-level can perform operations on a low-level module
class Research {
    
    // No longer dependent on Relationships
    // We still get a list but the underlying code (relations) could swap out the List for something else without affect the below
    public Research(RelationshipBrowser browser){
        List<Person> children = browser.findAllChildrenOf("Paul");
        for(Person child : children){
            System.out.println("Paul has a child called " + child.name);
        }
    }
}

// WE DON'T NEED TO CHANGE ANYTHING IN MAIN

Summary

Below is a table that summaries the SOLID principles

Single Responsibility Principle
  • A class should have only one reason to change
  • Separation of concerns, different classes handling different independent tasks/problems
Open-Closed Principle
  • Classes should be open for extension but closed for modification
Liskov Substitution Principle
  • You should be able to substitute a base type for a subtype
Iterface Segregation Principle
  • Don't put too much into an Interface, split into separate specific Interfaces
  • YAGNI - You Ain't Going To Need It
Dependency Inversion Principle
  • High-Level modules should not depend upon Low-Level Modules, use Abstractions instead