SOLID design principles
SOLID is a mnemonic acronym for five design principles.
- S - Single responsibility principle
- O - Open closed principle
- L - Liscov substitution principle
- I - Interface segregation principle
- D - Dependency inversion principle
Single responsibility principle
It is pretty simple, according to this principle a class should have a single responsibility. It means a class should not have unrelated responsibility. So, what is responsibility ? It is the methods that the class have. And every method has some purpose. So, by single responsibility principle, these purpose should be related.
To make all this concrete, let me take an example of a Todo class which has two fields a list of entries
and a
static int count
. And this Todo class has method to add the entries and remove the entries.
import java.util.ArrayList;
import java.util.List;
public class Todo {
List<String> entries = new ArrayList<>();
private static int count;
void addEntry(String text) {
entries.add("" + (++count) + ": " + text);
}
void removeEntry(int index) {
entries.remove(index);
}
@Override
public String toString() {
return String.join(System.lineSeparator(), entries);
}
}
At this point the responsibility of Todo
class is only to add the entries and remove the entries which are
highly related and at this point it is following the single responsibility which is managing the todos.
Now, if i add two more method which is save
and load
- whose purpose is to save the entries to file and
load them from file. Our Todo
class now gets responsible for managing the persistence also hence breaking the
single responsibility principle. Here is the example -
import java.util.ArrayList;
import java.util.List;
public class Todo {
List<String> entries = new ArrayList<>();
private static int count;
void addEntry(String text) {
entries.add("" + (++count) + ": " + text);
}
void removeEntry(int index) {
entries.remove(index);
}
// it breaks the single responsibility principle
void save(String fileName) {
// saving logic
}
// it breaks the single responsibility principle
void load(String fileName) {
// loading logic
}
@Override
public String toString() {
return String.join(System.lineSeparator(), entries);
}
}
Open closed principle
This principle states that entities should be open for extension but closed for modification. The gist of that statement is that whenever some changes are required to be done classes should be extended instead of modified. Now what is this extension and modification. Can you explain it with example? Yes.
Let us assume a scenario where are building a filtering feature of a product on e-commerce site. So, lets first define
the product
class and its features say color
and size
.
public enum Color {
RED, GREEN, BLUE
}
public enum Size {
SMALL, MEDIUM, LARGE, HUGE
}
public class Product {
String name;
Color color;
Size size;
public Product(String name, Color color, Size size) {
this.name = name;
this.color = color;
this.size = size;
}
}
Now remember the requirement that we have to implement the filter and this time we only create a filter that receives the list of product and returns stream of filtered product. Now we only want to create a filter that filter by color.
import java.util.List;
import java.util.stream.Stream;
public class ProductFilter {
public Stream<Product> filterByColor(List<Product> products,
Color color) {
return products.stream().filter(p -> p.color == color);
}
}
Let's assume a requirement to implement new filter(s) comes that need to filter by size and combination of both color and size. So, our Product filter will look like this.
import java.util.List;
import java.util.stream.Stream;
public class ProductFilter {
public Stream<Product> filterByColor(List<Product> products,
Color color) {
return products.stream().filter(p -> p.color == color);
}
public Stream<Product> filterBySize(List<Product> products,
Size size) {
return products.stream().filter(p -> p.size == size);
}
public Stream<Product> filterBySizeAndColor(List<Product> products,
Size size,
Color color) {
return products.stream().filter(p -> p.size == size
&& p.color == color);
}
}
As we can see, every time a requirement comes we are modifying our existing, already tested class and that breaks the open closed principle. So, what can we do to make our example follow open closed principle.
We can use the Specification pattern to solve our problem. For that we require a very general Specification interface that has a single method isSatisfied. This isSatisfied method returns the boolean and is general purpose and can be applied to anything. We require another interface that is Filter. It has a single method filter that returns streams of item. It receives the list of items and specification instance.
public interface Specification<T> {
boolean isSatisfied(T item);
}
public interface Filter<T> {
Stream<T> filter(List<T> items, Specification<T> spec);
}
Now the next step to create our color and size specifications. Something like this.
public class ColorSpecification implements Specification<Product> {
private final Color color;
public ColorSpecification(Color color) {
this.color = color;
}
@Override
public boolean isSatisfied(Product item) {
return item.color == color;
}
}
public class SizeSpecification implements Specification<Product> {
private final Size size;
public SizeSpecification(Size size) {
this.size = size;
}
@Override
public boolean isSatisfied(Product item) {
return item.size == size;
}
}
Let's build our better filter now. Its implementation is simple.
public class BetterFilter implements Filter<Product> {
@Override
public Stream<Product> filter(List<Product> items,
Specification<Product> spec) {
return items.stream().filter(spec::isSatisfied);
}
}
To use this from main method here is the demo code
import java.util.List;
public class Demo {
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.HUGE);
List<Product> products = List.of(apple, tree, house);
System.out.println("Green Products:");
BetterFilter betterFilter = new BetterFilter();
betterFilter.filter(products,
new ColorSpecification(Color.GREEN))
.forEach(p -> System.out.println(" - "
+ p.name
+ " is green"));
}
}
Now you might be wondering and I can combine these specifications, for that we need something called combinator and its implementations is simple.
public class AndSpecification<T> implements Specification<T> {
private final Specification<T> first, second;
public AndSpecification(Specification<T> first, Specification<T> second) {
this.first = first;
this.second = second;
}
@Override
public boolean isSatisfied(T item) {
return first.isSatisfied(item) && second.isSatisfied(item);
}
}
Now our demo for AndSpecification
.
import java.util.List;
public class Demo {
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.HUGE);
List<Product> products = List.of(apple, tree, house);
betterFilter.filter(products, new AndSpecification<>(
new ColorSpecification(Color.BLUE),
new SizeSpecification(Size.HUGE)
)).forEach(p -> System.out.println(" - "
+ p.name
+ " is blue and huge"));
}
}
As we can see from our new approach, now if a requirement comes. We only need to create a new specification. And we are using interface inheritance to achieve our goal.
Liscov substitution principle
Liscov substitution principle states that instance of base class can be replaceable by instances of its subclass without breaking the application.
So, first i write a program that violates the liscov substitution principle. For that we need two classes one base class
and one derived class. Let's assume base class is Rectangle
that has two fields width and height and it has a method
area
. And let's call our derived class Square.
public class Rectangle {
protected int width;
protected int 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 area() {
return width * height;
}
@Override
public String toString() {
return "Rectangle{" +
"width=" + width +
", height=" + height +
'}';
}
}
public class Square extends Rectangle {
public Square() {
}
public Square(int size) {
width = height = size;
}
@Override
public void setHeight(int height) {
super.setHeight(height);
super.setWidth(height);
}
@Override
public void setWidth(int width) {
super.setWidth(width);
super.setHeight(width);
}
}
We can see the setters of Square
class are bad. For example name of method is setHeight, but it set width as well.
So, client has no way of knowing this side effect without actually looking at code.
Now, let me write a demo class with static method that violates liscov substitution principle.
public class Demo {
static void useIt(Rectangle r) {
int width = r.getWidth();
r.setHeight(10);
System.out.println(
"Expect area of " + (width * 10) +
", got " + r.area()
);
}
public static void main(String[] args) {
Rectangle rc = new Rectangle(2, 3);
useIt(rc);
Rectangle sq = new Square();
sq.setHeight(5);
useIt(sq);
}
}
output:
Expect area of 20, got 20
Expect area of 50, got 100
To improve our example, we actually not need Square
class in first place. we can have a method in Rectangle
class
that returns is particular instance of rectangle is square. Or we can write a Factory class, something like this.
public 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
Interface segregation principle states that client should not be forced to implement code that they do not use. So, what does that mean, in simple terms it means our interface should have minimum amount of code. And it recommends to break the interface in two or more parts so that clients using the interface should implement that they only require.
Let me take an example of interface and its client that breaks the interface segregation principle.
First let us take an example Machine
interface that has three methods.
public class Document {}
public interface Machine {
void print(Document d);
void fax(Document d);
void scan(Document d);
}
Now consider its clients. One of the client is MultiFunctionPrinter
that implements all three of the methods.
public class MultiFunctionPrinter implements Machine {
@Override
public void print(Document d) {
// some implementation
}
@Override
public void fax(Document d) {
// some implementation
}
@Override
public void scan(Document d) {
// some implementation
}
}
In this case, MultiFunctionPrinter
implements all three methods and everything is fine till now. But what about
OldFashionedPrinter
that need to implement only print method.
public class OldFashionedPrinter implements Machine {
@Override
public void print(Document d) {
// some implementation
}
@Override
public void fax(Document d) {
// not required - confuse clients.
// or throw some exception
}
@Override
public void scan(Document d) {
// not required - confuse clients
// or throw some exception
}
}
So, how can we solve this problem. Well, it is simple just create different interface for different methods. Something like this.
public interface Printer {
void print(Document d);
}
public interface Scanner {
void scan(Document d);
}
And when a Photocopier
require both print and scan. It can implement both of these methods.
public class Photocopier implements Printer, Scanner {
@Override
public void print(Document d) {
// implementation
}
@Override
public void scan(Document d) {
// implementation
}
}
There is another way to combine these separate interfaces to common interface.
public interface MultiFunctionDevice extends Printer, Scanner {
}
Dependency inversion principle
Dependency inversion is defined in two parts.
- High level modules should not depend on low level modules. Both should depend on abstraction.
- Abstraction should not depend on detail. Details should depend on abstraction.
It is also to be noted that, dependency inversion is not same as dependency injection. First, what is abstraction ? It is an abstract class or an interface. Second what is high-level modules and low level modules. To get the idea, we can consider low level modules as utility and high level modules as complex business logic.
Suppose we are creating a family tree kind of structure, with entities like Person, enum Relation, Relationship class that store list of relations that contain tuple of person, relation and person. Following diagram may help.
First, start with lowest level by creating an enum Relationship. (Look below code is an enum and not a class Relationships)
public enum Relationship {
PARENT,
CHILD,
SIBLING
}
Second, let's create a Person
class, that has a single field name.
public class Person {
String name;
public Person(String name) {
this.name = name;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
'}';
}
}
Now, we are implementing our low level module Relationships
. Here we are using a Triplet class of an external library
javatuples
.
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import org.javatuples.Triplet;
public class Relationships {
private List<Triplet<Person, Relationship, Person>> relations =
new ArrayList<>();
public List<Triplet<Person, Relationship, Person>> getRelations() {
return relations;
}
public void addParentAndChild(Person parent, Person child) {
relations.add(new Triplet<>(parent, Relationship.PARENT, child));
relations.add(new Triplet<>(child, Relationship.CHILD, parent));
}
}
See Relationships
class concerns with relations
data structure and there modification only. So, here in our example it
act as a low level module. Now we implement a high level module called Research
.
import org.javatuples.Triplet;
public class Research {
public Research (Relationships relationships) {
List<Triplet<Person, Relationship, Person>> relations =
relationships.getRelations();
relations.stream()
.filter(x -> x.getValue0().name.equals("John")
&& x.getValue1() == Relationship.PARENT)
.forEach(ch -> System.out.println(
"John has a child called " + ch.getValue2().name
));
}
}
As we can see, Relationships
has been passed as an argument to Research's
constructor, which are exposing
the private data structure relations
of Relationships
class though getter. According to dependency inversion
principle high level module should not depend on low level module but here our example is violating this principle.
Now what is the solution of this problem. Well we need to introduce an abstraction as pointed out by definition of
dependency inversion principle. I am introducing an interface called RelationshipBrowser
.
public interface RelationshipBrowser {
List<Person> findAllChildrenOf(String name);
}
A new method - findAllChildrenOf
is required to be implemented in Relationships
class. And it contains the logic of finding the
children of a Person
public class Relationships implements RelationshipBrowser {
private List<Triplet<Person, Relationship, Person>> relations =
new ArrayList<>();
public List<Triplet<Person, Relationship, Person>> getRelations() {
return relations;
}
public void addParentAndChild(Person parent, Person child) {
relations.add(new Triplet<>(parent, Relationship.PARENT, child));
relations.add(new Triplet<>(child, Relationship.CHILD, parent));
}
@Override
public List<Person> findAllChildrenOf(String name) {
return relations.stream()
.filter(x -> x.getValue0().name.equals(name)
&& x.getValue1() == Relationship.PARENT)
.map(Triplet::getValue2)
.collect(Collectors.toList());
}
}
Our Research
class will now look like it.
import java.util.List;
public class Research {
public Research(RelationshipBrowser browser) {
List<Person> childrenOfJohn = browser.findAllChildrenOf("John");
for (Person child:childrenOfJohn) {
System.out.println("John has a child called " + child.name);
}
}
}
And at last here is our demo.
public class Demo {
public static void main(String[] args) {
Person john = new Person("John");
Person child1 = new Person("Chris");
Person child2 = new Person("Matt");
Relationships relationships = new Relationships();
relationships.addParentAndChild(john, child1);
relationships.addParentAndChild(john, child2);
new Research(relationships);
}
}