Skip to content

Design Principles

To make the creation of good code easier, several principles for software development have been formulated. These can be seen as a kind of checklist that helps the developer's daily work to reflect on his own work or to avoid errors and critical constructs from the outset.

Prominent representatives of such principles are the SOLID principles. SOLID was coined by Robert C. Martin. It is an acronym and stands for:

  • Single responsibility principle
  • Open-closed principle
  • Liskov's principle of substitution
  • Interface segregation principle
  • Dependency inversion principle

Following these principles results in better code and makes software more maintainable. In this article you will see the SOLID principles and a few othe ones. These principles belong to the most important design principles in the object oriented paradigm.

Single responsibility principle

The single responsibility principle states that a class should only have one responsibility. Changes to the functionality should only affect a few classes. The more code that needs to be changed, the higher the risk of errors.

Failure to adhere to this principle leads to many dependencies and high levels of networking. It's like in real life: Above a certain size, a universal tool becomes unwieldy.

How can you tell if the class is doing more than one task? - The class can only have one reason to change. This means that if two requirements change, only one of them may have an effect on the class. If the class has multiple reasons for change, it is doing too many tasks.

So it is better to have many small classes than a few large ones. This does not make the code larger. It's just organized differently. Analogy from the handicraft basement: When all the screws are in a box, it is difficult to find the right one. If they are sorted into several boxes, the search is much faster. It is the same with the classes.

Open-closed principle

According to the open-closed principle, a class should be open to extensions, but closed to modifications. The behavior of a class may be expanded but not changed. This principle helps to avoid errors in already finished code parts. If an extension can only be achieved through changes within a class, the risk that the changes will result in completely implemented functions receiving new errors is very high.

The open-closed principle can usually be achieved in two ways: inheritance and the use of interfaces. By adhering to this principle, new functions can be added to an application without changing existing classes.

Simply put, classes should be open for extension, but closed for modification. In doing so, we stop ourselves from modifying existing code and causing potential new bugs in an otherwise happy application.

Liskov's principle of substitution

Liskov's principle of substitution requires that derived classes always have to be usable instead of their base class. Subtypes must behave like their base type. That sounds obvious, but is it? The compiler knows that a derived class is also of the type of the base class - so it can always be converted into this. Is that enough?

Liskov's principle of substitution goes further than the compiler.

Interface segregation principle

The interface segregation principle states that a client must not be dependent on the functions of a server that it does not need. This means that an interface can only contain functions that really belong together. The problem is that "fat" interfaces create couplings between the otherwise independent clients.

If one aspect of the interface is changed, this affects all clients - even if they do not use this aspect.

Dependency inversion principle

The dependency inversion principle states that classes on a higher abstraction level should not be dependent on classes on a lower abstraction level. But it's not about simply reversing the dependencies. There should no longer be any dependencies between classes; there should only be dependencies on interfaces (both sides).

Don't Repeat Yourself (DRY) Principle

The principle of Don't Repeat Yourself (DRY) is a popular concept across programming paradigms but it is particularly relevant in OOP. Under the principle:

Every piece of information or logic within a system needs to have a single, unambiguous representation.

This means the use of abstract classes , interfaces, and public constants when it comes to OOPs. Any time there is a common functionality across classes, either abstracting them into a common parent class or using interfaces to combine their functionality may make sense.

public class Animal {
    public void eatFood() {
        System.out.println("Eating food...");
    }
}
public class Cat extends Animal {
    public void meow() {
        System.out.println("Meow! *purrs*");
    }
}
public class Dog extends Animal {
    public void woof() {
        System.out.println("Woof! *wags tail*");
    }
}
Both a Cat and a Dog need to eat food, but they speak differently. Since eating food is a common functionality for them, we can abstract it into a parent class such as Animal and then have them extend the class.

Keep It Simple and Stupid (KISS) Principle

Keep It Simple and Stupid (KISS) is a reminder for people to keep the code simple and readable. If multiple use cases are addressed by your system divide them into smaller functions.

The essence of this theory is that the output of your software will not be significantly affected by another stack call unless efficiency is extremely crucial for most cases. In reality, some compilers or runtime environments can also simplify an inline execution call to the process.

On the other hand, unreadable and long methods are going to be very difficult for human programmers to manage, bugs are going to be harder to find and you might find yourself violating DRY as well because if a feature does two things, you can't call it to do just one of them, so you're going to do another method.

All in all, if you find yourself caught up in your own code and confused of what each component is doing, it's time to reassess.

It's almost likely you will tweak the style to make it more readable. And if you have trouble as the one who planned it when it's all all new in your mind, think about how someone who will see it in the future will work for the first time.

The Composition Over Inheritance Principle

When designing the structures one should always choose composition over inheritance. In Java, that means we should define and implement interfaces more often than not, rather than defining and extending classes.

We have already discussed that the Car is a Vehicle as a general guiding principle used by people to decide whether or not groups can inherit each other.

Although the Liskov Substitution Principle is difficult to think about and appear to follow, this way of thinking is extremely problematic when it comes to reusing and repurposing code later in production.

If your class is going to implement all of the functionalities and your child class can be used as a substitute for your parent class, use inheritance. If you class is going to implement some specific functionalities, use composition.

Exercise 1: apply Single Responsibility Principle:

public class VehicleServiceResource {
  public List getVehicles(){...}  
  public double calculateTotalValue(){}
}

Exercise 2: apply Open-Closed Principle:

public class VehicleValueCalculator {
  // lets assume a simple method to calculate the total value of a vehicle
  // with extra cost depending the type.
  public double calculateVehicle(Vehicle v){
    double value = 0;
    if(v instanceof Car){
      value = v.getValue() + 2.0;
    } else if(v instanceof MotorBike) {
      value = v.getValue() + 0.4;
    } 
    return value;
  }
}

Exercise 3: apply Liskovsches Substitution Principle:

public class Bird{
  public void fly(){}
}
public class Eagle extends Bird{}
public class Ostrich extends Bird{}
Exercise 4: apply Interface Segregation Principle:
public interface Connection {
  void open();
  void close();
  byte[] receive();
  void send(byte[] data);  
}

Exercise 5: apply Dependency Inversion Principle:

public class Emailer{
  private SpellChecker spellChecker;    
  public Emailer(SpellChecker sc) {
    this.spellChecker = sc;  
  }
  public void checkEmail() {
    this.spellChecker.check();
  }
}
public class SpellChecker {
  public void check() throws SpellFormatException {
  }  
}