11 principles of object-oriented design
- Single Duty principle (the Responsibility Principle , abbreviation SRP )
- Open-Close principle (the Open-close Principle , abbreviated as OCP )
- Liskov Replacement principle (the Liskov Substitution , hereinafter referred to as LSP )
- Dependency Inversion principle (the Dependency inversion Principle , referred to as Dip )
- Interface Isolation principle (the Interface segregation Principle , referred to as ISP )
- Reuse Publishing Equivalence principle (the Reuse-release equivalence Principle , referred to as rep )
- Common Reuse principle (the Common reuse Principle , referred to as CRP )
- Common closure principle (the Common close Principle , referred to as CCP )
- The principle of non-cyclic dependence (the No-annulus Dependency Principle , referred to as ADP )
- Stable Reliance principle (the Steady Dependency Principle , referred to as SDP )
- Stable Abstract principle (the Steady abstract Principle , referred to as SAP )
1-5 of the principles focus on the structure and coupling of all software entities (classes, modules, functions, etc.) that guide us in designing software entities and determining the interrelationships of software entities, while 6-8 of the principles focus on the cohesion of the package, which guides us to the class pack, and the principle of 9-11 to the coupling of the package, These principles help us determine the interrelationships between packages.
1 single duty principle (SRP)
As far as a class is concerned, there should be only one cause for it to change.
In the SRP, we define responsibility as "the cause of change". If you can think of more than one motive to change a class, then this class has more than one responsibility. Sometimes it's hard to notice this, and we're used to thinking about responsibility in a group form.
1.1 Rectangle Class
For example, the figure 2.1-1,rectangle class has two methods, one method draws the rectangle on the screen, and the other method calculates the rectangular area.
Figure 2.1-1 more than one duty
There are two different applications that use the rectangle class. One is about computational geometry, and the rectangle class helps with geometry calculations, and it never draws rectangles on the screen. Another application is about drawing, which may do some geometrical work, but it will certainly draw rectangles on the screen.
This design violates the SRP. The rectangle class has two responsibilities. The first responsibility provides a mathematical model of rectangular geometry; the second responsibility is to draw the rectangle on a graphical user interface.
The violation of the SRP has led to some serious problems. First, we must include the GUI code in the computational Geometry application. If this is a C + + program, you have to link the GUI code, which wastes the link time, compile time, and memory consumption. In the case of a Java program, the GUI's. class file must be deployed to the target platform.
Secondly, if the change of the graphical application is caused by some reason rectangle change, then this change will force us to rebuild, test already deployed computational Geometry application. If you forget to do this, computational Geometry application may fail in unpredictable ways.
A good design is to separate these two responsibilities into the two completely different classes shown in Figure 2.1-2. This design moves the calculated part of the rectangle class to the Geometryrectangle class, and now the change in the rectangle's rendering does not affect the computational Geometry application.
Figure 2.1-2 Segregation of duties
1.2 Conclusion
The SRP is one of the simplest principles of all principles and one of the most difficult principles to be used correctly. We will naturally combine our responsibilities. The real thing about software design is discovering responsibilities and separating those responsibilities from each other. In fact, the rest of the principles we are going to discuss will return to this issue in one way or another.
2 Open-closed principle (OCP)
Software entities (classes, modules, functions, and so on) should be extensible, but not modifiable.
The modules that follow the OCP design have two main features:
1, for the extension is open (open for extension)
This means that the behavior of the module can be extended. When the requirements of the application change, we can extend the module to the new behavior that satisfies those changes. In other words, we can change the function of the module.
2, for the change is closed (Closed for modification)
When you extend the behavior of a module, you do not have to change the module's source code or the binaries. The binary executable version of the module, whether it is a shared library, DLL, or Java jar file, does not have to be changed.
These two characteristics seem to contradict each other. The usual way to extend module behavior is to modify the source code of the module. Modules that are not allowed to be modified are often considered to have a fixed behavior. How is it possible to change the behavior of a module without altering its source code? How can you change the functionality of a module without having to make changes to it? --The key is abstraction!
2.1 Shape Application
We have an application that needs to draw circles and squares on the standard GUI.
2.1.1 Violation of OCP
program 2.2.1.1-1 square/circle the process solution of the problem
------------------------------shape.h------------------------------
Enum ShapeType {circle, square};
struct Shape
{
ShapeType Itstype;
}
------------------------------circle.h------------------------------
#include shape.h
struct Circle
{
ShapeType Itstype;
Double Itsradius;
Point Itscenter;
};
------------------------------square.h------------------------------
#include shape.h
struct Aquare
{
ShapeType Itstype;
Double itsside;
Point Itstopleft;
};
------------------------------DRAWALLSHAPES.C------------------------------
#include shape.h
#include circle.h
#include square.h
typedef struct SHAPE* Shapepointer;
Void drawallshapes (Shapepointer list[], int n)
{
int i;
for (i = 0; i < n; i++)
{
struct shape* s = list[i];
Switch (s->itstype)
{
Case Square:
Drawsquare ((struct square*) s);
break;
Case Circle:
Drawcircle ((struct circle*) s);
break;
}
}
}
The Drawallshapes function does not conform to the OCP because its addition to the new shape type is not closed. If you want this function to be able to draw a list that contains triangles, you must change the function. In fact, every new type of shape you add must change this function.
Again, we have to add a new member to the ShapeType enum when we make the above changes. Since all the different kinds of shapes depend on the declaration of this enum, all we have to do is recompile all the shape modules. And you also have to recompile all modules that depend on the shape class.
The solution in program 2.2.1.1-1 is inflexible because increasing triangle will cause the recompilation and redeployment of shape, Square, Circle, and Drawallshapes. This approach is fragile because it is likely that there are similar switch/case or IF/ELSE statements in other parts of the program that are difficult to find and understand. This approach is strong, because when you want to reuse drawallshapes in another program, you must attach square and Circle, even if the new program doesn't need them. So the program shows a lot of bad design smells.
2.1.2 Follow OCP
program 2.2.1.2-1 square/circle the Ood of the problem Solution Solutions
Class Shape
{
Public
virtual void Draw () const = 0;
};
Class Square:public Shape
{
Public
virtual void Draw () const;
};
Class Circle:public Shape
{
Public
virtual void Draw () const;
};
void Drawallshapes (vector<shape*>& list)
{
Vector<shape*>::iterator i;
for (i = = List.begin (); I! = List.end (); i++)
(*i)->draw ();
}
As you can see, if we want to extend the behavior of the Drawallshapes function in the program 2.2.1.2-1 so that it can draw a new shape, we simply add a new shape derived class. The Drawallshapes function does not need to be changed. This drawallshapes is in line with the OCP. You can extend the behavior of your code without having to change it. In fact, adding a triangle class has absolutely no effect on any of the modules shown here. Obviously, in order to be able to handle the triangle class, you have to change some parts of the system, but all the code shown here doesn't have to be changed.
This procedure is in accordance with the OCP. Changes to it are made by adding new code instead of changing the existing code. As a result, it does not cause cascading changes like a program that does not follow the OCP. The changes required are simply additions to the new module and changes around main to be able to instantiate the new type of object.
2.1.3 Yes, I lied.
The above example is not actually 100% closed! If we ask all the circles to be drawn before the square, then the Drawallshapes function in the program 2.2.1.2-1 cannot be closed for this change.
This leads to a troublesome result, in general, no matter how close the module is, there will be some changes that cannot be closed to it. There are no models that are appropriate for all situations.
Since it is impossible to be completely closed, the problem must be approached strategically. In other words, the designer must choose which of the modules he designed should be closed. He must first guess what kinds of changes are most likely to occur and then construct abstractions to isolate those changes.
There is an old saying: "Fool me once, should be ashamed of you." To fool me again, should be ashamed of me. "It's also an effective approach to software design. To prevent the software from being burdened with unnecessary complexity, we allow ourselves to be fooled once. This means that when we first wrote the code, we assumed that the change would not happen. When changes occur, we create abstractions to isolate similar changes that occur later. In short, we are willing to be hit by the first bullet, and then we will make sure that we are no longer hit by any other bullets fired by the same gun.
2.2 Conclusion
In many ways, OCP is at the heart of object-oriented design. Following this principle leads to the enormous benefits that object-oriented technology claims (i.e., flexibility, reusability, and maintainability). However, it is not that the principle is followed by the use of an object-oriented language. It is also not a good idea to make an arbitrary abstraction of every part of the application. The right thing to do is that developers should simply abstract those parts of the program that show frequent changes. Rejecting immature abstractions is as important as abstraction itself.
3 Liskov Replacement principle (LSP)
Sub-type ( subtype ) must be able to replace their base type ( base Type ).
Barbara Liskov wrote this principle for the first time in 1988 years. She said:
The following substitution properties are required: If each type S the object O1 , there is a type T the Object O2 , so that at all the T- program written by P Medium, O1 Replace O2 after the program P behavior and function are not changed, the S is T sub-type.
Consider the consequences of violating this principle, the importance of LSP is self-evident. Suppose there is a function f, whose argument is a pointer or reference to a base type B. It is also assumed that the derived class D of a B, if the object of D is passed as type B to f, causes the error behavior of F. Then d would violate the LSP. Obviously D is vulnerable to f.
The writer of F will want to do some tests on D so that when the object of D is passed to F, F can have the correct behavior. This test violates the OCP because this f is no longer closed for all of the different derived classes of B. Such tests are a stink of code that is the result of an inexperienced developer (or, worse, a hurried developer) violating the LSP.
3.1 Squares and rectangles, subtle irregularities
program 2.3.1-1 Rectangle Classes and square class
Class Rectangle
{
Public
void SetWidth (double w) {itswidth = W;}
void SetHeight (Double h) {itsheight = h;}
Double getwidth () {return itswidth;}
Double GetHeight () {return itsheight;}
Private
Point Itstopleft;
Double itswidth;
Double itsheight;
};
Class Square:public Rectangle
{
Public
{
Rectangle::setwidth (w);
Rectangle::setheight (w);
}
{
Rectangle::setwidth (h);
Rectangle::setheight (h);
}
};
A square is a rectangle in the general sense. Therefore, it is logical to treat the square class as derived from the rectangle class.
This use of is-a relationships is sometimes considered to be one of the basic techniques of object-oriented analysis (OOA). A square is a rectangle, so the square class derives from the rectangle class. But the idea will bring some subtle but very serious problems. In general, these problems are unpredictable and will not be discovered until we write the code.
We first notice that the problem is that the square class does not require both member variables itsheight and itswidth. But the square class will still inherit them in the rectangle class. Obviously it's a waste. In many cases, this waste is irrelevant. However, if we have to create hundreds of square objects, the amount of waste is huge.
Let's say we don't really care about memory efficiency at the moment. Deriving the square class from the rectangle class also produces some other problems. Consider the following function:
void F (rectangle& R)
{
R.setwidth (+); Calls Rectangle::setwideth ()
}
If we pass a reference to the square object to this function, the square object will be destroyed because their length will not change. This clearly violates the LSP. When an object of the rectangle derived class is passed in as a parameter, the function f does not run correctly. The reason for the error is that setwidth and setheight are not declared as virtual functions in rectangle, so they are not polymorphic.
This error is easy to fix. However, if the creation of a derived class causes us to change the base class, this often means that the design is flawed. Of course it violates the OCP. One might argue that the real design flaw is to forget to declare setwidth and setheight as virtual functions, and we have made corrections. However, this is hard to convince because setting a rectangle's length and width is a very basic operation. If the existence of the square class is not foreseen, why should we declare the two functions as virtual functions?
Nevertheless, suppose we accept this reason and fix these classes.
program 2.3.1-2 rectangle after the correction class
Class Rectangle
{
Public
virtual void SetWidth (double w) {itswidth = W;}
virtual void SetHeight (double h) {itsheight = h;}
Double getwidth () {return itswidth;}
Double GetHeight () {return itsheight;}
Private
Point Itstopleft;
Double Itswidth;
Double Itsheight;
};
3.1.1 The real problem
Now both square and rectangle seem to work properly. Whatever action the square object does, it is consistent with the mathematical squares. Whatever the rectangle object does, it is consistent with the mathematical meaning of the rectangle. In addition, square can be passed to a function that accepts pointers or references to rectangle, while Square retains the properties of a square that is consistent with a square in mathematical sense.
In this view, the design seems to be self-compatible and correct. However, this conclusion is wrong. A self-compatible design is not necessarily self-compatible with all user programs. Consider the following function, G:
void G (rectangle& R)
{
R.setwidth (5);
R.setheight (4);
ASSERT (R.area () = = 20);
}
This function believes that the passed in must be rectangle, and calls its member functions SetWidth and SetHeight. For rectangle, this function works correctly, but an assertion error (assertion error) occurs if the square object is passed in. So the real question is: The writer of function G assumes that changing the width of the rectangle does not cause its long change.
It is clear that changing the width of a rectangle does not affect its long hypothesis that it is reasonable! However, not all objects that can be passed as rectangle satisfy this hypothesis. If you pass an instance of a square class to G to do a hypothetical function, the function will behave incorrectly. The function g is fragile for square/rectangle hierarchies.
The performance of function g shows that there are some pointers to rectangle objects or functions referenced, which do not correctly manipulate the square object. For these functions, square cannot replace rectangle, so the relationship between square and rectangle is a violation of the LSP.
3.1.2 Is-a is about behavior.
So how exactly is it going to make? Why is there a problem with the apparently reasonable model of square and rectangle? After all, square should be rectangle. Aren't there is-a relationships between them?
For those that are not G, the square can be a rectangle, but from the G's point of view, the square object is definitely not a rectangle object. Why!? Because the square object behaves in a manner incompatible with the behavior of the rectangle object expected by function G. From the point of view of behavior, Square is not rectangle, and the behavior of objects is the real concern of software. The LSP clearly points out that the is-a relationship in Ood is in the way of behavior, and that the behavior can be reasonably assumed and relied on by the client program.
3.2 Throwing exceptions from derived classes
Another form of LSP violation is the addition of exceptions that are not thrown by other base classes in the methods of the derived class. If the consumer of the base class does not expect these exceptions, then adding them to the methods of the derived class results in a non-replaceable nature. To follow the LSP, either you must change the expectations of the user, or the derived class should not throw these exceptions.
3.3 Validity is not an essential attribute
When considering whether a particular design is appropriate, it is not possible to see this solution completely in isolation. It must be examined according to the reasonable assumptions made by the user of the design.
Does anyone know what reasonable assumptions the user of the design will make? Most of these assumptions are difficult to predict. In fact, if we try to predict all of these assumptions, the system we get is likely to be filled with the stench of unnecessary complexity. Thus, as with all other principles, it is usually best to predict only those most obvious breaches of the LSP and postpone all other predictions, and to deal with the associated vulnerability when the odor is present.
3.4 Conclusion
The OCP is at the heart of many of Ood's claims. If this principle is applied effectively, the application will have more maintainability, reusability, and robustness. LSP is one of the main principles that make OCP possible. It is the substitution of subtypes that allows modules that use the base class type to scale without modification. This kind of substitution must be something that developers can implicitly rely on. Therefore, if you do not explicitly enforce a contract of the base class type, then the code must be well and clearly expressed in this way.
The saying "is-a" is too broad to be defined as a subtype. The correct definition of a subtype is "replaceable", where the substitution can be defined by an explicit or implicit contract.
4 dependency Inversion principle (DIP)
A, High-level modules should not be dependent on lower-layer modules, both of which should be dependent on abstraction.
B, abstractions should not be dependent on detail, and detail should be dependent on abstraction.
The term "inverted" is used in the name of this principle because many traditional software development methods, such as structured analysis and design, always tend to create some high-level modules that rely on low-layer modules, and policy relies on the details of the software architecture. In fact, one of the purposes of these methods is to define the subroutine hierarchy, which describes how the high-level module calls the lower-layer module. The initial design of the copy program in section 1.2 of the first chapter is a typical example of this hierarchy. A well-designed object-oriented program whose dependent program structure is "inverted" relative to the usual structure of the traditional process method design.
Consider what it means when a high-level module relies on a lower-layer module. The high-level module contains an important policy choice and business model for an application. It is these high-level modules that make their application different from the others. However, if these high-level modules are dependent on lower-layer modules, changes to the lower-level modules directly affect the high-layer modules, forcing them to make changes in turn.
This situation is very absurd! This should be a high-level strategy to set up the module to affect the lower levels of detail implementation module. The module that contains the business rules should take precedence over and separate from the module that contains the implementation details. In any case, high-level modules should not be dependent on lower modules.
In addition, we want to be able to reuse the high-level policy settings module. We are already very good at reusing low-level modules in the form of a sub-library. Reusing high-level modules in different contexts can become very difficult if the high-level modules are dependent on the lower modules. However, high-level modules can be easily reused if they are independent of low-level modules. This principle is the core principle of framework design.
4.1 Hierarchy
Take a look at the hierarchical scheme of Figure 2.4.1-1:
Figure 2.4.1-1 Simple Hierarchical scheme
In the diagram, the policy layer on the upper level uses the lower mechanism layer, and the mechanism layer uses the more detailed layers utility. This seems to be true, but there is a hidden error characteristic: The Policy layer is sensitive to changes to its next until utility layer. This dependency is passed on. The policy layer relies on certain layers that depend on the utility layer, so the policy layer transitivity relies on the utility layer. This is very bad.
Figure 2.4.1-2 shows a more suitable model. Each higher level declares an abstract interface for the service it needs, and the lower level implements the abstract interface, with each high-level class using the next layer through the abstraction interface, so that the high-level is not dependent on the lower layers. The lower layer relies instead on the abstract service interfaces declared in the upper layer. This not only relieves the policy layer's transitive dependency on the utility layer, it also relieves the policy layer's dependency on the mechanism layer.
Figure 2.4.1-2 Inverted hierarchy
Note that the inversion here is not just an inversion of dependency, it is also an inversion of the ownership of the interface. We generally assume that the ToolPak should have its own interface. But when the dip is applied, we find that the client often has an abstract interface, and that the service providers derive from these abstract interfaces.
4.1.1 Inverted Interface Ownership
This is the famous Hollywood principle: "Don ' t call us, we'll call you." (Don't call us, we'll call you.) The low-level module implements an interface that is declared in a high-rise module and called by a high-level module.
By inverting the interface ownership, any changes to the mechanism layer or utility layer will not affect the policy layer. Also, the policy layer can be reused in any context that implements the policy Service interface. Thus, by inverting these dependencies, we create a more flexible, durable, and easily changing structure.
4.1.2 relies on abstraction
A slightly simpler but still very effective interpretation of dips is such a simple heuristic rule: "Dependent on abstraction". This is a simple statement that the heuristic rule recommendation should not be dependent on a specific class-that is, all dependencies in the program should be terminated in an abstract class or interface.
According to heuristic rules:
- Any variable should not hold a pointer to a specific class or reference
- No class should derive from a specific class
- No method should overwrite a method that has already been implemented in any of its base classes
Of course, every program will have a violation of this rule. Sometimes you have to create instances of specific classes, and the modules that create those instances will depend on them. In addition, this heuristic rule seems unreasonable for those classes that are specific but stable (nonvolatile). If a specific class is less likely to change and does not create other similar derived classes, then relying on it does not cause damage.
For example, in most systems, classes that describe strings are specific (such as the String class in Java), and the class is sometimes stable, that is, it is less likely to change. Therefore, direct reliance on it does not cause damage.
However, most of the specific classes we write in the application are unstable. We do not want to rely directly on these unstable concrete classes. By hiding them behind an abstract interface, you can isolate their instability.
This is not a perfect solution. Often, if the interface of an unstable class must change, this change must affect the abstract interface of the class. This change undermines the isolation of the abstract interface.
As a rule, the heuristic rules are a little easier to think about. On the other hand, if you look farther and think that the client is declaring the service interface it needs, then the interface will be changed only when the customer needs it. Thus, changing the class that implements the abstract interface does not affect the customer.
4.2 Conclusion
Using the dependency structure created by the traditional procedural programming, the strategy is dependent on the details. This is bad because it makes the strategy subject to changes in detail. Object-oriented programming inverts the dependency structure, making the details and strategy dependent on abstraction, and often the customer has a service interface.
In fact, the inversion of this dependency is exactly where the object-oriented design marks. It doesn't matter what language you use to write your program. If the dependencies of a program are inverted, it is an object-oriented design. Otherwise, it is a process-engineered design.
Dip is a basic low-level mechanism for realizing the benefits claimed by many object-oriented technologies. Its correct application is necessary for implementing a reusable framework. It is also important to build code that is resilient in the face of change. Because abstractions and details are isolated from each other, the code is also very easy to maintain.
5 Interface Isolation principle (ISP)
Customers should not be forced to rely on methods that they do not want. The interface belongs to the customer and does not belong to the class hierarchy in which it resides.
This principle is used to deal with the shortcomings of the "fat" interface. If the interface of the class is not cohesive (cohesive),
means that the class has a "fat" interface. In other words, the "fat" interface of a class can be decomposed into multiple sets of methods. Each group of methods serves a different set of client programs. In this way, some client programs can use a set of member functions, while other client programs can use the member functions of other groups.
ISPs admit that there are some objects that do not need to be clustered interfaces: But ISPs recommend that clients not see them as a single class presence. Instead, the client program should see more than one abstract base class with a cohesive interface.
If the client program is forced to rely on methods that they do not use, then these client programs face changes due to changes in these unused methods. This inadvertently results in a coupling between all client programs. In other words, if a client program relies on a class that contains a method that it does not use, but other client programs use it, the client program is affected when other customers ask for this class to change. We want to avoid this coupling as much as possible, so we want to detach the interface.
Example of 5.1 ATM user interface
Now let's consider an example: a traditional ATM problem. ATM requires a very flexible user interface. Its output information needs to be translated into many different languages. The output information may be displayed on the screen, or on a braille writing pad, or through a speech synthesizer. Obviously, this requirement can be achieved by creating an abstract base class with an abstract method to handle all the different messages that need to be rendered by the interface. As shown in 2.5.1-1:
Figure 2.5.1-1 ATM Interface Hierarchy
Similarly, different operations that can be performed by each ATM can be encapsulated as derived classes of class transaction. In this way, we can get classes Deposittransaction, Withdrawaltransaction and Transfertransaction. Each class invokes a method of the UI. For example, to require the user to enter the amount that they want to store, the Deposittransaction object invokes the Requestdepositamount method in the UI class. Similarly, in order to require the user to enter the amount to be transferred, the Transfertransaction object invokes the Requesttransferamount method in the UI class. Figure 2.5.1-2 is the corresponding class diagram.
Figure 2.5.1-2 ATM Operations Hierarchy
Please note that this is exactly what the ISP tells us should avoid. Each action uses a method of the UI, and no other action classes are used. Thus, changes to any one of the transaction's derived classes will force a corresponding change to the UI, which will also affect all other transaction derived classes and all other classes that depend on the UI interface. This design has the rigidity and the odor of fragility.
For example, if you want to add an action paygasbilltransaction, in order to handle the specific message that the operation wants to display, you must add a new method to the UI, which, unfortunately, is due to deposittransaction, Withdrawaltransaction and transfertransaction all depend on the UI interface, so they all need to be recompiled. Even worse, if these operations are deployed as different DLLs or shared libraries, the components must be redeployed, even if their logic has not changed. Did you smell the sticky smell?
This inappropriate coupling can be avoided by decomposing the UI interface into separate interfaces such as Depositui, Withdrawalui, and Transferui. The final UI interface can go multiple to inherit these individual interfaces. Figure 2.5.1-3 shows this model.
Figure 2.5.1-3 separate ATM UI interface
Each time a new derived class of the transaction class is created, the abstract interface UI needs to add a corresponding base class and therefore the UI interface and all of his derived classes must be changed. However, these classes are not widely used. In fact, they may be used only by main or by processes that start the system and create a specific UI instance. As a result, the impact of adding new UI base classes is minimized.
5.2 Conclusion
Fat classes can cause unhealthy and harmful coupling relationships between their client programs. When a client program asks for a change to the Fat class, it affects all other client programs. Therefore, client programs should rely solely on the methods they actually invoke. This can be achieved by decomposing the fat-like interface into multiple client-specific interfaces. Each client-specific interface declares only those functions that are called by its specific client or client group. The Fat class can then inherit all the client-specific interfaces and implement them. This solves the dependencies between the client program and the methods that they do not call, and makes the client program independent of each other.
11 principles of object-oriented design