JavaRush /Java Blog /Random EN /Five Basic Principles of Class Design (SOLID) in Java
Ve4niY
Level 14

Five Basic Principles of Class Design (SOLID) in Java

Published in the Random EN group
Classes are the blocks from which an application is built. Just like the bricks in a building. Poorly written classes can cause problems one day. Five Basic Principles of Class Design (SOLID) in Java - 1To understand whether a class is written correctly, you can check the “quality standards”. In Java, these are the so-called SOLID principles. Let's talk about them.

SOLID Principles in Java

SOLID is an acronym formed from the capital letters of the first five principles of OOP and design. The principles were invented by Robert Martin in the early 2000s, and the acronym was later coined by Michael Feathers. Here's what the SOLID principles include:
  1. Single Responsibility Principle.
  2. Open Closed Principle.
  3. Liskov's 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, completely encapsulated in a class. All class services are aimed at ensuring this responsibility. Such classes will always be easy to change if necessary, because it is clear what the class is responsible for and what it is not. That is, it will be possible to make changes and not be afraid of the consequences - the impact on other objects. And such code is much easier to test, because you cover one functionality with tests in isolation from all others. Imagine a module that processes orders. If the order is correctly formed, it saves it in the 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 to the database

        return true;
    }

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

        // Sending a letter to the client
    }
}
Such a module may change for three reasons. Firstly, the order processing logic may be different, secondly, the method of saving it (database type), thirdly, the method of sending a confirmation letter (for example, instead of email you need to send SMS). The Single Responsibility Principle implies that the three aspects of this problem are actually three different responsibilities. This means they must be in different classes or modules. Combining multiple entities that may change at different times and for different reasons is considered a bad design decision. It is much better to divide the module into three separate ones, each of which will perform one single function:
public class MySQLOrderRepository {
    public boolean save(Order order) {
        MySqlConnection connection = new MySqlConnection("database.url");
        // save the order to the database

        return true;
    }
}

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

        // Sending a letter to the client
    }
}

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 succinctly described as follows: software entities (classes, modules, functions, etc.) must be open for extension, but closed for change . This means that it should be possible to change the external behavior of a class without making physical changes to the class itself. Following this principle, classes are developed so that to adjust the class to specific application conditions, it is enough to extend it and redefine some functions. Therefore, the system must be flexible, able to work under variable conditions without changing the source code. Continuing with our order example, let's say we need to perform some actions before the order is processed and after the confirmation email is sent. Instead of changing the class itself OrderProcessor, we will extend it and achieve a solution to the problem at hand without violating the OCP principle:
public class OrderProcessorWithPreAndPostProcessing extends OrderProcessor {

    @Override
    public void process(Order order) {
        beforeProcessing();
        super.process(order);
        afterProcessing();
    }

    private void beforeProcessing() {
        // Perform some actions before processing the order
    }

    private void afterProcessing() {
        // Perform some actions after order processing
    }
}

Barbara Liskov Substitution Principle (LSP)

This is a variation of the open/closed principle discussed earlier. It can be described as follows: objects in a program can be replaced by their heirs without changing the properties of the program. This means that a class developed by extending a base class must override its methods in a way that does not break functionality from the client's point of view. That is, if a developer extends your class and uses it in an application, he should not change the expected behavior of the overridden methods. Subclasses must override base class methods in a way that does not break functionality from the client's point of view. This can be examined in detail using the following example. Let's assume we have a class that is responsible for order validation and checks whether all of the order items are in stock. This class has a method isValidthat returns true or false :
public class OrderStockValidator {

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

        return true;
    }
}
Let's also assume that some orders need to be validated differently: check whether all goods in the order are in stock and whether all goods are packaged. To do this, we extended the class OrderStockValidatorwith the class OrderStockAndPackValidator:
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;
    }
}
However, in this class we violated the LSP principle, because instead of returning false if the order did not pass validation, our method throws an exception IllegalStateException. Clients of this code don't expect this: they expect true or false to be returned . This may lead to errors in the program.

Interface Split Principle (ISP)

Characterized by the following statement: Clients should not be forced to implement methods that they will not use . The principle of interface separation suggests that interfaces that are too “thick” need to be divided into smaller and more specific ones, so that clients of small interfaces know only about the methods necessary for their work. As a result, when changing an interface method, clients that do not use this method should not change. Let's look at an example. Developer Alex created the "report" interface and added two methods: generateExcel()and generatedPdf(). Now Client A wants to use this interface, but he only intends to use PDF reports and not Excel. Will he be satisfied with this functionality? No. He will have to implement two methods, one of which is largely unnecessary and exists only thanks to Alex, the software designer. The client will either use a different interface or leave the Excel field blank. So what's the solution? It consists of dividing the existing interface into two smaller ones. One is a report in PDF format, the second is a report in Excel format. This will give the user the opportunity to use only the functionality necessary for him.

Dependency Inversion Principle (DIP)

This SOLID principle in Java is described as follows: dependencies within the system are built on the basis of abstractions . Top-level modules are independent of lower-level modules. Abstractions should not depend on details. Details must depend on abstractions. Software needs to be designed so that the various modules are autonomous and connect to each other using abstraction. A classic application of this principle is the Spring framework. Within the Spring framework, all modules are implemented as separate components that can work together. They are so self-contained that they can be used just as easily in other software modules besides the Spring framework. This is achieved through the dependence of closed and open principles. All modules provide access only to an abstraction that can be used in another module. Let's try to demonstrate this with an example. Speaking about the principle of sole responsibility, we considered some OrderProcessor. 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, ours OrderProcessordepends on two specific classes MySQLOrderRepositoryand ConfirmationEmailSender. We also present the code for these classes:
public class MySQLOrderRepository {
    public boolean save(Order order) {
        MySqlConnection connection = new MySqlConnection("database.url");
        // save the order to the database

        return true;
    }
}

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

        // Sending a letter to the client
    }
}
These classes are far from being called abstractions. And from the point of view of the DIP principle, it would be more correct to start by creating some abstractions that will allow us to operate with them in the future, rather than with specific implementations. Let's create two interfaces MailSenderand OrderRepository, which will become our abstractions:
public interface MailSender {
    void sendConfirmationEmail(Order order);
}

public interface OrderRepository {
    boolean save(Order order);
}
Now let’s implement these interfaces in classes that are already ready for this:
public class ConfirmationEmailSender implements MailSender {

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

        // Sending a letter to the client
    }

}

public class MySQLOrderRepository implements OrderRepository {

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

        return true;
    }
}
We have done the preparatory work so that our class OrderProcessordepends not on concrete details, but on abstractions. Let's make changes to it by introducing our dependencies in 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);
        }
    }
}
Our class now depends on abstractions rather than concrete implementations. You can easily change its behavior by injecting the desired dependency at the time the instance is created OrderProcessor. We looked at SOLID - design principles in Java. More about OOP in general, the basics of this programming language - not boring and with hundreds of hours of practice - in the JavaRush course. Time to solve some problems :) Five Basic Principles of Class Design (SOLID) in Java - 2
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION