1. A good abstraction 1.1 class interface should display a consistent Abstraction Level
? A good way to consider classes is to regard classes as a mechanism to implement abstract data types. Each class should implement an ADT and only implement this ADT. If you find that a class implements more than one ADT, or you cannot determine what ADT it implements, you should reorganize the class into one or more explicit ADTs.
? If you take a fancy to the common subprograms of the class as the air lock valve used to prevent water from entering the submarine, the inconsistent common subprograms in the class are equivalent to the leaking dashboard. These leaking dashboards may not allow water to quickly enter as quickly as the air lock valve is opened, but they can still let the submarine sink as long as there is enough time. In fact, this is the consequence of the hybrid abstraction layer. When modifying a program, the mixed abstract layers will make the program more and more difficult to understand, and the whole program will gradually fall into the state until it becomes unable to be maintained.
1.2 understand the abstract implemented by the class
? Some classes are very similar. You must carefully understand which abstraction should be captured by the class interfaces. I have developed such a program that allows users to edit information in the form of tables. We want to use a simple grid control, but it cannot change the color of the input cells for the data, so we decided to use a workbook control that provides this function.
? A Workbook control is much more complex than a raster control. It provides 150 subprograms and only 15 raster controls. Because our goal is to use a grid control instead of a spreadsheet control, we asked programmers to write a package class to hide the fact that "using a spreadsheet control as a grid control. The programmer complained that it was unnecessary to increase the cost, bureaucratic style, and then left. A few days later, he brought a well-written package class, which faithfully exposed all the 150 subroutines owned by spreadsheet controls.
? This is not what we want. What we need is a raster control interface, which encapsulates the fact that "the entity is actually using a more complex spreadsheet control. The programmer should only expose the subroutines of the 15 grid controls, and add 16th subroutines that support cell color settings. He exposes all 150 subprograms, which means that once we want to modify the underlying implementation details, we have to support 150 subprograms. This programmer did not implement the encapsulation we needed, and brought him a lot of fearless work.
? The correct abstraction may be a spreadsheet control or a raster control, depending on the actual situation. When you have to make a choice between two similar abstractions, make sure that your choice is correct.
1.3 provide paired services (refer to the sub-control naming Rules)
? Most operations have the corresponding, equal, and opposite operations. If one operation is used to turn the light on, you may need another operation to turn the light off. If you have an operation to add a project to the list, you may also need another operation to delete the project from the list. If one operation is used to activate a menu item, you may also need another operation to shield the menu item. When designing a class, you need to check every Common Subprogram and decide whether to need another operation that is complementary to it. Do not blindly create the opposite operation, but you must consider it to see if it is needed.
1.4 transfer irrelevant information to other classes
? Sometimes you will find that half of a class's subprograms use the general data of the class, while the other half uses the other half. Now you have used the two classes together and split them!
1.5 make interfaces programmable as much as possible, rather than expressing Semantics
? Each interface consists of a programmable part and a semantic part. The programmable part consists of the Data Type and other attributes in the interface, and the compiler can forcibly request them (check for errors during compilation ). The semantic part is composed of the Assumption "How the interface will be used", which cannot be enforced by the compiler. For example, "routinea must be called before routineb" or "If datamember is passed to routinea without initialization, it will cause routinea to crash ". Semantic interfaces should be annotated, but do not rely on them as much as possible. Any part of an interface that cannot be enforced by the compiler is a part that may be misused. We need to find a way to replace the semantic interface element with the element of the programming interface, for example, using asserts or other technologies.
1.6 beware of damaging the abstraction of interfaces during Modification
? During the modification and extension of the class, you often find some additional functions. These functions are not very suitable for the original class interface, but they seem difficult to be implemented using another method. For example, you may find that the employee class has evolved into the following.
// Class employee {public: fullname getname () const; address getaddress () const; phonenumber getworkphone () const ;... bool Merge (jobclassification jobclass); bool iszipcodevalid (Address); bool Merge (phonenumber); sqlquery Merge () const; sqlquery getquerytomodifyemployee () const; sqlquery Merge () const ;... PRIVATE :...};
? The clear abstraction in the previous code example has now become a hodgedge of scattered functions. There is no logical association between an employee and a subroutine that checks the zip code, phone number, or position, the abstraction of subprograms that expose SQL statement query details is much lower than that of the employee class, and they all destroy the abstraction of the employee class.
1.7 do not add public members inconsistent with the interface Abstraction
? Every time you add a subroutine to the class interface, ask "is this subroutine consistent with the abstraction provided by the existing interface ?" If any inconsistency is found, it is necessary to modify it in another way to ensure abstract integrity.
1.8 abstract and cohesion considerations
? Abstract and cohesion are closely related to each other. A Class interface that presents a good abstraction usually has a high cohesion. Classes with strong cohesion tend to be very abstract, although this relationship is not as strong as the former.
? I found that the abstraction expressed by the interfaces of the category class is more helpful to understand the class design than the cohesion of the category class. If you find that the cohesion of a class is weak and you do not know how to change it, you can use another method to check whether the class is always abstract.
2. Good Encapsulation
? Encapsulation is a more abstract concept. Abstraction manages complexity by providing a model that allows you to ignore implementation details, while encapsulation forces you to see details-even if you want.
? These two concepts are related because abstraction is often easily broken when there is no encapsulation. In my experience, either both encapsulation and abstraction are possible, or both are lost. There is no other possibility.
2.1 restrict the accessibility of classes and members as much as possible
? Making accessibility as low as possible is one of the principles that facilitate encapsulation. When you hesitate to set the accessibility of a subprogram to public, private, or protected, experience should adopt the strictest and most feasible access level. I think this is a good idea, but I think there are more important suggestions, that is, to consider "which method can best protect the integrity of interface abstraction ?" If exposing a sub-program does not make the abstraction inconsistent, this may be feasible. If you are not sure, hiding is usually better than hiding less.
2.2 do not expose Member data publicly
? Exposing member data will undermine encapsulation and limit your control over this abstraction. If a point class exposes the following members:
float x;float y;float z;
It destroys encapsulation because the caller's code can freely use the data in the point class, but the point class does not even know when the data is modified. However, if the point class exposes these methods:
float GetX();float GetY();float GetZ();void SetX(float x);void SetY(float y);void SetZ(float z);
It is well encapsulated. You cannot know whether float x, y, and z are used in the underlying implementation, or whether point saves the data as double and then converts it to float, it is also impossible to know whether the Point stores them on the moon, and then retrieves them from the satellites in the outer space.
2.3 avoid putting private implementation details into the class Interface
? After real encapsulation, programmers will not see any implementation details at all. They are hidden both literally and metaphorical. However, some popular programming languages, including C ++, require programmers to reveal implementation details in class interfaces in terms of the language structure.
// Exposes the implementation details of the class. Class employee {public :... employee (fullname name, string address, string workphone, string homephone, taxid taxidnumber, jobclassification jobclass );... PRIVATE: String m_name; string m_address; int m_jobclass ;...};
? Putting the private segment declaration in the Class header file seems to be a small violation of the principle, but it actually encourages programmers to check the implementation details. In this example, the Customer Code is intended to use the address type to represent the address information, but the Implementation Details of "address information is saved with string" in the header file are exposed.
? Scott Meyers introduced a useful technique to solve this problem in article 2nd of Objective C ++. It is recommended that you isolate the class interface from the class implementation and include a pointer in the class declaration to point the pointer to the class implementation, but it cannot contain any other implementation details.
// Hides the class implementation details. Class employee {public :... employee (...)); fullname getname () const; address getaddress () const ;... PRIVATE: emplyeeimplementation * m_implementation ;};
? Now you can put the implementation details in the emplyeeimplementation class. This class is only visible to the employee class, but not to the code using the employee class.
? If you have already written a lot of code in your project that does not use this method, you may feel that it is not worthwhile to change a lot of existing code to use this method. But when you read the code that exposes its implementation details, you should resist the temptation and not look for clues about the implementation details in the private part of the class interface.
2.4 do not make any assumptions for class users
? The settings and Implementation of the class must comply with the contract implied in the class interface. It should not make any assumptions about how the interface will be used or not used-unless explicitly stated in the interface. The following annotations show that this class assumes that its users are too many.
// Initialize X, Y, and Z to 1.0, because if they are initialized to 0.0, derivedclass will crash.
2.5 avoid user Meta
? In some cases, for example, in the State mode, using a friend class in the correct way will help to manage complexity. But in general, the meta-class will destroy the encapsulation, because it allows you to consider more code at the same time, thus increasing complexity.
2.6 do not classify a subprogram as a public interface because it only uses a public subprogram.
? The fact that a subprogram only uses common subprograms is not a very important consideration. On the contrary, the question to be asked is whether the abstraction displayed by the interface is consistent after this subroutine is exposed to the outside world.
2.7 make it easier to read code than to write code
? Reading code is much more frequently than writing code, even in the early stages of development. Therefore, it is very economical to reduce the readability of the Code to make it easier to write the code. Especially when creating a class interface, even if a subroutine does not match the abstract of the interface, sometimes people usually add this subroutine to the interface, this makes it easier to call code somewhere in the class being developed. However, the addition of this subroutine is the beginning of the official code downhill, so it is better not to step out of this step.
2.8 be especially vigilant against semantic destruction of encapsulation
? I once thought that as long as I learned to avoid syntax errors, I would be able to steadily win the coupons. However, I soon discovered that it was just the beginning to learn to avoid syntax errors, followed by countless encoding errors, and most of them were more difficult to diagnose and correct than syntax errors.
? In comparison, the semantic encapsulation and syntax encapsulation are almost the same. From the syntax point of view, to avoid spying on the internal implementation details of another class, it is relatively easy to declare its internal subroutines and data as private. However, it is completely another thing to achieve semantic encapsulation. The following is an example of how the code of some class callers destroys its encapsulation in terms of semantics.
Do not call the initializeoperations () subroutine of Class A, because you know that the preformfirstoperation () subroutine of Class A will automatically call it.
We will no longer call the database. Connect () subprogram before employee. retrive (database), because you know that when no database connection is established, employee. retrive () will connect to the database.
Do not call the terminate () subroutine of Class A, because you know that the preformfinaloperation () subroutine of Class A has already called it.
Even after objecta leaves the scope, you still use the pointer or reference created by objecta to point to objectb, because you know that objecta places objectb in a static bucket, therefore, objectb is certainly usable.
Use classb. maximum_elements instead of classa. maximum_elements, because you know the two values are equal.
? The problem with the above examples is that they allow the caller code to depend on the public interface of the class, but on the private Implementation of the class. When you find that you know how to use this class by checking the internal implementation of the classInterface Programming, But inImplement internal programming through interfaces. If you program through interfaces, encapsulation will be destroyed. Once encapsulation begins to be destroyed, abstraction will soon suffer.
? If you still cannot know how to use a class based on the class interface document, the correct method is not to pull the source code of the class and view its internal implementation. This is a good intention, but it is a wrong decision. The correct method should be to contact the author of the class and tell him, "I don't know how to use this class ." For the class author, the correct method is not to tell you the answer face to face, but to check out the interface file of the class from the code base, modify the interface document of the class, and then check the file back, then I will tell you, "See if you know how to use it now." You want to make this conversation appear in the interface code so that it can be seen by future programmers. You do not want this conversation value to be stored in your own mind, so that the caller code using this class is branded with the semantic amount of subtle dependencies. You also don't want this conversation to be conducted only in your personal opinion. This will only benefit your code, but not others.
2.9 pay attention to the close coupling relationship
"Coupling" refers to the closeness of the two types of relationships. Generally, the looser the relationship is, the better. Based on this concept, we can provide the following guidance:
- Restrict the accessibility of classes and members as much as possible.
- Avoid metaclasses because they are tightly coupled.
- Declare data as private rather than protected in the base class to reduce the coupling between the derived class and the base class.
- Avoid exposing member data in the public interface of the class.
- Be vigilant against semantic damages to encapsulation.
- Detect "Demeter rules"
Coupling is closely related to abstraction and encapsulation. Close Coupling always occurs when the abstraction is not rigorous or the encapsulation is damaged. If a class provides an incomplete set of services, other subprograms may directly read and write internal data of the class. In this way, the class is opened, and it is converted from a black box to a glass box, thus eliminating the encapsulation of the class.
07 good class interfaces