Design Patterns - Seven Design Principles - Dimitt's Law and Richter's Replacement Principle

Keywords: Programming

Summary

Briefly introduce the seven design principles:

  1. Open-Close Principle: The core of all object-oriented design, open to extensions, close to modifications
  2. Dependency Inversion Principle: Programming for interfaces, dependent on abstraction rather than specificity
  3. Single Responsibility Principle: An interface is responsible for only one thing, and there can only be one reason for the class change
  4. Interface Isolation Principle: Use multiple specialized interfaces instead of one general interface
  5. Dimitt's rule (least-known principle): communicate only with friends (member variables, method input-output parameters), don't talk to strangers, and control access modifiers
  6. Richter Replacement Principle: Subclasses can extend the functionality of the parent class, but cannot change its original functionality
  7. Composite reuse principle: use object combinations (has-a)/aggregations (contanis-a) whenever possible, rather than inheritance relationships to achieve software reuse purposes

LoD

Definition

The Law of Demeter LoD refers to the Least Knowledge Principle (LKP), which means that an object should maintain a minimum understanding of other objects and minimize class-to-class coupling.

Dimitt's principle mainly emphasizes communicating only with friends and not with strangers.Classes appearing in member variables, input and output parameters of methods can be called member friend classes, while classes appearing inside methods do not belong to friend classes.

Example

Now to design a permission system, Boss needs to see how many courses are currently being published online.At this point, Boss finds TeamLeader to do statistics, and TeamLeader tells Boss the statistics.Next, let's look at the code:

Course class:

/**
 * @author eamon.zhang
 * @date 2019-09-26 9:17 a.m.
 */
public class Course {
}

TeamLeader class:

/**
 * @author eamon.zhang
 * @date 2019-09-26 9:17 a.m.
 */
public class TeamLeader {
    public void checkNumberOfCourses(List<Course> courseList) {
        System.out.println("The number of courses currently published is:"   courseList.size());
    }
}

Boss class:

/**
 * @author eamon.zhang
 * @date 2019-09-26 9:17 a.m.
 */
public class Boss {
    public void commandCheckNumber(TeamLeader teamLeader) {
        //Simulate Boss page by page, TeamLeader real-time statistics
        List<Course> courseList = new ArrayList<Course>();
        for (int i = 0; i < 20; i  ) {
            courseList.add(new Course());
        }
        teamLeader.checkNumberOfCourses(courseList);
    }
}

Test code:

public static void main(String[] args) {
    Boss boss = new Boss();
    TeamLeader teamLeader = new TeamLeader();
    boss.commandCheckNumber(teamLeader);
}

At this point, the functionality is already implemented and the code looks fine.According to Dimitt's principle, Boss only wants results and does not need to have direct communication with Course.TeamLeader statistics need to reference Course objects.Boss and Corse are not friends, as you can see from the class diagram below:

Here's how to transform the code:
TeamLeader class:

/**
 * @author eamon.zhang
 * @date 2019-09-26 9:17 a.m.
 */
public class TeamLeader {
    public void checkNumberOfCourses() {
        List<Course> courseList = new ArrayList<Course>();
        for (int i = 0; i < 20; i  ) {
            courseList.add(new Course());
        }
        System.out.println("The number of courses currently published is:"   courseList.size());
    }
}

Boss class:

/**
 * @author eamon.zhang
 * @date 2019-09-26 9:17 a.m.
 */
public class Boss {
    public void commandCheckNumber(TeamLeader teamLeader) {
        teamLeader.checkNumberOfCourses();
    }
}

Looking at the class diagram below, Course is no longer associated with Boss.

Learn the principles of software design and never form obsessive-compulsive disorder.When we encounter business-complex scenarios, we need to be contingent.

Richter Replacement Principle

Definition

The Liskov Substitution Principle (LSP) means that type T2 is a subtype of type T1 if, for each object O1 of type T1, there is object o2 of type T2, so that when all program P defined in T1 is replaced by o2 in all object o1, the behavior of program P does not change.

The definition looks abstract, so we can reconsider that if a software entity applies a parent class, it must apply to its subclasses. All references to the parent class must use its subclasses'objects transparently, and the subclasses' objects can replace the parent objects without changing the program logic.Based on this understanding, let's summarize:

Extended meaning: Subclasses can extend the functions of the parent class, but cannot change the original functions of the parent class.

  1. Subclasses implement abstract methods of the parent class, but cannot override non-abstract methods of the parent class.
  2. Subclasses can add their own unique methods.
  3. When the method of the subclass overloads the method of the parent class, the preconditions of the method (i.e., input/input parameters of the method) are looser than the input parameters of the parent method.
  4. When a subclass's method implements a parent class's method (overrides/overloads or implements an abstract method), the method's postconditions (that is, the method's output/return values) are stricter or equal than the parent class's.

Example

When we talked about the open-close principle earlier, we remember that rewriting the getPrice () method that overrides the parent class and adding a getOriginPrice() method to get the original price clearly violates the Richter replacement principle.Let's modify the code to not override the getPrice() method and add the getDiscountPrice() method:

/**
 * @author eamon.zhang
 * @date 2019-09-25 10:36 a.m.
 */
public class NovelDiscountBook extends NovelBook {
    public NovelDiscountBook(String name, int price, String author) {
        super(name, price, author);
    }

    public double getDiscountPrice(){
        return super.getPrice() * 0.85;
    }
}

The advantages of using the Richter replacement principle are as follows:

  1. Constraint inheritance overflows, a reflection of the open and close principle.
  2. Enhance the robustness of the program, while changing can also achieve very good compatibility, improve the maintenance and extensibility of the program.Reduce the risks introduced when demand changes.

Now describe a classic business scenario, illustrating the Richter Replacement principle with the relationship between a square, a rectangle, and a quadrilateral. We all know that a square is a special rectangle, so we can create a rectangular parent Rectangle class:

/**
 * @author eamon.zhang
 * @date 2019-09-26 9:59 a.m.
 */
public class Rectangle {
    private long height;
    private long width;

    public long getHeight() {
        return height;
    }

    public void setHeight(long height) {
        this.height = height;
    }

    public long getWidth() {
        return width;
    }

    public void setWidth(long width) {
        this.width = width;
    }
}

Create Square Square Class Inheritance Rectangle:

/**
 * @author eamon.zhang
 * @date 2019-09-26 10:01 a.m.
 */
public class Square extends Rectangle {
    private long length;

    public long getLength() {
        return length;
    }

    public void setLength(long length) {
        this.length = length;
    }

    @Override
    public long getHeight() {
        return super.getHeight();
    }

    @Override
    public void setHeight(long height) {
        super.setHeight(height);
    }

    @Override
    public long getWidth() {
        return super.getWidth();
    }

    @Override
    public void setWidth(long width) {
        super.setWidth(width);
    }
}

Create the resize() method in the test class, and according to the logical rectangle, the width should be greater than or equal to the height, we let the height increase all the time, knowing that the higher than the width becomes the square:

 public static void resize(Rectangle rectangle) {
    while (rectangle.getWidth() >= rectangle.getHeight()) {
        rectangle.setHeight(rectangle.getHeight()   1);
        System.out.println("width:"   rectangle.getWidth()   ",height:"   rectangle.getHeight());
    }
    System.out.println("resize Method End"  
            "\nwidth:"   rectangle.getWidth()   ",height:"   rectangle.getHeight());
}

Test code:

public static void main(String[] args) {
    Rectangle rectangle = new Rectangle();
    rectangle.setWidth(20);
    rectangle.setHeight(10);
    resize(rectangle);
}

Run result:

It is found that the aspect ratio is also large, which is a very normal situation in rectangles.Now let's look at the following code to modify the test code by replacing Rectangle rectangle with Square, its subclass:

public static void main(String[] args) {
    Square square = new Square();
    square.setLength(10);
    resize(square);
}

At this time, when we run, there is a dead loop, which violates the principle of Richter substitution. After replacing the parent class with the child class, the program does not run as expected.Therefore, there is some risk in our code design.The Richter replacement principle only exists between parent and child classes, and restriction inheritance is overflowing.Let's create an abstract quadrilateral Quadrangle interface based on the common square and rectangle:

/**
 * @author eamon.zhang
 * @date 2019-09-26 10:12 a.m.
 */
public interface Quadrangle {
    long getWidth();

    long getHeight();
}

Modify the Rectangle rectangle class:

/**
 * @author eamon.zhang
 * @date 2019-09-26 9:59 a.m.
 */
public class Rectangle implements Quadrangle {
    private long height;
    private long width;

    @Override
    public long getWidth() {
        return width;
    }

    public long getHeight() {
        return height;
    }

    public void setHeight(long height) {
        this.height = height;
    }

    public void setWidth(long width) {
        this.width = width;
    }
}

Modify Square class:

/**
 * @author eamon.zhang
 * @date 2019-09-26 10:01 a.m.
 */
public class Square implements Quadrangle {
    private long length;

    public long getLength() {
        return length;
    }

    public void setLength(long length) {
        this.length = length;
    }

    @Override
    public long getWidth() {
        return length;
    }

    @Override
    public long getHeight() {
        return length;
    }
}

At this point, if we change the parameters of the resize() method to the quadrilateral Quadrangle class, an error will occur inside the method.

Because Square no longer has setWidth() and setHeight() methods.Therefore, to constrain the overflow of inheritance, the method parameter for resize() can only use Rectangle rectangles.Of course, we will continue to explain this in depth in the next series of Design Patterns articles.

Posted by cronus on Wed, 25 Sep 2019 19:35:46 -0700