User Brian
Brian
Level 41

SOLID: Five basic principles of class design in Java

Published in the Java Developer group
Classes are the building blocks of applications. Just like bricks in a building. Poorly written classes can eventually cause problems. SOLID: Five basic principles of class design in Java - 1To understand whether a class is properly written, you can check how it measures up to "quality standards". In Java, these are the so-called SOLID principles, and we're going to talk about them.

SOLID principles in Java

SOLID is an acronym formed from the capital letters of the first five principles of OOP and class design. The principles were expressed by Robert Martin in the early 2000s, and then the abbreviation was introduced later by Michael Feathers. Here are the SOLID principles:
  1. Single Responsibility Principle
  2. Open Closed Principle
  3. Liskov Substitution Principle
  4. Interface Segregation Principle
  5. Dependency Inversion Principle

Single Responsibility Principle (SRP)

This principle states that there should never be more than one reason to change a class. Each object has one responsibility, which is fully encapsulated in the class. All of a class's services are aimed at supporting this responsibility. Such classes will always be easy to modify if necessary, because it is clear what the class is and is not responsible for. In other words, we will be able to make changes and not be afraid of the consequences, i.e. the impact on other objects. Additionally, such code is much easier to test, because your tests are covering one piece of functionality in isolation from all others. Imagine a module that processes orders. If an order is correctly formed, this module saves it in a database and sends an email to confirm the order:

public class OrderProcessor {

    public void process(Order order){
        if (order.isValid() && save(order)) {
            sendConfirmationEmail(order);
        }
    }

    private boolean save(Order order) {
        MySqlConnection connection = new MySqlConnection("database.url");
        // Save the order in the database

        return true;
    }

    private void sendConfirmationEmail(Order order) {
        String name = order.getCustomerName();
        String email = order.getCustomerEmail();

        // Send an email to the customer
    }
}
This module could change for three reasons. First, the logic for processing orders may change. Second, the way orders are saved (database type) may change. Third, the way confirmation is sent may change (for example, suppose we need to send a text message rather than an email). The single responsibility principle implies that the three aspects of this problem are actually three different responsibilities. That means they should be in different classes or modules. Combining several entities that can change at different times and for different reasons is considered a poor design decision. It is much better to split a module into three separate modules, each of which performs a single function:

public class MySQLOrderRepository {
    public boolean save(Order order) {
        MySqlConnection connection = new MySqlConnection("database.url");
        // Save the order in the database

        return true;
    }
}

public class ConfirmationEmailSender {
    public void sendConfirmationEmail(Order order) {
        String name = order.getCustomerName();
        String email = order.getCustomerEmail();

        // Send an email to the customer
    }
}

public class OrderProcessor {
    public void process(Order order){

        MySQLOrderRepository repository = new MySQLOrderRepository();
        ConfirmationEmailSender mailSender = new ConfirmationEmailSender();

        if (order.isValid() && repository.save(order)) {
            mailSender.sendConfirmationEmail(order);
        }
    }

}

Open Closed Principle (OCP)

This principle is described as follows: software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification. This means that it should be possible to change a class's external behavior without making changes to class's existing code. According to this principle, classes are designed so that tweaking a class to fit specific conditions simply requires extending it and overriding some functions. This means that the system must be flexible, able to work in changing conditions without changing the source code. Continuing our example involving order processing, suppose we need to perform some actions before an order is processed as well as after the confirmation email is sent. Instead of changing the OrderProcessor class itself, we will extend it to accomplish our objective without violating the Open Closed Principle:

public class OrderProcessorWithPreAndPostProcessing extends OrderProcessor {

    @Override
    public void process(Order order) {
        beforeProcessing();
        super.process(order);
        afterProcessing();
    }
    
    private void beforeProcessing() {
        // Take some action before processing the order
    }
    
    private void afterProcessing() {
        // Take some action after processing the order
    }
}

Liskov Substitution Principle (LSP)

This is a variation of the open closed principle that we mentioned earlier. It can be defined as follows: objects can be replaced by objects of subclasses without changing a program's properties. This means that a class created by extending a base class must override its methods so that the functionality is not compromised from the client's point of view. That is, if a developer extends your class and uses it in an application, he or she should not change the expected behavior of any overridden methods. Subclasses must override the methods of the base class so that the functionality is not broken from the point of view of the client. We can explore this in detail in the following example. Suppose we have a class that is responsible for validating an order and checking whether all of the goods in the order are in stock. This class has an isValid() method that returns true or false:

public class OrderStockValidator {

    public boolean isValid(Order order) {
        for (Item item : order.getItems()) {
            if (!item.isInStock()) {
                return false;
            }
        }

        return true;
    }
}
Suppose also that some orders need to be validated differently that others, e.g. for some orders we need to check whether all the goods in the order are in stock and whether all of the goods are packed. To do this, we extend the OrderStockValidator class by creating the OrderStockAndPackValidator class:

public class OrderStockAndPackValidator extends OrderStockValidator {

    @Override
    public boolean isValid(Order order) {
        for (Item item : order.getItems()) {
            if ( !item.isInStock() || !item.isPacked() ){
                throw new IllegalStateException(
                     String.format("Order %d is not valid!", order.getId())
                );
            }
        }

        return true;
    }
}
But here we have violated the Liskov substitution principle, because instead of returning false if the order fails validation, our method throws an IllegalStateException. Clients using this code don't expect this: they expect a return value of either true or false. This can lead to runtime errors.

Interface Segregation Principle (ISP)

This is principle is characterized by the following statement: client should not be forced to implement methods that they will not use. The interface segregation principle means that interfaces that are too "thick" must be divided into smaller, more specific ones, so that clients using small interfaces know only about the methods they need for their work. As a result, when an interface method changes, any clients that don't use that method should not change. Consider this example: Alex, a developer, has created a "report" interface and added two methods: generateExcel() and generatedPdf(). Now a client wants to use this interface, but only intends to use reports in PDF format, not in Excel. Will this functionality satisfy this client? No. The client will have to implement two methods, one of which is largely not needed and exists only thanks to Alex, the one who designed the software. The client will use either a different interface or do nothing with the method for Excel reports. So what's the solution? It is to split the existing interface into two smaller ones. One for PDF reports, the other for Excel reports. This lets clients use only the functionality they need.

Dependency Inversion Principle (DIP)

In Java, this SOLID principle is described as follows: dependencies within the system are built based on abstractions. Higher-level modules do not depend on lower-level modules. Abstractions should not depend on details. Details should depend on abstractions. Software needs to be designed so that the various modules are self-contained and connected to each other through abstraction. A classic application of this principle is the Spring Framework. In the Spring Framework, all modules are implemented as separate components that can work together. They are so autonomous that they can be used just as easily in program modules other than the Spring Framework. This is achieved thanks to the dependence of the closed and open principles. All the modules provide access only to the abstraction, which can be used in another module. Let's try to illustrate this using an example. Speaking of the single responsibility principle, we considered the OrderProcessor class. Let's take another look at the code of this class:

public class OrderProcessor {
    public void process(Order order){

        MySQLOrderRepository repository = new MySQLOrderRepository();
        ConfirmationEmailSender mailSender = new ConfirmationEmailSender();

        if (order.isValid() && repository.save(order)) {
            mailSender.sendConfirmationEmail(order);
        }
    }

}
In this example, our OrderProcessor class depends on two specific classes: MySQLOrderRepository and ConfirmationEmailSender. We'll also present the code of these classes:

public class MySQLOrderRepository {
    public boolean save(Order order) {
        MySqlConnection connection = new MySqlConnection("database.url");
        // Save the order in the database

        return true;
    }
}

public class ConfirmationEmailSender {
    public void sendConfirmationEmail(Order order) {
        String name = order.getCustomerName();
        String email = order.getCustomerEmail();

        // Send an email to the customer
    }
}
These classes are far from what we would call abstractions. And from the point of view of the dependency inversion principle, it would be better to start by creating some abstractions that we can work with in the future, rather than specific implementations. Let's create two interfaces: MailSender and OrderRepository). These will be our abstractions:

public interface MailSender {
    void sendConfirmationEmail(Order order);
}

public interface OrderRepository {
    boolean save(Order order);
}
Now we implement these interfaces in classes that have already been prepared for this:

public class ConfirmationEmailSender implements MailSender {

    @Override
    public void sendConfirmationEmail(Order order) {
        String name = order.getCustomerName();
        String email = order.getCustomerEmail();

        // Send an email to the customer
    }

}

public class MySQLOrderRepository implements OrderRepository {

    @Override
    public boolean save(Order order) {
        MySqlConnection connection = new MySqlConnection("database.url");
        // Save the order in the database

        return true;
    }
}
We did the preparatory work so that our OrderProcessor class depends, not on concrete details, but on abstractions. We'll change it by adding our dependencies to the class constructor:

public class OrderProcessor {

    private MailSender mailSender;
    private OrderRepository repository;

    public OrderProcessor(MailSender mailSender, OrderRepository repository) {
        this.mailSender = mailSender;
        this.repository = repository;
    }

    public void process(Order order){
        if (order.isValid() && repository.save(order)) {
            mailSender.sendConfirmationEmail(order);
        }
    }
}
Now our class depends on abstractions, not specific implementations. We can easily change its behavior by adding the desired dependency at the time an OrderProcessor object is created. We've examined the SOLID design principles in Java. You'll learn more about OOP in general and the basics of Java programming — nothing boring and hundreds of hours of practice — in the CodeGym course. Time to solve a few tasks :)
Comments (2)
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION
Tevin McDowell Level 4, Columbus, United States
25 November 2021
I think I'm having trouble understanding the difference between a class and a module. Would it be accurate to say they mean the same thing and are being used interchangeably in this article?
Enoma Uwaifo Level 8, Nigeria, Nigeria
18 June 2020
This was really overwhelming. I really got a grasp of this whole SOLID Process. It's so wonder seeing how classes can be seperated in an orderly manner. So challenging and demanding but doable. The thing is bringing all these together in a real application is not going to be an easy task. Well, like they say here at codegym repetition is the mother of all learning.