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) } } |
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")); } } |
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 |
Below is a table that summaries the SOLID principles
Single Responsibility Principle |
|
Open-Closed Principle |
|
Liskov Substitution Principle |
|
Iterface Segregation Principle |
|
Dependency Inversion Principle |
|