Object Oriented Software Design - Solid Principles - with examples


Image


Software design is typically complex. Object oriented design takes it to the next level. There are a number of design patterns to be aware of. Can we make things simple?

SOLID Principles is a great starting point for Object Oriented Design.

What will you learn?

  • What are SOLID Principles?
  • What are the best practices in designing objected oriented software?

Free Courses - Learn in 10 Steps

Software Design Principles

This is the third article in a series of articles on important Software Design Principles:

What Are The SOLID Principles?

Plainly listed out, the SOLID principles are:

  • SRP: Single Responsibility Principle
  • OCP: Open Closed Principle
  • LSP: Liskov Substitution Principle
  • ISP: Interface Segregation Principle
  • DIP: Dependency Inversion Principle

Let’s look at each of these by turn.

Single Responsibility Principle (SRP)

“A class should have one, and only one reason to change”

This principle is not just applicable to a class, but also at the level of a package, a component, an application, or a service. Each of these should each have just one responsibility.

You should be able to state what each element does in a single sentence.

When your class or component or application has single responsibility, the options for reuse increases significantly.

Have a look at the following class:


	public class Task {
		public void downloadFile(String location) {
			//...
		}
		
		public void parseTheFile(File file) {
			//...
		}

		public void persistTheData(Data data) {
			//...
		}
	}

Task has three main responsibilities:

  • Download a file from a location
  • Parse the contents of the file
  • Save the data obtained to a database

It clearly does not satisfy SRP. Let’s look at another example:


	public class Employee {
		public Money calculatePay() {
			//...
		}

		public String reportHours() {
			//...
		}

		public void save() {
			//...
		}
	}

Employee has multiple responsibilities as well:

  • Calculate the employee’s pay
  • Create a report of hours of work put in
  • Save the employee details to a database

Above examples need re-design to meet Single Responsibility Principle.

Open Closed Principle (OCP)

“Software entities should be open for extension, but closed for modification”

Let’s take a quick look at the calculateArea method in the Shape class.

class Shape {
	public double calculateArea(Shape[] shapes) {
		double area = 0;
		for(Shape shape:shapes) {
			if(shape instanceof Rectangle) {
				//Calculate Area of Rectange
			}
			else if(shape instanceof Circle) {
				//Calculate Area of Circle
			}
		}
		return area;
	}
}

class Rectangle extends Shape {
	
}

class Circle extends Shape {
	
	
}

Is there a problem with calculateArea() method?

What if we add a new shape? What if we remove a shape? What if we want to change the area algorithm for one of the shapes.

For all these modifications, calculateArea() method needs to change.

How can we make it better?

Parts of the above code below pseudo code, for ease of explanation


abstract class Shape {

	
    abstract double area();
}

class Rectangle extends Shape {

	@Override
	double area() {
		// Area implementation for Rectangle
		return 0;
	}
	
}

class Circle extends Shape {

	@Override
	double area() {
		// Area implementation for Rectangle
		return 0;
	}
	
}

A better solution would be to allow each of the shapes, to define their own area() method. We have created an abstract class called Shape (which could also have been an interface), and have each of the different shapes extend it. Each shape also overrides Shape’s abstract area() method, to compute its specific area.

The standalone calculateArea() method would now look like this:

abstract class Shape {
	
	
	public double calculateArea(Shape[] shapes)
	{
		double area = 0;
		for(Shape shape:shapes) {
			area += shape.area();
		}
		return area;
	}
	
    abstract double area();
}

calculateArea() is now responsible just for looping around the shapes, and invoking the area() method of individual shapes.

This is a very good example of the OCP.

If you now want to add another shape, then you need to extend the Shape class, and override its area() method:. That’s it.

Here, Shape class is open to extension, and calculateArea() is closed to modification.


Image Image Image Image


Liskov Substitution Principle (LSP)

According to this principle:

“Subtypes must be substitutable for their base types”

One of the things people try to do with object oriented programming, is to use inheritance even when it is not appropriate. They may do it just for the sake of reusing the code. Have a look at this example:


	class Rectangle {
		void setWidth(double w)
		void setHeight(double h)

		double getWidth()
		double getHeight()
	}

	class Square extends Rectangle {
		void setWidth(double w) 	// Set both width and height to w
		void setHeight(double h) 	// Set both height and width to h

		double getWidth()
		double getHeight()
	}

We have a Rectangle class that has methods to set and get its width and height. Suppose we have a fresh requirement for a Square class.

Instead of implementing it as a separate class, Let’s say we extend Rectangle (as we know from school geometry that a square is a rectangle). Square redefines all the set and get methods that it inherits from Rectangle to set both height and width to the same values.

Note that the pair of setters and the pair of getters inside Square are redundant.

Consider a piece of client code such as this:


	void test(Rectangle r) {
		r.setWidth(5);
		r.setHeight(4);

		assertEquals(5*4, r.getWidth() * r.getHeight());
	}

test() is a method to test rectangles. A Square is a Rectangle, hence test() can be called on a Square object as well. Unfortunately, since both setWidth() and setHeight() modify both the width and height to be equal, the expression r.getWidth() * r.getHeight() always amounts to 4 * 4, as that was the last value set. test() always fails for a Square!

LSP states that inheritance should be used very carefully. Make sure that the super class is always fully replaceable by each of its sub classes.

Interface Segregation Principle (ISP)

If two systems are communicating to each other, then the dependency should be in the smallest possible interface.

Clients should not be forced to implement interfaces that they don’t use

Instead of one fat interfaces, a group of smaller interfaces is preferred, each one addressing a sub-module.

Let’s look at an example:

	
	public abstract class Animal {
		public abstract void feed();
	}

	public class Dog extends Animal {
		public void feed() {
			//...
		}
	}

	public class Tiger extends Animal {
		public void feed() {
			//...
		}
	}

Let’s say we now need to add a feature called groom() to each kind of Animal.

One option is to add it to Animal as an abstract method:

	
	public abstract class Animal {
		public abstract void feed();
		public abstract void groom();
	}

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

	public class Tiger extends Animal {
		public void feed() {
			//...
		}

		// DUMMY implementation, to keep compiler happy
		public void groom() {
			//...
		}
	}

In that case, both Dog and Tiger need to provide implementations for groom().

Now, groom() makes sense for a Dog, but not so much for a Tiger.

However, we are forced to provide a dummy implementation in Tiger to make the code compile.

Above example does not adhere to ISP.

Let’s see how to fix this:

	
	public abstract class Animal {
		public abstract void feed();
	}

	public class Pet extends Animal {
		public abstract void groom();
		public void feed() {
			//...
		}
	}

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

	public class Tiger extends Animal {
		public void feed() {
			//...
		}
	}

The new interface Pet extends the existing Animal, and also adds its own abstract method groom(). Now, Dog will extend Pet, as it needs both feed() and groom(), whereas Tiger chooses to extend just Animal for feed().

ISP recommends that you keep your interfaces as small as possible.

The moment you have a fat interface, any changes to that interface will result in changes in all your implementations.

Dependency Inversion Principle (DIP)

The Dependency Inversion Principle states that:

“Depend upon abstract classes (interfaces), not upon concrete classes”

Let’s look at an example of what this means:


abstract class OutputDevice {
	void copy(String device) {
		Keyboard keyboard = new Keyboard();
		int character;
		while ((character = keyboard.read()) != -1) {
			if (device.equals("Printer")) {
				writeToPrinter(character);
			} else {
				writeToDevice(character);
			}
		}
	}

	private void writeToDevice(int character) {
		// TODO Auto-generated method stub
		
	}

	private void writeToPrinter(int c) {
		// TODO Auto-generated method stub
		
	}
}


What does the copy() method do?

It reads a character from the keyboard, and then decides where this character needs to go. If it’s a printer, write to the printer. Else, send it to the disk.

The problem here is that as the number of OutputDevice types increase, the logic of copy() needs to change every single time.

Let’s look at an alternate implementation:


	public interface Reader {
		public char read();
	}

	public interface Writer {
		public void write(char ch);
	}

	void copy(Reader r, Writer w) {
		int c;

		while((c = r.read()) != EOF) {
			w.write(c);
		}
	}

copy() method explicitly states its dependencies - Reader and Writer interfaces.

It inverts responsibility, and leaves the creation of Reader and Writer to its users.

copy() will not change even when there are new Writer or Reader implementations.

This is called Dependency Inversion principle.

Frameworks like Spring make it easy to implement Dependency Inversion.

Do check out our video on this:

image info

Summary

In this article, we looked at the five SOLID principles: SRP, OCP, LSP, ISP and DIP. We also looked at a few code examples to understand more about each one of them.

Best Selling Udemy Courses

Image
Image Image Image Image Image Image Image Image Image

Join 450,000 Learners and 30+ Amazing Courses

350,000 Learners are learning everyday with our Best Selling Courses : Spring Boot Microservices, Spring, Spring Boot, Web Services, Hibernate, Full Stack React, Full Stack Angular, Python, Spring Interview Guide, Java Interview, Java Functional Programming, AWS, Docker, Kubernetes, PCF, AWS Fargate and Azure


Do not know where to start your learning journey? Check out our amazing learning paths:
Learning Path 01 - Spring and Spring Boot Web Applications and API Developer,
Learning Path 02 - Full Stack Developer with Spring Boot, React & Angular,
Learning Path 03 - Cloud Microservices Developer with Docker and Kubernetes,
Learning Path 04 - Learn Cloud with Spring Boot, AWS, Azure and PCF and
Learning Path 05 - Learn AWS with Microservices, Docker and Kubernetes


Subscribe

FREE COURSES



Related Posts

Writing Integration Tests for Rest Services with Spring Boot

Setting up a basic REST Service with Spring Boot is a cake walk. We will go one step further and add great integration tests!

Integrating Spring Boot and Spring JDBC with H2 and Starter JDBC

Learn using Spring Boot Starter JDBC to connect Spring Boot to H2 (in memory database) using Spring JDBC. You will create a simple project with Spring Boot. You will add code to the project to connect to a database using Spring JDBC. You will learn to implement the basic CRUD methods.

JUnit Tutorial for Beginners in 5 Steps

JUnit Tutorial for Beginners in 5 Steps. Setting up a basic JUnit example and understanding the basics of junit.

JPA and Hibernate Tutorial For Beginners - 10 Steps with Spring Boot and H2

JPA and Hibernate in 10 Steps with H2 - Setting up a basic project example with Spring Boot and in memory database H2. Its a cake walk.

Spring Boot Tutorial For Beginners in 10 Steps

Introduction to Spring Boot in 10 Steps. Learn the basics of Spring Boot setting up a basic project example with Spring Boot.

Spring Framework Tutorial for Beginners - Your First 10 Steps

Learn the basics of Spring Framework setting up a very simple example.

JPA and Hibernate Tutorial using Spring Boot Data JPA

Complete journey starting from JDBC to JPA to Spring Data JPA using an example with Spring Boot Data JPA starter project. We use Hibernate as the JPA Implementation.

Creating a Web Application with Spring Boot with JSP

Setting up a basic web application with Spring Boot is a cake walk. We will create a simple web application using Spring Initializr and add JSP features to it.

What is Spring Boot Auto Configuration?

Auto Configuration is the most important feature in Spring Boot. In this tutorial, we will learn important concepts about Auto Configuration with a couple of examples.

Unit Testing Rest Services with Spring Boot and JUnit

Setting up a Basic REST Service with Spring Boot is a cake walk. We will go one step further and add great unit tests to our RESTful Service.


Search