Item 35: consider alternative methods for optional virtual functions
By Scott Meyers
Translator: fatalerror99 (itepub's nirvana)
Release: http://blog.csdn.net/fatalerror99/
Now you are working on a video game, and you have designed a hierarchy (inheritance system) for the role in the game ). Your game has a variety of harsh environments, and it is not uncommon for a role to be hurt or other health statuses to degrade. Therefore, you decide to provide a member function (member function) healthvalue, which returns an integer indicating the role's health. Because different roles may have different methods for calculating health values, declaring healthvalue as virtual seems to be an obvious design choice:
Class gamecharacter {
Public:
Virtual int healthvalue () const; // Return character's health rating;
... // Derived classes may redefine this
};
The fact that healthvalue is not declared as pure virtual implies that there is a default Algorithm for calculating the health value (see item 34 ).
This is indeed an obvious design choice, and in a sense, it is its shortcoming. Because this design is too obvious, you may not pay enough attention to other optional methods. To help you break away from the habitual path of object-oriented design, we will consider other methods to solve this problem.
The template method pattern via the non-virtual interface idiom (the template method mode is implemented through non-Virtual Interface Usage)
We started with an interesting idea that virtual functions should almost always be private. Advocates of this view suggested that a better design should retain healthvalue as a public member function (Public member function), but should change it to non-virtual (non-virtual) and let it call a private virtual function to do the real work, that is, dohealthvalue:
Class gamecharacter {
Public:
Int healthvalue () const// Derived classes do not redefine
{// This-see item 36
... // Do "before" stuff-see below
Int retval = dohealthvalue (); // do the real work
... // Do "after" stuff-see below
Return retval;
}
...
PRIVATE:
Virtual int dohealthvalue () const// Derived classes may redefine this
{
... // Default Algorithm for calculating
} // Character's health
};
In this Code (and other code of this item), I will display the ontology of member functions (member function) in the class definition. As described in item 30, this implicitly declares them as inline (Inline ). I use this method to demonstrate the code just so that it is easier to see what it is doing. The design I described has nothing to do with inline, so you don't have to delve into the meaningful meaning of the member functions (member function) definition within the class. No.
This basic design allows customers to call private virtual functions (private virtual functions) through public non-Virtual Member funber (Public Non-virtual member functions ).Non-Virtual Interface (nvi) Idiom(Non-Virtual Interface Usage ). This is a more general form of design pattern called template method (a pattern, unfortunately, not associated with the c ++ templates (Template. I call the non-virtual function (non-virtual function) (for example, healthvalue) virtual function'sWrapper(Shell of the virtual function ).
One advantage of nvi idiom is identified in the Code through comments "Do 'before' stuff" and "Do 'after' stuff. The code snippets marked with these annotations are called before or after the virtual function that performs real work. This means that the wrapper (Shell) can ensure that the specific background environment is set before the virtual function (virtual function) is called, and the background environment is cleared after the call ends. For example, "before" stuff can include locking a mutex (mutex), generating a log entry, verifying whether the preconditions of class variables and functions are met, and so on. "After" stuff can include unlocking a mutex (mutex), verifying the postconditions (termination condition) of the function, and recovering class constants. If you ask the customer to directly call virtual functions, there is really no good way to do this.
Involves Derived classes (derived classes) to redefine private virtual functions (private virtual functions) (these udfs cannot be called !) Nvi idiom may disturb your mind. There is no design conflict here. Redefine a virtual function to specify how to do something. Call a virtual function to specify when to do it. There is no relationship between them. Nvi idiom allows Derived classes (derived class) to redefine a virtual function, which gives them the ability to control how functions are implemented, but the base class (base class) reserves the right to determine when a function is called. At first glance, it is strange, but C ++ requires that derived classes (derived classes) can redefine private inherited virtual functions (Private inherited virtual functions.
Under nvi idiom, virtual functions (virtual functions) become private (private) is not absolutely necessary. In some class hierarchies (class inheritance system), the derived class (derived class) implementation of a virtual function is expected to call its base class (base class) (For example, the 120th page example). To make such a call legal, the virtual must be protected, rather than private ). Sometimes a virtual function (virtual function) must even be public (public) (for example, Destructors (destructor) in polymorphic base classes (polymorphism base class)-See item 7 ), in this way, nvi idiom cannot be applied.
The Strategy pattern via function pointers (Policy Mode Implemented by function pointers)
Nvi idiom is an interesting alternative to public virtual functions (public virtual function), but from the design point of view, it is not much more than the decoration door. After all, we are still using virtual functions to calculate the health value of each role. A more striking design claims that computing a role's health value does not depend on the role's type-such computing does not need to be part of the role at all. For example, we may need to pass a pointer to the health value calculation function for each role's constructor (constructor), and we can call this function for actual calculation:
Class gamecharacter; // Forward Declaration
// Function for the default health calculation algorithm
Int defaulthealthcalc (const gamecharacter & GC );
Class gamecharacter {
Public:
Typedef int (* healthcalcfunc) (const gamecharacter &);
Explicit gamecharacter (Healthcalcfunc HCF = defaulthealthcalc)
: Healthfunc (HCF)
{}
Int healthvalue () const
{ReturnHealthfunc (* This);}
...
PRIVATE:
Healthcalcfunc healthfunc;
};
This method is another simple application of strategy, a general design pattern. Compared with the virtual functions (virtual functions) based on gamecharacter hierarchy, it provides some more appealing mobility:
- Different instances of the same role type can have different health value calculation functions. For example:
Class evilbadguy: Public gamecharacter {
Public:
Explicit evilbadguy (healthcalcfunc HCF = defaulthealthcalc)
: Gamecharacter (HCF)
{...}
...
};
Int losehealthquickly (const gamecharacter &); // health Calculation
Int losehealthslowly (const gamecharacter &); // funcs with different
// Behavior
EvilbadguyEbg1(Losehealthquickly); // same-type charac-
EvilbadguyEbg2(Losehealthslowly); // ters with different
// Health-related
// Behavior
- The computing function of a specified role's health value can be changed at runtime. For example, gamecharacter can provide a member function (member function) sethealthcalculator, which is allowed to replace the current health value calculation function.
On the other hand, the health value calculation function is no longer a member function (member function) Fact of gamecharacter hierarchy, this means that it no longer has the privilege to access the internal component of the object it calculates. For example, defaulthealthcalc cannot access the Non-Public (non-public) component of evilbadguy. If a role's health value is calculated based entirely on the information that can be obtained through the role's public interface (public interface), there is no problem. However, if you need non-public (non-public) information for accurate health value calculation, the problem may occur. In fact, in any equivalent function that you want to use outside class (for example, through a non-member non-friend function (non-member non-friend function) or use the non-friend member function (non-member function) of another class to replace the internal function of the class (class) (for example, through a member function (member function )) it is a potential problem. This problem will continue to affect the rest of this item, because all other design options we will consider include the use of external functions of gamecharacter hierarchy (inheritance system.
As a general rule, the non-public (non-public) the only method required for component access is to weaken the class's encapsulation (encapsulation ). For example, a class can declare a non-member functions (non-member function) as a friend or, it provides public accessor functions (a public visitor function) for "the public accessor functions that it wants to hide itself in other cases ). The advantage of using a function pointer instead of a virtual function (for example, whether it can offset the possible encapsulation (encapsulation) Reduction of gamecharacter by the ability to compute functions based on object health values and change such functions at runtime) the need is an important part of the decision you must make at design.
(Click here, next)