More Effective C ++ Item M33: Designed non-tail class as abstract class

zhaozj2021-02-16  58

Item M33: Designed from non-tail class as abstract class

Suppose you are working in a software project, it handles animals. In this software, most animals can be abstract, but two animals-clarity and chicks - need special treatment. Obviously, the link between the clarity and the chick and the animal is like this:

Animal

| | |

/ /

/ /

/ /

Lizard Chicken

Animal class treats all animals common characteristics, clarity and chickens specialized animals to suit these two animals.

This is their simplified definition:

Class Animal {

PUBLIC:

Animal & Operator = (Const Animal & RHS);

...

}

Class Lizard: Public Animal {

PUBLIC:

Lizard & Operator = (Const Lizard & RHS);

...

}

Class chicken: public animal {

PUBLIC:

Chicken & Operator = (Const Chicken & RHS);

...

}

Here only the assignment function is written, but it is enough for us to be busy. Look at this code:

Lizard Liz1;

Lizard Liz2;

Animal * PANIMAL1 = & liz1;

Animal * PANIMAL2 = & liz2;

...

* PANIMAL1 = * PANIMAL2;

There are two problems here. First, the final row assignment is called the Animal class, although the type of related object is lizard. As a result, only the Animal section of LIZ1 is modified. This is partial assignment. After assignment, Liz1's Animal has a value from Liz2, but its Lizard member part has not been changed.

The second question is really some programmers write the code. It is a lot of programs that are assigned to objects with pointers, especially those who have extensive experience to C . Therefore, we should design more reasonable. As Item M32 pointed out, our class should be easily applicable without being used wrong, and the above level is easy to use.

A solution is to declare the assignment operation as a virtual function. If Animal :: Operator = is a virtual function, the assignment statement will call the value of lizard (the version that should be called). However, let's take a look at what it will happen to the virtual future:

Class Animal {

PUBLIC:

Virtual Animal & Operator = (Const Animal & RHS);

...

}

Class Lizard: Public Animal {

PUBLIC:

Virtual Lizard & Operator = (Const Animal & RHS);

...

}

Class chicken: public animal {

PUBLIC:

Virtual Chicken & Operator = (Const Animal & RHS);

...

}

Based on the revised modification based on the C language, we can modify the type of return value (so each of the correct classes), but C rules force us to declare the same parameter type. This means that the assignment of the Lizard class and the Chicken class must be prepared to accept any type of Animal object. That is, this means that we must face this fact: the following code is legal:

Lizard Liz; Chicken Chick;

Animal * PANIMAL1 = & liz;

Animal * PANIMAL2 = & cho;

...

* PANIMAL1 = * PANIMAL2; // Assign A Chicken TO

// a lizard!

This is a mixed type assignment: the left is a lizard, and the right is a Chicken. Mixed type assignment is usually not a problem in C , because the strong type of C will be illegally illegal. However, by setting animal assignment operation to a virtual function, we open the door of the mixed type operation.

This makes our situation difficult. We should allow the same type to assign values ​​through the pointer, but it is forbidden to assign a mixed type by the same pointer. In other words, we want to allow this:

Animal * PANIMAL1 = & liz1;

Animal * PANIMAL2 = & liz2;

...

* PANIMAL1 = * PANIMAL2; // Assign a lizard to a lizard

And want to ban this:

Animal * PANIMAL1 = & liz;

Animal * PANIMAL2 = & cho;

...

* PANIMAL1 = * PANIMAL2; // Assign A Chicken to a lizard

You can only distinguish between the running period, because * Panimal2 is assigned * Panimal1 is sometimes correct, sometimes not. We have fallen into the dark world of runtime runtime. In particular, we need to assign a value when mixing type, and the type is incorrectly in Operator =, and the type is the same, we expect to complete the assignment in the usual manner.

We can use Dynamic_CAST (see Item M2) to be implemented. Here's how to implement the assignment of lizard:

Lizard & lizard :: Operator = (Const Animal & RHS)

{

// Make Sure RHS Is Really a Lizard

Const lizard & rhs_liz = Dynamic_cast (rhs);

Proceed with a normal assocignment of rhs_liz to * this;

}

This function is only given to * this when RHS is indeed a lizard type. If rhs is not a lizard type, the function passes the BAD_CAST type of the Dynamic_cast to fail. (In fact, the type of abnormality is std :: BAD_CAST, because the components of the standard runtime, including the exceptions thrown, are located in the namespace STD. For the outline of the standard runtime, see Item E49 and ITEM M35).

Even if there is an abnormality, this function looks unnecessary complex and expensive - Dynamic_Cast is necessary to reference a Type_info structure; see Item M24 - Because it is usually a lizard object to give another:

Lizard Liz1, Liz2;

...

Liz1 = Liz2; // no need to perform a

// Dynamic_cast: this

// Assignment Must Be Valid

We can handle this situation without increasing complexity or spending Dynamic_CAST, as long as it adds a usual assignment operation in lizard: Class Lizard: Public Animal {

PUBLIC:

Virtual Lizard & Operator = (Const Animal & RHS);

Lizard & Operator = (const lizard & rhs); // add this

...

}

Lizard Liz1, Liz2;

...

Liz1 = Liz2; // Calls operator = taking

// a const lizard &

Animal * PANIMAL1 = & liz1;

Animal * PANIMAL2 = & liz2;

...

* PANIMAL1 = * PANIMAL2; // Calls operator = TAKING

// a const animal &

In fact, it gives the Operator =, which simplifies the realization of the former:

Lizard & lizard :: Operator = (Const Animal & RHS)

{

Return Operator

}

This function is now trying to convert RHS to a lizard. If the conversion is successful, the usual assignment operation is called; otherwise, a BAD_CAST exception is thrown.

To be honest, use Dynamic_cast to detect in the runtime period, which makes me very nervous. One thing to note, some compilers still do not support Dynamic_cast, so although it is theoretically portable, it is actually not necessarily. More importantly, it requires users who use lizard and chicken must be ready to capture Bad_cast exceptions at each assignment. If they don't do this, then I don't know if we get the benefits of exceeding the initial solution.

It is pointed out that this is a very unsatisfactory state of the virtual value operation, rearculates in the most beginning to try to find a way to prevent the user from writing a problem with the assignment statement is necessary. If this assignment statement is rejected in the compile period, we don't have to worry about doing something wrong.

The easiest way is to set the Operator = Private in Animal. Thus, the Lizard object can assign a value to the Lizard object, and the Chicken object can assign a value to the Chicken object, but the partial or mixed type assignment is disabled:

Class Animal {

Private:

Animal & Operator = (Const Animal & RHS); // this is now

... // Private

}

Class Lizard: Public Animal {

PUBLIC:

Lizard & Operator = (Const Lizard & RHS);

...

}

Class chicken: public animal {

PUBLIC:

Chicken & Operator = (Const Chicken & RHS);

...

}

Lizard Liz1, Liz2;

...

Liz1 = liz2; // fine

Chicken Chick1, Chick2; ...

Chick1 = Chick2; // Also Fine

Animal * PANIMAL1 = & liz1;

Animal * PANIMAL2 = & chick1;

...

* PANIMAL1 = * PANIMAL2; // Error! Attempt to Call

// private animal :: operator =

Unfortunately, Animal is also a physical class. This method simultaneously evaluates the assignment between animal objects as illegal:

Animal Animal1, Animal2;

...

Animal1 = animal2; // error! Attempt to call

// private animal :: operator =

Moreover, it also makes it impossible to correctly implement the assignment operation of the Lizard and Chicken classes, because the assignment of the derived class is responsible for calling the assignment function of its base class:

Lizard & lizard :: Operator = (const lizard & rhs)

{

IF (this == & r Hs) return * this;

Animal :: Operator = (rhs); // error! Attempt to Call

// private function. but

// lizard :: Operator = MUST

// Call this function to

... // Assign THE Animal Parts

} // of * this!

Later, this problem can be resolved by applying animal :: Operator = to protected, but "Allows the assignment between Animal objects to prevent Lizard and Chicken objects from partially assigning partial assignments". " What should I do?

The easiest thing is to rule out the needs of the AnImal object assignment, and the easiest implementation method is to design animal as an abstract class. As an abstract class, Animal cannot be instantiated, so there is no need to assign a value between Animal objects. Of course, this has led to a new issue because our original design indicates that animal object is necessary. There is a very easy solution: don't have to set animal to an abstract class, we create a new class - called Abstractanimal - to include the common attributes of Animal, Lizard, ChikCen, and set it to an abstract class. Each entity class is then inherited from Abstractanimal. The modified inheritance system is like this:

Abstractanimal

| | | | |

/ | /

/ | /

/ | /

Lizard Animal Chicken

The definition of the class is:

Class abstractanimal {

protected:

Abstractanimal & Operator = (Const Abstractanimal & RHS);

PUBLIC:

Virtual ~ Abstractanimal () = 0; // See Below ...

}

Class Animal: Public AbstractActanimal {

PUBLIC:

Animal & Operator = (Const Animal & RHS);

...

}

Class Lizard: Public AbstractActanimal {

PUBLIC:

Lizard & Operator = (Const Lizard & RHS);

...

}

Class Chicken: Public AbstractActanimal {

PUBLIC:

Chicken & Operator = (Const Chicken & RHS);

...

}

This design is given to you so what you need. Assignment between the same type is allowed, partial assignment, or different types of assignments are disabled; the assignment operation function of the derived class can invoke the value of the base class. In addition, all code involving an AIAML, Lizard or Chicken class does not need to be modified, because these classes still operate, their behavior is consistent with the introduction of Abstractanimal. Affairs, these codes need to be recompiled, but this is for "ensuring that the behavior of the assignment statement" is correct and the behavior may incorrect assignment statements cannot be compiled through "the small price paid.

To make this, the Abstractanimal class must be an abstract class - it must have at least one pure virtual function. In most cases, it is no problem with such a function, but in the case of very little, you will find that you need to create a class such as Abstractanimal, no member function is a natural pure virtual function. At this point, the traditional method is to declare the destructive function as a pure virtual function; this is also used above. In order to support the polymorphism, the base class generally requires the false prefix function (see Item 14), and the only troublesome to set it to pure virtuality must implement it outside the definition of the class (see P195, ITEM M29).

(If you implement a pure virtual function, you just have a knowledge that you are not well enough. The declaration of a function does not mean that it does not implement it, it means:

* The current class is an abstract class

* Any physical classes from this class must declare this function as a "ordinary" virtual function (that is, can not bring "= 0")

Yes, most of the pure virtual functions are not implemented, but the pure false prevention function is a special case. They must be implemented because they will also be called when the derived classification function is called. Moreover, they often perform useful tasks, such as release resources (see Item M9) or record messages. Implementing a pure virtual function is generally uncommon, but it is not just common, it is necessary. )

You may have noticed that the problem of assigning through the base class pointer here is based on the assumption that the entity class (such as animal) is a data member. If they do not have a data member, you may point out, then there will be no problem, which is safe from a new entity class from an unfailed entity class.

There are no data categories that can become entity classes without data: in the future, or it may have data members, or it still doesn't. If it may have a data member in the future, you are doing now is delayed (until the data member is added), you have long pain with a shortness (see Item M32). If this base class does not have a data member, then it is now an abstract class, what is the use of entity categories without data?

The physical base class such as Animal is replaced with abstract base classes such as Abstractanimal, which is much easier to easily understand the behavior of Operator =. It also reduces the possibility of trying to use polymorphisms to array, this behavior is unpleasant, interpretation of Item M3. However, this skill is the most designed level, because this replacement force you explicitly recognize an entity of an abstract behavior of a useful place. That is, it makes you create a new abstract class for useful prototypes, even if you don't know the existence of this useful prototype.

If you have two entity classes C1 and C2 and you like C2 public inheritance from C1, you should change the inheritance level of the two classes to the inheritance hierarchy, by creating a new abstract class A and will C1 and C2 From it to:

C1 a

| //

| / /

C2 C1 C2

Your initial idea has changed the inheritance level

The importance of this modification is to force you to determine abstract class A. It is very clear, C1 and C2 have contrast; this is why they use public inheritance (see Item E35). After the modification, you must determine what these copies are. Moreover, you must organize these commonly organized these copies of C , it will no longer be ambiguous, it reaches an abstract type level, which has a clear defined member function and a clear definition.

All this has led to some disturbing thinking. After all, each class has completed certain types of abstraction, should we create two classes in this inheritance system to target each prototype (one is an abstract class to indicate its abstract part (To Embody the Abstract Part of Thae Abstract, one is the physical class to represent the object generating part (to Embody the objection part of the Abstract)? No. If you do this, there will be too many classes in the inheritance system. Such inheritance systems are difficult to understand, it is difficult to maintain, the cost of compilation is expensive. This is not the purpose of object-oriented design.

Its purpose is to confirm useful abstraction and force them (and only them) put them in an entity such as an abstract class. But how do you confirm useful abstraction? Who knows what abstraction is proved in the future? Who can predict what he will inherit from the future?

Ok, I don't know how to predict a inheritance system future use, but I know one thing: abstraction in a place may just make a hacker, but many places need abstraction usually make sense. Then, useful abstraction is the abstraction that is needed. That is, they are equivalent to this class: It is useful to themselves (for example, there is such a type of object), and they are also useful for one or more derived classes.

When a prototype is first required, we can't prove the simultaneous creation of an abstract class (for this prototype) and a physical class (for the object corresponding to the prototype) is correct, but the second time, we can do this. is correct. The modifications I have described simply realize this process and forced design and programmers to express those useful abstractions in the process of doing this. Even if they don't know those useful prototypes. This also happens to make the correct assignment behavior.

Let's take a look at a simple example. Suppose you are preparing a program to handle mobile information between the local area online, by disassembling it as a packet and transmits according to some protocol. We believe should be used to represent these data packets, and these packets are the core of the program.

Suppose only one transport protocol you handle, there is only one package. Maybe you have heard of other protocols and packet types, but they have never supported them, nor can they support them in the future. Will you design a abstract class for the packet (for the concert tryet represents, designing a physical class you actually use? If you do this, you can add new packets in the future without changing the base class. This allows you to add new packet types when programs do not need to recompile. But this design requires two classes, and you only need one now (for the special packet type you are using now). Is this worth it, increase the complexity of the design to allow expansion characteristics, and this expansion may never happen? There is no correct choice, but experience shows that the excellent class is almost impossible for our prototype design. If you design an abstract class for your packet, how do you guarantee it correct, especially in your experience, is only limited to this only packet type? Remember, you can get benefits from the abstract class from your packets only when your design class can be inherited from it from it without any modifications. (If it needs to be modified, you have to recompile all the code that uses the packet class, you have not got any benefits.)

It is not easy to design a receipt of abstract design package, unless you are proficient in the differences between various packets and their corresponding environment. Given your limited experience, I recommend not defining abstract classes, waiting until it will be added from the physical classes.

What I said is a method of judging whether an abstract class is needed, but not only the way. There are still many other good ways; talking about object-oriented books is full of such methods. "When the discovery requirement is derived from one entity class, this is not the only place to introduce an abstract class. Anyway, it is necessary to link two entities through the public inheritance, usually referred to as a new abstract class.

This situation is so common, so it has caused our deep thinking. Third-party C class libraries are more and more, when you find that you need a new entity class from the entity class in the class library, and this library you only have only read rights, what do you do?

You can't modify the class library to join a new abstract class, so your choice will be very limited, very boring:

* From the existing entity class, your entity class is derived, and tolerate our assignment issues at this ITEM. You should also pay attention to the array questions in Item M3.

* Attempting to find an abstract class that completed the most of the features you need, inheriting from it. Of course, there may be no suitable classes; even if you have, you may have to repeat things that have been implemented in the entity class (you try to extension).

* Use the way you try to inherit the class of the class to achieve your new class (see Item E40 and Item E42). In the case of the example, you use the objects of the classes in a class library as a data member and focus on your interface in your class:

Class window {// this is the library class

PUBLIC:

Virtual Void Resize (int newwidth, int newheight);

Virtual void repaint () const;

Int width () const;

INT height () const;

}

Class SpecialWindow {// this is the class you

Public: // wanded to have inherit

... // from Window // Pass-Through Implementations Of Nonvirtual Functions

Int width () const {return w.width ();

INT height () const {return w.height ();

// new importations of "inherited" Virtual Functions

Virtual Void Resize (int newwidth, int newheight);

Virtual void repaint () const;

Private:

WINDOW W;

}

This method requires you to update your own class when you upgrade each time the class library. It also needs you to give up the ability to redefine the virtual functions of the classes in the class library, because you are not inherited.

* Use you get it. Use classes in the class library to modify your own program. Use non-member functions to provide extension (those you want to join that class). As a result, the program will not be as clear, efficient, maintained, and scalable, but at least it has completed the features you need.

These choices are not very attractive, so you have to make a judgment and choose the lightest poison. This is not interesting, but life is sometimes the case. Want to make things easier to you (and our others), feedback to the class library manufacturer. Relying on luck (and a large number of user feedback), the design may be improved over time.

Finally, the general rules are: non-end classes should be an abstract class. When processed outside the class library, you may need to violate this rule; but for the code you can control, comply with it can improve the programs, robust, readability, scalability.

转载请注明原文地址:https://www.9cbs.com/read-26732.html

New Post(0)