I l@ve RuBoard |
![]() ![]() |
1.12 Decomposing a Class into PoliciesThe hardest part of creating policy-based class design is to correctly decompose the functionality of a class in policies. The rule of thumb is to identify and name the design decisions that take part in a class's behavior. Anything that can be done in more than one way should be identified and migrated from the class to a policy. Don't forget: Design constraints buried in a class's design are as bad as magic constants buried in code. For example, consider a WidgetManager class. If WidgetManager creates new Widget objects internally, creation should be deferred to a policy. If WidgetManager stores a collection of Widgets, it makes sense to make that collection a storage policy, unless there is a strong preference for a specific storage mechanism. At an extreme, a host class is totally depleted of any intrinsic policy. It delegates all design decisions and constraints to policies. Such a host class is a shell over a collection of policies and deals only with assembling the policies into a coherent behavior. The disadvantage of an overly generic host class is the abundance of template parameters. In practice, it is awkward to work with more than four to six template parameters. Still, they justify their presence if the host class offers complex, useful functionality. Type definitions—typedef statements—are an essential tool in using classes that rely on policies. Using typedef is not merely a matter of convenience; it ensures ordered use and easy maintenance. For example, consider the following type definition: typedef SmartPtr < Widget, RefCounted, NoChecked > WidgetPtr; It would be very tedious to use the lengthy specialization of SmartPtr instead of WidgetPtr in code. But the tediousness of writing code is nothing compared with the major problems in understanding and maintaining that code. As the design evolves, WidgetPtr's definition might change—for example, to use a checking policy other than NoChecked in debug builds. It is essential that all the code use WidgetPtr instead of a hardcoded instantiation of SmartPtr. It's just like the difference between calling a function and writing the equivalent inline code: The inline code technically does the same thing but fails to build an abstraction behind it. When you decompose a class in policies, it is very important to find an orthogonal decomposition. An orthogonal decomposition yields policies that are completely independent of each other. You can easily spot a nonorthogonal decomposition when various policies need to know about each other. For example, think of an Array policy in a smart pointer. The Array policy is very simple—it dictates whether or not the smart pointer points to an array. The policy can be defined to have a member function T& ElementAt(T* ptr, unsigned int index), plus a similar version for const T. The non-array policy simply does not define an ElementAt member function, so trying to use it would yield a compile-time error. The ElementAt function is an optional enriched behavior as defined in Section 1.6. The implementations of two policy classes that implement the Array policy follow. template <class T> struct IsArray { T& ElementAt(T* ptr, unsigned int index) { return ptr[index]; } const T& ElementAt(T* ptr, unsigned int index) const { return ptr[index]; } }; template <class T> struct IsNotArray {}; The problem is that purpose of the Array policy—specifying whether or not the smart pointer points to an array—interacts unfavorably with another policy: destruction. You must destroy pointers to objects using the delete operator, and destroy pointers to arrays of objects using the delete[] operator. Two policies that do not interact with each other are orthogonal. By this definition, the Array and the Destroy policies are not orthogonal. If you still need to confine the qualities of being an array and of destruction to separate policies, you need to establish a way for the two policies to communicate. You must have the Array policy expose a Boolean constant in addition to a function, and pass that Boolean to the Destroy policy. This complicates and somewhat constrains the design of both the Array and the Destroy policies. Nonorthogonal policies are an imperfection you should strive to avoid. They reduce compile-time type safety and complicate the design of both the host class and the policy classes. If you must use nonorthogonal policies, you can minimize dependencies by passing a policy class as an argument to another policy class's template function. This way you can benefit from the flexibility specific to template-based interfaces. The downside remains that one policy must expose some of its implementation details to other policies. This decreases encapsulation. ![]() |
I l@ve RuBoard |
![]() ![]() |