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

Five Basic Class Design Principles (SOLID) in Java

Published in the Random EN group
Classes are the building blocks of an application. Just like bricks in a building. Poorly written classes can bring problems one day. Five Basic Class Design Principles (SOLID) in Java - 1To understand if 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 coined by Robert Martin in the early 2000s, and the abbreviation was later coined by Michael Feathers. Here is what is included in the SOLID principles:
  1. Single Responsibility Principle.
  2. Open Closed Principle (Principle of openness / closeness).
  3. Liskov's Substitution Principle (Barbara 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, fully encapsulated in a class. All services of the class are aimed at providing this duty. 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. Also, such code is much easier to test, because you cover one functionality with tests in isolation from all the others. Imagine a module that processes orders. If the order is correctly generated, it saves it to 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 can change for three reasons. Firstly, the order processing logic may become different, secondly, the way it is saved (database type), and thirdly, the way to send a confirmation letter (say, you need to send SMS instead of email). The Single Responsibility Principle implies that the three aspects of this problem are actually three different responsibilities. So, they must be in different classes or modules. Combining multiple entities that can 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 modules, 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: program 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 designed so that in order to tailor a class to specific conditions of use, it is enough to extend it and redefine some functions. Therefore, the system must be flexible, with the ability to work in variable conditions without changing the source code. Continuing with our order example, let's say we need to perform some action before processing the order and after sending the confirmation email. Instead of changing the class itselfOrderProcessor, we will expand it and achieve a solution to the problem 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 on the open/closed principle discussed earlier. It can be described as follows: objects in the 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, they should not change the expected behavior of the overridden methods. Subclasses should override base class methods in a way that doesn't break functionality from the client's point of view. This can be seen in detail in the following example. Suppose we have a class that is responsible for order validation and checks if all of the order items are in stock. This class has a methodisValidwhich 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: to check if all the items in the order are in stock and if all the items 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 was not validated, our method throws an exception IllegalStateException. Clients of this code don't expect this: they expect true or false to be returned . This can lead to errors in the program.

Interface Separation Principle (ISP)

Characterized by the following statement: Clients should not be forced to implement methods they will not use . The principle of separation of interfaces says that too "thick" interfaces must be divided into smaller and more specific ones, so that clients of small interfaces know only about the methods that are necessary in their work. As a result, when changing an interface method, clients that do not use this method should not change. Consider an example. The developer Alex created a "report" interface and added two methods: generateExcel()andgeneratedPdf(). Client A now wants to use this interface, but wants to use reports in PDF format only, not in Excel. Will this functionality suit him? 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 in dividing the existing interface into two smaller ones. One is a PDF report and the other is an Excel report. This will give the user the opportunity to use only the functionality they need.

Dependency Inversion Principle (DIP)

This principle of SOLID 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 should depend on abstractions. Software needs to be designed so that the various modules are stand-alone 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 just as easily be used in other programming 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 of the Single Responsibility Principle,OrderProcessor. Let's take another look at the code for 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 concrete classes MySQLOrderRepositoryand ConfirmationEmailSender. Here is also 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 first create some abstractions that will allow us to operate with them in the future, and not 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 we 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 some preparatory work so that our class OrderProcessordoes not depend on specific details, but on abstractions. Let's make changes to it by injecting 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);
        }
    }
}
Now our class depends on abstractions, not concrete implementations. You can easily change its behavior by injecting the desired dependency at instantiation 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 CodeGym course. It's time to do a few things :) Five Basic Class Design Principles (SOLID) in Java - 2
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION