In software development, in order to improve the maintainability and reusability of the software system, and increase the scalability and flexibility of the software, programmers should try to develop programs according to the six principles, so as to improve the efficiency of software development, save software development costs and maintenance costs.
1. Open and close principle
Open for extensions, closed for modifications. When the program needs to be extended, you cannot modify the original code to achieve a hot plug effect. In short, to make the program extensible, easy to maintain and upgrade.
To achieve this, we need to use interfaces and abstract classes.
Because abstraction has good flexibility and wide adaptability, as long as abstraction is reasonable, it can basically maintain the stability of software architecture. The changeable details in software can be extended from the abstract implementation class. When the software needs to change, it only needs to derive an implementation class to extend according to the requirements.
The following skin sogou input method as an example to introduce the application of the open and close principle.
The skin design of Sogou input method.
Analysis: The skin of sogou input method is the combination of elements such as input background picture, window color and sound. Users can change the skin of their input method according to their own preference, or download a new skin from the Internet. These skins have common characteristics, and you can define an abstract class (AbstractSkin) for them, with each specific skin (DefaultSpecificSkin and HeimaSpecificSkin) as a subclass. The user form can select or add new themes as needed without modifying the original code, so it is open and closed.
2. Richter’s substitution principle
Richter’s substitution principle is one of the basic principles of object-oriented design.
Richter’s substitution rule: Wherever a base class can appear, a subclass must appear. A subclass extends the functionality of its parent class, but does not change the functionality of its parent class. In other words, when a subclass inherits from a parent class, it should try not to override the methods of the parent class, except to add new methods to accomplish new functionality.
If the method of rewriting the parent class to complete the new function, although it is simple to write, but the reusability of the whole inheritance system will be relatively poor, especially when the use of more frequent polymorphism, the probability of program running error will be very large.
Now let’s look at a classic example of Richter’s substitution principle
A square is not a rectangle.
In mathematics, a square is unquestionably a rectangle; it is a rectangle of equal length and width. So, we developed a software system related to geometry, and it is natural to let the square inherit from the rectangle.
/ / rectangle
public class Rectangle {
private double length;
private double width;
public double getLength(a) {
return length;
}
public void setLength(double length) {
this.length = length;
}
public double getWidth(a) {
return width;
}
public void setWidth(double width) {
this.width = width; }}Copy the code
/ / square
public class Square extends Rectangle {
// Call the superclass method to set the edge length
@Override
public void setLength(double length) {
super.setLength(length);
super.setWidth(length);
}
@Override
public void setWidth(double width) {
super.setWidth(width);
super.setLength(width); }}Copy the code
public class RectangleDemo {
public static void main(String[] args) {
Rectangle r = new Rectangle();
r.setLength(20);
r.setWidth(10);
resize(r);
printLengthAndWidth(r); / / length: 20.0 | wide: 21.0
System.out.println("= = = = = = = = = = = = = = = = = = = = =");
Square s = new Square();
s.setLength(20);
resize(s);
printLengthAndWidth(s); / / death cycle
}
private static void resize(Rectangle rectangle) {
/ / to broaden
while (rectangle.getWidth() <= rectangle.getLength()) {
rectangle.setWidth(rectangle.getWidth() + 1); }}private static void printLengthAndWidth(Rectangle rectangle) {
System.out.println("Long." + rectangle.getLength() + "| wide."+ rectangle.getWidth()); }}Copy the code
If we pass a normal rectangle as a parameter to the resize method, we will see a gradual increase in the width of the rectangle. When the width is greater than the length, the code will stop.
If we pass another square as an argument to the resize method, we see that the square’s width and length grow, and the code keeps running until the system generates an overflow error. So, normal rectangles are appropriate for this code, squares are not.
We conclude that the Rectang1e parameter cannot be replaced by the Square parameter in the resize method, and that the desired result will not result if the substitution is made. Thus, the inheritance relationship between the class Square and Rectang1e violates the Principle of Richter’s substitution, the inheritance relationship between them does not hold, and the Square is not a rectangle.
How can it be improved? We need to redesign the relationship between them. Abstract a Quadrilateral interface and have the Rectangle and Square classes implement the Quadrilateral interface
public interface Quadrilateral {
double getLength(a);
double getWidth(a);
}
Copy the code
public class Rectangle implements Quadrilateral {
private double length;
private double width;
public void setLength(double length) {
this.length = length;
}
public void setWidth(double width) {
this.width = width;
}
@Override
public double getLength(a) {
return length;
}
@Override
public double getWidth(a) {
returnwidth; }}Copy the code
public class Square implements Quadrilateral {
private double side;
public double getSide(a) {
return side;
}
public void setSide(double side) {
this.side = side;
}
@Override
public double getLength(a) {
return side;
}
@Override
public double getWidth(a) {
returnside; }}Copy the code
public class RectangleDemo {
public static void main(String[] args) {
Rectangle r = new Rectangle();
r.setLength(20);
r.setWidth(10);
resize(r);
printLengthAndWidth(r); / / length: 20.0 | wide: 21.0
System.out.println("= = = = = = = = = = = = = = = = = = = = =");
Square s = new Square();
s.setSide(20);
printLengthAndWidth(s); / / length: 20.0 | wide: 20.0
}
// There is no inheritance relationship between rectangle and square
private static void resize(Rectangle rectangle) {
/ / to broaden
while (rectangle.getWidth() <= rectangle.getLength()) {
rectangle.setWidth(rectangle.getWidth() + 1); }}private static void printLengthAndWidth(Quadrilateral quadrilateral) {
System.out.println("Long." + quadrilateral.getLength() + "| wide."+ quadrilateral.getWidth()); }}Copy the code
3. Rely on the inversion principle
A high-level module should not depend on a low-level module; both should depend on its abstraction; Abstractions should not depend on details, details should depend on abstractions. Simply put, requiring that the abstraction be programmed, not the implementation, reduces the coupling between the client and the implementation module.
Let’s look at an example to understand the dependency inversion principle
Assemble a computer
Now to assemble a computer, you need accessories CPU, hard disk, memory. Only when these configurations are available can the computer run properly. There are many choices to choose from CPU, such as Intel, AMD, etc., hard disk can choose Seagate, Western digital, etc., memory can choose Kingston, Pirate Ship, etc.
// Seagate hard drive
public class XijieHardDisk {
public void save(String data) {
System.out.println("Used Seagate hard drive to store data:" + data);
}
public String get(a) {
System.out.println("Using a Seagate drive to read the data.");
return "Data"; }}Copy the code
// Intel CPU
public class IntelCPU {
public void run(a) {
System.out.println("Using Intel processors."); }}Copy the code
// Kingston memory
public class KingstonMemory {
public void save(a){
System.out.println("Use Kingston MEMORY"); }}Copy the code
/ / computer
public class Computer {
private XijieHardDisk hardDisk;
private IntelCPU cpu;
private KingstonMemory memory;
public void run(a){
System.out.println("Run the computer...");
String data = hardDisk.get();
System.out.println("Data read from the hard disk is:"+data);
cpu.run();
memory.save();
}
public XijieHardDisk getHardDisk(a) {
return hardDisk;
}
public void setHardDisk(XijieHardDisk hardDisk) {
this.hardDisk = hardDisk;
}
public IntelCPU getCpu(a) {
return cpu;
}
public void setCpu(IntelCPU cpu) {
this.cpu = cpu;
}
public KingstonMemory getMemory(a) {
return memory;
}
public void setMemory(KingstonMemory memory) {
this.memory = memory; }}Copy the code
public class ComputerDemo {
public static void main(String[] args) {
/ / the new computer
Computer computer = new Computer();
/ / capacity
computer.setCpu(new IntelCPU());
computer.setHardDisk(new XijieHardDisk());
computer.setMemory(new KingstonMemory());
/ / run
computer.run();
/* Run the computer... Read data using Seagate hard drives Data read from hard drives is: Data using Intel processors using Kingston memory sticks */}}Copy the code
The code above shows that a computer has been assembled, but it seems that the CPU must be Intel, the memory must be Kingston, and the hard disk must be Seagate. This is not user-friendly, and users must want to choose their own accessories according to their own preferences.
Improvement according to the principle of dependency reversal:
In the code, we just need to modify the Computer class so that it relies on abstraction (interfaces for individual components) rather than on concrete implementation classes for individual components.
Design of the interface
public interface Cpu {
void run(a);
}
Copy the code
public interface HardDisk {
void save(String data);
String get(a);
}
Copy the code
public interface Memory {
void save(a);
}
Copy the code
Specific hardware implementation
public class XijieHardDisk implements HardDisk {
@Override
public void save(String data) {
System.out.println("Used Seagate hard drive to store data:" + data);
}
@Override
public String get(a) {
System.out.println("Using a Seagate drive to read the data.");
return "Data"; }}Copy the code
public class IntelCPU implements Cpu {
@Override
public void run(a) {
System.out.println("Using Intel processors."); }}Copy the code
public class KingstonMemory implements Memory {
@Override
public void save(a) {
System.out.println("Use Kingston MEMORY"); }}Copy the code
The design of computer
public class Computer {
// No longer a concrete implementation, but an abstract interface
private HardDisk hardDisk;
private Cpu cpu;
private Memory memory;
public void run(a){
System.out.println("Run the computer...");
String data = hardDisk.get();
System.out.println("Data read from the hard disk is:"+data);
cpu.run();
memory.save();
}
public HardDisk getHardDisk(a) {
return hardDisk;
}
public void setHardDisk(HardDisk hardDisk) {
this.hardDisk = hardDisk;
}
public Cpu getCpu(a) {
return cpu;
}
public void setCpu(Cpu cpu) {
this.cpu = cpu;
}
public Memory getMemory(a) {
return memory;
}
public void setMemory(Memory memory) {
this.memory = memory; }}Copy the code
To start the installed
public class ComputerDemo {
public static void main(String[] args) {
/ / the new computer
Computer computer = new Computer();
// Install the new accessories only to realize their respective interface can be used
computer.setCpu(new IntelCPU());
computer.setHardDisk(new XijieHardDisk());
computer.setMemory(new KingstonMemory());
/ / run
computer.run();
/* Run the computer... Read data using Seagate hard drives Data read from hard drives is: Data using Intel processors using Kingston memory sticks */}}Copy the code
4. Interface isolation principle
A client should not be forced to rely on methods it does not use; The dependency of one class on another should be based on the smallest interface.
Let’s look at an example to understand the interface isolation principle
Safety door case
We need to create a dark horse brand safety door, the safety door with fire, waterproof, anti-theft functions. Can be fire, waterproof, anti-theft function extraction into an interface, the formation of a set of specifications. The class diagram is as follows:
We found the problems in the above design. The safety door of Dark Horse brand has the functions of anti-theft, waterproof and fire prevention. Now if we also need to create a brand of safety door, and the safety door only has anti-theft, waterproof function? It is clear that implementing the SafetyDoor interface violates interface isolation, so how can we change it? Look at the following class diagram:
5. Demeter’s Rule
Demeter’s rule is also known as the least knowledge principle.
Talk only to your immediate friends and not to strangers.
The implication is that if two software entities do not communicate directly, then direct calls to each other should not occur and can be forwarded by a third party. Its purpose is to reduce the degree of coupling between classes and improve the relative independence of modules.
The “friend” in Demeter’s law refers to the current object itself, its member object, the object created by the current object, the method parameters of the current object, etc. These objects are associated, aggregated or combined with the current object, and can directly access the methods of these objects.
Let’s look at an example to understand Demeter’s rule
An example of the relationship between a star and his agent
As stars devote themselves to their art, their agents handle many daily tasks, from meeting fans to negotiating with media companies. Agents here are friends of the stars, and fans and media companies are strangers, so Demeter’s rule applies.
The class diagram is as follows:
/ / star
public class Star {
private String name;
public String getName(a) {
return name;
}
public void setName(String name) {
this.name = name; }}Copy the code
// Business company
public class Company {
private String name;
public String getName(a) {
return name;
}
public void setName(String name) {
this.name = name; }}Copy the code
// Broker (firm) third party
public class Agent {
private Star star;
private Fans fans;
private Company company;
public void meeting(a){
System.out.println(fans.getName()+"With the Stars"+star.getName()+"Meet you.");
}
public void business(a){
System.out.println(company.getName()+"With the Stars"+star.getName()+"Business negotiation");
}
public void setStar(Star star) {
this.star = star;
}
public void setFans(Fans fans) {
this.fans = fans;
}
public void setCompany(Company company) {
this.company = company; }}Copy the code
public class Test {
public static void main(String[] args) {
Star star = new Star();
Company company = new Company();
Fans fans = new Fans();
star.setName("Jay Chou");
fans.setName("Mr. Wang");
company.setName(Volcano Corporation);
Agent agent = new Agent();
agent.setCompany(company);
agent.setStar(star);
agent.setFans(fans);
agent.business(); // Volcano Company talks business with star Jay Chou
agent.meeting(); // Mr. Wang met the star Jay Chou}}Copy the code
6. Principle of composite reuse
The principle of composite reuse is to use association relations such as composition or aggregation first, and then consider inheritance relations to achieve.
Generally, class reuse can be divided into inheritance reuse and composite reuse.
Although inheritance reuse has the advantage of being simple and easy to implement, it also has the following disadvantages:
-
Inheritance reuse breaks class encapsulation. Because inheritance exposes the implementation details of the parent class to the child class, the parent class is transparent to the child class, so this reuse is also known as “white box” reuse.
-
A subclass has a high degree of coupling with its parent. Any change in the implementation of the parent class results in a change in the implementation of the subclass, which is not conducive to the extension and maintenance of the class.
-
It limits the flexibility of reuse. An implementation inherited from a parent class is static and defined at compile time, so it cannot be changed at run time.
In combination or aggregate reuse, existing objects can be incorporated into the new object and become a part of the new object. The new object can invoke the functions of the existing object, which has the following advantages:
-
It maintains class encapsulation. Because the internal details of component objects are invisible to the new object, this reuse is also known as “black box” reuse.
-
Low coupling between objects. You can declare abstractions at the member locations of a class.
-
High flexibility of reuse. This reuse can occur dynamically at run time, with new objects dynamically referencing objects of the same type as component objects.
Let’s look at an example to understand the composite reuse principle
An automobile classification management program
Automobiles can be divided into gasoline automobiles and electric automobiles according to “power source”. According to the “color” can be divided into white cars, black cars and red car years. If you consider both categories, the combinations are numerous. The class diagram is as follows:
As you can see from the class diagram above, there are many subclasses using inheritance reuse. If there are new power sources or new colors, new classes need to be defined. Let’s try to change inheritance reuse to aggregate reuse and let’s see.