Object Oriented Design Principles Everythone Should Know


16 May 2019  Michal Fabjanski  15 mins read.

During my study, I have collected quite a large and interesting list of object-oriented programming design principles that may be useful to you. This post is a shortened note. I invite you to explore knowledge reading books such as “Clean Architecture”, Martin Robert C. Ready? I invite you to read the 10 most important OOP principles.

Object-Oriented Design Principles represent a set of rules that are the essence of object-oriented programming and help us/others to prepare well, easy to understand design.

SOLID

Solid is a set of five rules which tell us how to arrange our methods and data structures in classes. There are three main goals to understand and use SOLID:

Code and functionality changes are easier

Code is easy to understand

Software structures are the basis of components that can be used in many software systems

There are five SOLID principles (each letter - one principle):

Single Responsibility Principle

Single Responsibility Principle tells us that each software entity (classes, modules, methods) is designed to assume only one responsibility. If a code change is to be made, we should need to make a changes in one class that processes it. The good example is the Employee class from a company application:

public class Employee {

    private String firstName;
    private String lastName;
    private BigDecimal salaryInUSD;
    private BigDecimal salaryInEuro;

    public Double getSalaryInPLN() {
        return salaryInUSD * Exchange.getDollarToZlotyRate();
    }

    public Double getSalaryInEuro() {
        return salaryInEuro * Exchange.getDollarToEuroRate();
    }

Why Employee class have to know something about currency exchange? By putting these two methods into a single Employee class, the developers have coupled Employee class with class responsible for exchange rates. How to do it to comply with the Single Responsibility Principle? We should remove getSalary.. methods from Employee class and create new class (e.g ExchangeService, SalaryCalculator, SalaryConverter…) which has knowledge of exchange rates. Next, we should provide a salary to ExchangeService class. ExchangeService takes the salary, calculate salary in another currency and return it.

Open/closed principle

Open/closed principle tell us that sofware entities should be open for extension, but closed for modification. Developer’s goal is to extend a class/module functionality without modyfying its source code. Let’s imagine creating a Square class. In accordance with the Single Responsibility Principle, you create a Square class and AreaCalculator which contain the method calculateArea(). See the following example:

public class Rectangle {
    private double length;
}
public class AreaCalculator {
    
    public double calculateArea(Shape shape) {
        return shape.getLength() * shape.getLength();
    }
}

Unfortunately, after some time we get a new task - we have to add two new figures: a triangle and a trapeze. The only solution is to modify the calculateArea() method:

public class AreaCalculator {

    public double calculateArea(Shape shape) {
        if (shape instanceof Square) {
            return shape.getLength() * shape.getLength();
        } else if (shape instanceof Triangle) {
            return (shape.getLength() * shape.getHeight()) / 2;
        } else if (shape instanceof Trapeze) {
            return (...);
        } else {
            return new RuntimeException("Cannot calculate area for provided shape");
        }
    }
}

This code starts to look like Arrow Anti-Pattern

How to do it in accordance with O/C principle? You should use Shape interface:

public interface Shape {
    double getArea();
}

And implementation:

public class Suqare implements Shape {
    private double length;

    @Override
    public double getArea() {
        return (length * length);
    }
}

Thanks to it AreaCalculator does not have to know all kinds of shapes. It relying on Shape abstraction:

public class AreaCalculator {

    public double calculateArea(Shape shape) {
       return shape.getArea();
    }
}

We’ve just made AreaCalculator closed for modification. If we get the task of creating a new shape - we will not have to modify this class. However, we can extend it.

Liskov Substitution Principle

Liskov Substitution Principle is a rule about the contract of the clasess: if a base class satisfies a contract, then by the LSP derived classes must also satisfy that contract. The main objectives of this principle are:

Classes in the application should be swapped by their subclasses without affecting the correctness of the program, i.e. the inheriting class must be a good equivalent of the base class.

A subclass should not do less than the base class. So it should always do more.

Good example is the“Square extends Rectangle” class:

public class Rectangle {

    private int height;
    private int width;

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

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

    public int getWidth() {
        return width;
    }

    public int getHeight() {
        return height;
    }
}

Mathematically, a square is a rectangle. Most people would misinterpret “is a” relation and model the relationship between the rectangle and a square with inheritance:

public class Square extends Rectangle {

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

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

As you probably guess, you can not have two different dimensions for a square. It is possible to bypass this by:

public class Square extends Rectangle {

    @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 overrided setHeight and setWidth methods to set both dimensions to the same value. What do you think of this fix? This design breaks LSP. A client can works with instances of Rectangle, but breaks when instances of Square are passed to it:

double countArea(Rectangle rec) {
    rec.setWidth(10);
    rec.setHeight(5);
//It will be a fail for the square:
    assertThat(rec.area() == 50);
}

How to do it correctly? The most critical aspect to inheritance is that we should model inheritance based on behaviours, not object properties. The easiest way to understand this is by way of example:

interface Shape {
    public double area();
}

public class Square implements Shape {

    private double size;

    public void setSize(double size) {
        this.size = size;
    }

    @Override
    public double area() {
        return size * size;
    }
}

public class Rectangle implements Shape {

    private double height;
    private double width;

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

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

    @Override
    public double area() {
        return height * width;
    }
}

Now clients of Shape cannot make any behavior changes via setter methods. When clients have to change properties of shapes, they have to do it in concrete classes.

Interface Segregation Principle

The main assumption of the ISP:

No client should be forced to depend on methods it does not use.

in other words:

Many client-specific interfaces are better than one general purpose interface.

So interfaces that we create should not contain methods that we do not need. The class that implements the interface can not be forced to implement methods that it does not need, and this is often the case with large interface. Let us understand the interface segregation principle by below example:

public interface GenerateTimeSheet{
  public void generateExcel();
  public void generateCSV();
}

We have one interface with two methods to generate time sheet report (e.g for Employee). Consider a case client TimeSheet wants to use this interface but want to use only Excel time sheets. The interface forces client to implement an unnecessary method generateCSV();. A better solution would be breaking the GenerateTimeSheet interface into two small ones which contains separate methods.

Dependency Inversion Principle

The general idea of this principle is as simple: High-level modules, which provide complex logic, should be easily reusable and unaffected by changes in low-level modules. Robert C. Martin’s definition of the Dependency Inversion Principle consists of two goals:

High-level modules should not depend on low-level modules. Both should depend on abstractions.

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

In practice DIP tells that a method requires an interface object instead of specific class. This way we can pass many different versions of our entity into the same method.

public class EventService {

    private DBRepository repository = new DBRepository();

    public void addEvent(Event event) {
        repository.saveEvent(event);
    }

    public void removeEvent(String event) {
        repository.deleteEvent(event);
    }
}

EventService class uses concrete DBRepository class which save or delete events from the database. In above example EventService is High-level module. DBRepository is a low-level module. We have a direct dependence between classes here. In this way, we violate the DIP policy. How to do it correctly? To solve the above problem, we should make the EventService class not dependent on the DBRepository class. In addition, both classes must depend on abstraction. Let’s create an abstraction - Repository interface. It will have methods for writing and reading tasks:

  public interface Repository {
 
     void saveEvent(Event Event);
 
     void deleteEvent(Event event);
 }
 

Let’s change the EventService class to use the Repository interface and thus depend on abstraction:

 public class EventService {

    private Repository repository;

    public EventService(Repository repository) {
        this.repository = repository;
    }

    public void addEvent(Event event) {
        repository.saveEvent(event);
    }

    public void removeEvent(String event) {
        repository.deleteEvent(event);
    }
}
 

Now we have to implement our DBRepository class (it will also depend on abstraction):

public class DBRepository implements Repository {

    @Override
    public void saveEvent(Event event) {
    }

    @Override
    public void deleteEvent(Event event) {
    }
}
 

Relationships have been inverted. Now the “high level module” does not depend on the “low level module”. The lower layer module depends on the abstract interface from the upper layer (Repository interface). Changes in the module at the lower level do not affect the module at a higher level. If, for example, we need to save events in the file instead of in a database it is simple. It is enough to add the appropriate class at a lower level (FileRepository).

DRY - Don’t Repeat Yourself

We should write a code that is reusable, not repeat the logic contained in one place in the application. If you are close to the copy/paste code, think about creating an abstraction (loop, common interface, function, class, some design pattern, eg Strategy, etc.) that you will be able to repeatedly use.

KISS - Keep it simple, stupid!

Simplicity (and avoiding complexity) should be a priority during programming. The code should be easy to read and understand, requiring as little effort as possible. Each method should only solve one small problem, not many use cases. If you have a lot of conditions in the method, break these out into smaller methods. This is not only about the way the code is created and written, but also about the names of our classes, methods, variables and objects. Everything should be written in such a way that the name of the variable, object, method or class tells what its purpose or use is.

YAGNI - You Aint’t gona need it

This is a nice principle that says that in our program we should put the most important functionalities that we will need at a given moment. We should not write code that will not be useful at the moment, which will be redundant and which will only grow unnecessarily in our program.

ADP - Acyclic dependencies principle

This principle says that there should be no cycles in the dependency structure. An exemplary structure breaking this principle will be when: packet A has a dependency in packet B, which has a relationship in packet C, which in turn has a relationship in packet A:

Acyclic-dependencies-principle

We can prevent this by using the Dependency inversion principle, design patterns (eg Observer) or create a new package and put all the common dependencies there.

LoD - Law of Demeter

The Law of Demeter is often described this way:

“Only talk to your immediate friends.”

This principle says that a class’s method can only refer to:

  • methods of the same class
  • fields (and their methods) of the same class
  • parameters (and their methods) passed to this method
  • objects (and their methods) that this method will create

Advantages:

  • reduction of dependence
  • the code calling the given method does not have to know the structure of other objects
  • changes in other objects do not require changing the method