Contents

Solid Design Principles

SOLID is a set design principles in object-oriented context to make maintainable, testable and extendable software.

Single Responsibility

A class should only have one responsibility. Furthermore, it should only have one reason to change.

Benefits:

  • Testing: A class with one responsibility will have far fewer test cases.
  • Lower coupling: Less functionality in a single class will have fewer dependencies.
  • Organization: Smaller, well-organized classes are easier to search than monolithic ones.

Example

Bad

A class representing a book:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public class Book {

    private String name;
    private String author;
    private String text;

    //constructor, getters and setters

		void printTextToConsole() {
        // implementation
    }
}

Why bad?

This code violates the single responsibility principle because it now has multiple responsibilities - to store book details and print text to the console. To fix this, create another class for printing text.

Good

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public class BookPrinter {

    void printTextToConsole(String text){
        // implementation
    }

    void printText(String text, PrintStream printStream){
        // implementation
    }
}

Open - Closed

A class should be open for extension but closed for modification. When adding new functionality to classes, it should be open for extension but closed for modification. This prevents modifying existing code and causing potential new bugs in an otherwise working code.

Example

Bad

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public class Example {
	public double Area(Object[] Shapes) {
		double area = 0;
		for (Object shape : Shapes) {
			if (shape.name == "rectangle") {
				Rectangle rectangle = (Rectangle) shape;
				area += rectangle.width*rectangle.height;
			} else {
				Circle circle = (Circle) shape;
				area += circle.radius*circle.radius*Math.PI;
			}
		return area;
		}
	}
}

Why bad?

If a few more shapes have to be added, the code in the area method will have to be modified. To fix this and extend, not modify code, an area method can be added to each shape and used here.

Good

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
abstract class Shape {
	public abstract double getArea();
}

class Rectangle extends Shape {
	private double width, height;
	@Override public double getArea() {
		return width*height;
	}
}

class Circle extends Shape {
	private double radius;
	@Override public double getArea() {
		return radius*radius*Math.PI;
	}
}

public class Example {
	public double Area(Object[] Shapes) {
		double area = 0;
		for (Object shape : Shapes) {
			area += shape.getArea();
		}
		return area;
		}
	}
}

Liskov Substitution Principle

Child classes or subclasses must be substitutable for their parent classes or super classes. In other words, the child class must be able to replace the parent class.

Example

Bad

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class Rectangle {
	public void setWidth(double width) { // implementation }
	public void setHeight(double height) { // implementation }
	public void getWidth() { // implementation }
	public void getHeight() { // implementation }
}

class Square extends Rectangle {
	public void setWidth(double width) { // set both h and w to w }
	public void setHeight(double height) { // set both h and w to h }
	public void getWidth() { // implementation }
	public void getHeight() { // implementation }
}

Why bad?

Consider the following test code. If an object of a Square is supplied to the method below, the assertEquals will fail as the result will be 16, not 20. So, here the base class is not replaceable by its derived class.

1
2
3
4
5
void test(Rectangle r) {
	r.setWidth(5);
	r.setHeight(4);
	assertEquals(5*4, r.getWidth()*r.getHeight());
}

Good

Don’t inherit the Rectangle class in the Square class.

Interface Segregation Principle

Larger interfaces should be split into smaller ones. This ensures that implementing classes only needs to be concerned about the methods that are of interest to them. If there’s a fat interface, then any change in it would result in changes in all the implementing classes.

Example

Bad

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
abstract class Animal {
	abstract void feed();
	abstract void groom();
}

class Dog extends Animal { 
	void feed() { // implementation }
	void groom() { // implementation }
}

class Tiger extends Animal { 
	void feed() { // implementation }
	void groom() { 
		// dummy implementation as a tiger can't be pet
	}
}

Why bad?

A dummy implementation is provided to the Tiger class so that the compiler does not throw an error. A Tiger can’t be petted, so its implementation doesn’t actually do anything.

Good

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
abstract class Animal {
	abstract void feed();
}

abstract class Pet extends Animal {
	abstract void groom();
}

class Dog extends Pet { 
	void feed() { // implementation }
	void groom() { // implementation }
}

class Tiger extends Animal { 
	void feed() { // implementation }
}

Dependency Inversion

Depend upon abstractions (interfaces), not upon concrete classes or implementations. Because the abstraction does not depend on detail but the detail depends on abstraction, it decouples the code.

Bad

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
enum OutputDevice {PRINTER, DISK};

void copy(OutputDevice device) {
	int c = readKeyboard();
	while (c != EOF) {
		if (device == PRINTER) writePrinter(c);
		else writeDisk();
		c = readKeyboard();
	}
}

Why bad?

As the number of OutputDevice increases, the copy method will have to keep on changing. To fix this, create Reader and Writer interfaces. These will be dependencies of the copy method, copy method will not create these objects. rather this responsibility is inverted to the user.

Good

1
2
3
4
5
6
7
8
9
interface Reader { char read(); }
interface Writer { void write(char c); }
void copy(Reader r, Writer w) {
	char c = r.read();
	while (c != EOF) {
		w.write(c);
		c = r.read();
	}
}