This is the sixth day of my participation in the November Gwen Challenge. Check out the event details: The last Gwen Challenge 2021

preface

Robert C. Martin proposes five object-oriented design principles, which he abbreviates as SOLID. Every letter of this acronym talks about principles in Java. When all SOLID principles are used in a combined way, it becomes easier to develop manageable software.

Robert C. Martin Robert C. Martin is a world-class programmer, design pattern and agile development pioneer, the first President of the Agile Alliance, and former editor of the C++ Report. He became a professional programmer in the early 1970s and later founded Object Mentor and served as president of the company. Younger programmers affectionately refer to them as “Uncle Bob.”

What does SOLID mean?

As mentioned above, SOLID represents the five principles of Java, which are:

  • S: The single responsibility principle
  • O: Open close principle
  • L: Richter’s substitution principle
  • I: interface isolation principle
  • D: Dependence inversion principle

Hei will discuss each principle in depth in this installment. Let’s start with the first principle, the single responsibility principle.

Single Responsibility Principle (SRP)

The single responsibility principle states that there can only be one reason to modify a class. If there are more than one reason to modify a class, then the class should be refactored into multiple classes based on functionality.

To make this easier to understand, use the following code. Suppose you now have a Customer class.

import java.util.List;
 
public class Customer {
    String name;
    int age;
    long bill;
    List<Item> listsOfItems;
    Customer(String name,int age){
        this.name=name;
        this.age=age;
    }
    // It should not be Customer's responsibility to calculate the bill
    public long calculateBill(long tax){
        for (Item item:listsOfItems) {
            bill+=item.getPrice();
        }
        bill+=tax;
        this.setBill(bill);
        return bill;
    }
    // Generating the report should not be Customer's responsibility either
    public void generateReport(String reportType) {
        if(reportType.equalsIgnoreCase("CSV")){
            System.out.println("Generate CSV report");
        }
        if(reportType.equalsIgnoreCase("XML")){
            System.out.println("Generate XML report"); }}/ / omitted getters and setters
}
 
Copy the code

The above example has the following problems:

  • If there is any change in the calculation logic of the bill, we need to change the Customer class;
  • If you want to add another report type to generate, we need to change the Customer class;

In principle, billing calculation and report generation should not be the responsibility of the Customer class and should be split into multiple classes.

Create a separate class for billing calculations.

import java.util.List;
 
public class BillCalculator {
    public long calculateBill(Customer customer,long tax){
        long bill=0;
        List listsOfItems=customer.getListsOfItems();
        for (Item item:listsOfItems) {
            bill+=item.getPrice();
        }
        bill+=tax;
        customer.setBill(bill);
        returnbill; }}Copy the code

Create a separate class for report generation.

public class ReportGenerator {
    public void generateReport(Customer customer,String reportType) {
        if(reportType.equalsIgnoreCase("CSV")) {
            System.out.println("Generate CSV report");
        }
        if(reportType.equalsIgnoreCase("XML")) {
            System.out.println("Generate XML report"); }}}Copy the code

If we need to change anything in the billing calculation, we do not need to change the Customer class; we will do so in the BillCalculator class.

If you want to add another report type, you need to make the change in the ReportGenerator class instead of the Customer class.

The open closed principle

Entities or objects should remain open for extension, but closed for modification.

Once a class has been written and tested, it should not be modified over and over again, but should be open to extension. If we modify a class that has already been tested, it may cause a lot of extra work to test it, and new bugs may be introduced.

The policy design pattern is an implementation of the open – close principle. Service classes can use different policies to perform specific tasks based on requirements, so we will keep the service classes closed, but at the same time, the system is open to extension by introducing new policies to implement policy interfaces. At run time, you can use any new policy as needed.

Let’s look at a simple code example to understand this principle.

Suppose we need to create a report file in two formats, such as CSV and XML, depending on the type of input. Be aware that new formats may be added later at design time.

First we define the file format by enumeration.

public enum ReportingType {
    CSV,XML;
}
Copy the code

Then create a service class to generate the file.

public class ReportingService {
    public void generateReportBasedOnType(ReportingType reportingType) {
        if("CSV".equalsIgnoreCase(reportingType.toString())) {
            generateCSVReport();
        } else if("XML".equalsIgnoreCase(reportingType.toString())) { generateXMLReport(); }}private void generateCSVReport(a) {
        System.out.println("Generate CSV Report");
    }
    private void generateXMLReport(a){
        System.out.println("Generate XML Report"); }}Copy the code

The code is logically simple, meets our requirements, and has been tested.

Now we need to add a new report file format, such as EXCEL, so our code will first modify the enumeration class:

public enum ReportingType {
    CSV,XML,EXCEL;
}
Copy the code

Next we will modify the service class that has been tested.

public class ReportingService {
    public void generateReportBasedOnType(ReportingType reportingType) {
        if("CSV".equalsIgnoreCase(reportingType.toString())) {
            generateCSVReport();
        } else if("XML".equalsIgnoreCase(reportingType.toString())) {
            generateXMLReport();
        } else if("Excel".equalsIgnoreCase(reportingType.toString())) {
			// Add excel generation logicgenerateExcelReport(); }}private void generateCSVReport(a) {
        System.out.println("Generate CSV Report");
    }
    private void generateXMLReport(a){
        System.out.println("Generate XML Report");
    }
    // Add a method to generate Excel
    private void generateExcelReport(a) {
        System.out.println("Generate Excel Report"); }}Copy the code

Because we made changes to the code that had already been tested, we had to retest everything to avoid bugs. This is obviously too inelegant, and the main reason is that it doesn’t fit the open/close principle.

Next we re-complete our service class using the open close principle.

public class ReportingService {
    public void generateReportBasedOnStrategy(ReportingStrategy reportingStrategy)
    {
        // Generate a report file through a policyreportingStrategy.generateReport(); }}Copy the code

Create an interface called ReportingStrategy that represents the policies for report file generation.

public interface ReportingStrategy {
     void generateReport(a);
}
Copy the code

We then create the respective policy implementations born into CSV and XML.

public class CSVReportingStrategy implements ReportingStrategy {
    @Override
    public void generateReport(a) {
        System.out.println("Generate CSV Report"); }}public class XMLReportingStrategy implements ReportingStrategy {
 
    @Override
    public void generateReport(a) {
        System.out.println("Generate XML Report"); }}Copy the code

In this way, we only need to create different policies as needed when we use them.

public class GenerateReportMain {
 
    public static void main(String[] args) {
        ReportingService rs=new ReportingService();
        // Generate a CSV file
        ReportingStrategy csvReportingStrategy=new CSVReportingStrategy();
        rs.generateReportBasedOnStrategy(csvReportingStrategy);
 
        // Generate an XML file
        ReportingStrategy xmlReportingStrategy=newXMLReportingStrategy(); rs.generateReportBasedOnStrategy(xmlReportingStrategy); }}Copy the code

When you need to add an Excel type, you only need to add an Excel policy.

public class ExcelReportingStrategy implements ReportingStrategy {
    @Override
    public void generateReport(a) {
        System.out.println("Generate Excel Report"); }}Copy the code

No changes have been made to ReportingService. We just added the new ExcelReportingStrategy class, which only tests Excel related logic without retesting the service class and other file format code.

Here’s another example to help you clarify the closure principle. You’ve probably installed extensions in your Chrome browser.

Chrome’s main function is to browse different websites. When you’re browsing a foreign language website in Chrome and you want to translate, just install a translation plugin.

This mechanism for adding content to add browser functionality is an extension. Thus, the browser is a perfect example of being open for extension but closed for modification.

Richter’s substitution principle

Richter’s substitution principle defines that every subclass or derived class should be able to replace its parent or base class.

It is a unique object-oriented principle. A child type of a particular parent type should be able to replace the parent type without causing any complexity or disruption.

Interface Isolation Principle

Simply put, the interface isolation principle states that a client should not be forced to implement methods it does not use.

Can you don’t support way to throw an UnsupportedOperationException, but it is not recommended to do this, it will make your class is very difficult to use.

Also to understand this principle, let’s use the following code example to better understand it.

Suppose we now have an interface Set.

public interface Set<E> {
   boolean add(E e);
   boolean contains(Object o);
   E ceiling(E e);
   E floor(E e);
}
Copy the code

Create a TreeSet to implement the Set interface.

public class TreeSet implements Set{
    @Override
    public boolean add(Object e) {
        // Implement this method
        return false;
    }
    @Override
    public boolean contains(Object o) {
        // Implement this method
        return false;
    }
    @Override
    public Object ceiling(Object e) {
        // Implement this method
        return null;
    }
    @Override
    public Object floor(Object e) {
        // Implement this method
        return null; }}Copy the code

Let’s create a HashSet.

public class HashSet implements Set{
    @Override
    public boolean add(Object e) {
        return false;
    }
    @Override
    public boolean contains(Object o) {
        return false;
    }
    @Override
    public Object ceiling(Object e) {
        return null;
    }
    @Override
    public Object floor(Object e) {
        return null; }}Copy the code

You see that the ceiling() and floor() methods are implemented even if they are not required in a hashSet.

This problem can be implemented as follows:

Create another interface called NavigableSet that will have ceiling() and floor() methods.

public interface NavigableSet<E> {
   E ceiling(E e);
   E floor(E e);
}
Copy the code

Modify the Set interface.

public interface Set<E> {
   boolean add(E e);
   boolean contains(Object o);  
}
Copy the code

TreeSet can now implement two interfaces, Set and NavigableSet.

public class TreeSet implements Set.NaviagableSet{
    @Override
    public boolean add(Object e) {
        // Implement this method
        return false;
    }
    @Override
    public boolean contains(Object o) {
        // Implement this method
        return false;
    }
    @Override
    public Object ceiling(Object e) {
        // Implement this method
        return null;
    }
    @Override
    public Object floor(Object e) {
        // Implement this method
        return null; }}Copy the code

A HashSet only needs to implement the Set interface.

public class HashSet implements Set{
    @Override
    public boolean add(Object e) {
        return false;
    }
    @Override
    public boolean contains(Object o) {
        return false; }}Copy the code

This is the interface isolation principle in Java. If you look at the JDK source code, you’ll see that the Set interface relies on the architecture in this way.

Dependency inversion principle

The dependency inversion principle defines that entities should rely only on the abstract rather than the concrete. A high-level module cannot depend on any low-level module, but should depend on abstraction.

Let’s look at another practical example to better understand this principle.

When you go to the mall to buy things, you decide to use your bank card payment, when you put the card to the cashier, the cashier will not focus on what you use is which bank card, even if you gave a piece of card of rural credit cooperatives, he will not give special credit card slot, even you are using a debit card or credit card is not important, can be successful credit card payment.

In this example, the cashier’s card reader relies on the abstraction of the card and does not care about the details of the card, which is the dependency inversion principle.

conclusion

You should now know the basic definitions for all five components of SOLID: the single responsibility principle, the open closed principle, the Richter substitution principle, the interface isolation principle, and the dependency inversion principle.

So when you write code, you should keep these core principles in mind and use them as guidelines to make your code more efficient and elegant.

If you lock help, a thumbs-up is the biggest encouragement for me!