From Mechanism to Method: Total Ellipse

Good class design is about effective separation of concerns, which can be accomplished only if we understand the kinds of relationships that are possible between classes. The well-known "is-a" relationship between a derived class and its ancestor(s) describes but one form of "substitutability," and a rather crude one at that. Henney uses the classic taxonomy of shapes (a circle is a specialization of an ellipse, etc.) to demonstrate this point. In particular, things get interesting (and problematic) when mutability enters the picture. As Henney deftly shows, changeability changes everything.


March 01, 2001
URL:http://www.drdobbs.com/from-mechanism-to-method-total-ellipse/184403771

March 2001 C++ Experts Forum/From Mechanism to Method


The interface to an object indicates what you can do with it, and what you can do with an object is a matter of design. C++ supports different kinds of interfaces, from the explicit compileable interface of a class to the more implicit requirements-based interfaces associated with the STL. Interfaces can be named and organized in terms of one another, again a matter of design.

The most common form of interface relationship is that of substitutability, which, in its popular form, is associated with the good use of inheritance — if class B is not a kind of a class A, B should probably not publicly inherit from A. More generally, one type is said to be substitutable for another if the second satisfies the interface of the first and can be used in the same context. Satisfaction is a matter of direct realization, where all the features expected of an interface are provided, or specialization, where new features may be added, existing features constrained, or both.

Not all specialization is inheritance and not all substitutability is inheritance based [1, 2, 3, 4]. Substitutability acts as a more general guideline for the use of inheritance, conversions, operator overloading, templates, and const-volatile qualification.

Substitutability is relative to a given context or use. Normally the context and use are implied, so that statements about substitutability come out as absolutes. For example, when a developer says that instances of class B may be used where A is expected, this is normally a shorthand for saying that pointers or references to B may be used where pointers or references to A are expected. It is unlikely that copy and slice were intended, especially if A is an abstract class.

The idea that a const-qualified type is effectively a supertype of a non-const-qualified type was introduced in my last column [4]. Each ordinary class can be considered — with respect to const qualification — to have two interfaces, one of which is a specialization of the other. This interesting and different perspective can influence how you go about the design of an individual class, but does it have any other practical and tangible consequences? A single class can support multiple interfaces, but by the same token you can split a class into multiple classes according to interfaces. Consideration of mutability does not simply allow us a rationale for adorning our member functions with const-volatile qualifiers; it can also allow us to separate a single class concept into base and derived parts.

Ellipsing the Circle

Modeling the relationship and similarity between circles and ellipses (or squares and rectangles, or real numbers and complex numbers) is a recurring question in books, articles, and newsgroups [5, 6, 7]. It pops up with a regularity you could almost set your system clock by. Some believe it to be a problem with inheritance — or indeed object orientation as a whole — whereas others believe it is one of semantics [5]. But I'm getting ahead of myself: "A circle is a kind of ellipse, therefore circle inherits from ellipse." Let's start at the (apparent) beginning.

Redundant State

A direct transliteration of that English statement gives us the following C++:

class ellipse
{
    ...
};

class circle : public ellipse
{
    ...
};

No problems so far. Depending on your sensibilities, you will next focus on either the public interface or the private implementation. Let's do this back-to-front and focus on the representation first, just to get one of the common problems out of the way. To keep things simple, let's focus only on the ellipse axes:

class ellipse
{
    ...
private:
    double a, b;
};

class circle : public ellipse
{
    ...
};

An ellipse's shape can be characterized by its semi-major (a) and semi-minor axes (b), whereas a circle needs only its radius to describe it. This means that there is no need to add any additional state representation in circle as everything that it needs has already been defined in ellipse. So what's the problem? Before you think it, the private specifier does not need to be protectedthat would be a problem. The problem is that not only does circle not need any additional state, it actually has too much state: a == b is an assertable invariant of the circle, which means that there is redundant data. It is neither possible to uninherit features (a rock) nor desirable to maintain redundant state (a hard place).

Focusing on inheritance of representation alone could lead you to the topsy-turvy view that ellipse should inherit from circle because it would add to its state. Although bizarre, this view is entertained surprisingly often, (but is entertaining only if the code is not yours to work with). From bad to worse, there would not only be the absence of any concept of substitutability whatsoever, but the circle would also have the wrong state to inherit in the first place: ellipse would not just inherit a double, it would specifically inherit a radius. Now, is the radius the semi-major or the semi-minor axis? The proper conclusion of a representation-focused approach should be that circle and ellipse should not share an inheritance relationship at all. At least this conservative view is safe and free from strange consequences. For some systems, it may also prove to be the right choice in practice: Premature generalization often leads to unnecessary coding effort in creating options that are never exercised.

Interface Classes

Let's focus on the interfaces instead. How can you represent the interface for using instances of a class without actually representing the implementation? In particular, how can you do this if you have related but separate types of objects, e.g., circle versus ellipse? Abstract base classes are often thought of as a mechanism for factoring out common features — interface or implementation, but typically a bit of both — in a class hierarchy. Their most common use is as partially implemented classes, but it turns out that their most powerful use is as pure interfaces.

Interface classes are an idiom rather than a language feature, a method for using a mechanism:

Writing a class that actually does nothing seems counter-intuitive, if not a little uncomfortable, for many developers. However, having a pure representation of interface gives the developer two clear benefits:

It turns out that these two benefits can be summarized more generally as one: separation of concerns.

Returning to our shapes, we can start establishing the usage for ellipses and circles without getting lost in representation:

class ellipse
{
public:
    virtual double semi_major_axis() const = 0;
    virtual double semi_minor_axis() const = 0;
    virtual double area() const = 0;
    virtual double eccentricity() const = 0;
    ...
};

class circle : public ellipse
{
public:
    virtual double radius() const = 0;
    ...
};

Concrete Leaves

The separation of interface from implementation class makes things much clearer. Now, when we come to provide sample implementations, we can see that there is little to be gained from trying to share implementation detail:

class concrete_ellipse : public ellipse
{
public:
    ellipse(double semi_major, double semi_minor)
      : a(semi_major), b(semi_minor)
    {
    }
    virtual double semi_major_axis() const
    {
        return a;
    }
    virtual double semi_minor_axis() const
    {
        return b;
    }
    virtual double area() const
    {
        return pi * a * b;
    }
    virtual double eccentricity() const
    {
        return sqrt(1 - (b * b) / (a * a));
    }
    ...
private:
    double a, b;
};

class concrete_circle : public circle
{
public:
    explicit circle(double radius)
      : r(radius)
    {
    }
    virtual double radius() const
    {
        return r;
    }
    virtual double semi_major_axis() const
    {
        return r;
    }
    virtual double semi_minor_axis() const
    {
        return r;
    }
    virtual double area() const
    {
        return pi * r * r;
    }
    virtual double eccentricity() const
    {
        return 0;
    }
    ...
private:
    double r;
};

Should you wish to share implementation, you can turn to other techniques to do so, e.g., the Bridge pattern [8], but the choice of representation is kept clear from the presentation of interface.

The hierarchy shown here has two very distinct parts to it: the classes that represent usage and the classes that represent implementation. Put another way, the classes exist either for interface, e.g.:

void draw(point, const ellipse &);

or for creation, e.g.:

concrete_circle unit(1);
draw(origin, unit);

In the class hierarchy, only the leaves are concrete, and the rest of the hierarchy is abstract — in this case, fully abstract. This design recommendation to "make non-leaf classes abstract" [9] can also be stated as "never inherit from concrete classes." It often helps to simplify subtle class hierarchies by teasing apart the classes according to the distinct roles that they play.

Implementation-Only Classes

If you wish to take decoupling a step further, the separation of concerns can be further reinforced by introducing the polar opposite of an interface class: an implementation-only class. This idiom allows developers to define classes that can only be used for object creation; subsequent object manipulation must be through interface classes [10]:

This idiom takes advantage of a feature of C++ that is often seen as a quirk: access specification and virtual function mechanisms are orthogonal. Here are alternative definitions for concrete circles and ellipses:

class creation_only_ellipse : public ellipse
{
public:
    creation_only_ellipse(double semi_major, double semi_minor);
    ...
private:
    virtual double semi_major_axis() const;
    virtual double semi_minor_axis() const;
    virtual double area() const;
    virtual double eccentricity() const;
    ...
    double a, b;
};

class creation_only_circle : public circle
{
public:
    explicit creation_only_circle(double radius);
    ...
private:
    virtual double radius() const;
    virtual double semi_major_axis() const;
    virtual double semi_minor_axis() const;
    virtual double area() const;
    virtual double eccentricity() const;
    ...
    double r;
};

In use, we could see a circle being created and used as follows:

circle *unit = new creation_only_circle(1);

Change Happens

Management summary of the story so far: circles substitutable for ellipses, some useful techniques, and no real problems. However, good as these techniques are in many designs, they have not resolved the challenge that really makes the circle-ellipse problem as infamous as it is. If you look back, or if you are already familiar with the problem, you will notice that there has been a little sleight of hand in the interfaces presented: The only ordinary member functions are const qualified. In other words, there are no modifiers. You cannot resize circles or stretch ellipses.

Let's introduce setters corresponding to the axis getters:

class ellipse
{
public:
    virtual void semi_major_axis(double) = 0;
    virtual void semi_minor_axis(double) = 0;
    ... // as before
};

class circle : public ellipse
{
public:
    virtual void radius(double) = 0;
    ... //as before
};

It is fairly obvious what an actual circle does for radius and an actual ellipse does for semi_minor_axis and semi_major_axis, but what does a circle do for the semi_minor_axis and semi_major_axis setters that it has inherited?

class concrete_ellipse : public ellipse
{
public:
    virtual void semi_major_axis(double new_a)
    {
        a = new_a;
    }
    virtual double semi_minor_axis(double new_b)
    {
        b = new_b;
    }
    ...
private:
    double a, b;
};

class concrete_circle : public circle
{
public:
    virtual void radius(double new_r)
    {
        r = new_r;
    }
    virtual void semi_major_axis(double new_a)
    {
        ... // what goes here?
    }
    virtual double semi_minor_axis(double new_b)
    {
        ... // what goes here?
    }
    ...
private:
    double r;
};

This is the issue that gets people excited, producing all kinds of fabulous suggestions, all of which violate substitutability in some way:

None of these are reasonable. Distinct unrelated classes are preferable to any of them.

No Change

When you find yourself at the bottom of a hole, stop digging. The design we developed moved forward in stable, well-defined steps, and only became unstuck with a particular requirement.... Wait a minute, resizing circles and ellipses was never a requirement. In fact, we have no requirements or real statement of the original problem domain. So, we're in the middle of a design that does not work, and we have no requirements; do we blame the design, the paradigm, or what? "A circle is a kind of ellipse" was about as much analysis as we — or indeed most people looking at the problem — set out with. We have not stated the purpose or context of our system: Design is context dependent, so if you remove the context you cannot have meaningful design.

Circles and ellipses also seem such a simple, familiar, and intuitive domain that we all feel like domain experts. However, intuition has misled us: When you were taught conic sections at school, were you ever taught that you could that you could resize them? If you transform an ellipse by stretching it, you have a different ellipse; if you resize a circle, you have a different circle. In other words, if we chose to model our classes after the strict mathematical model, we would not evolve the design beyond the const-member-function-only version, although we might wish to add factory functions to accommodate the transformations. That's it. What violated substitutability was the introduction of change: eliminate it and you have a model that is both accurate and works [11].

Now that we understand the real root of the problem — substitutability with respect to change — we can consider alternative approaches that accommodate both change and inheritance-based substitutability. If we do not need both of those features, we can choose to disallow modifiers or not to relate the classes by inheritance, as necessary.

Change Is the Concern

Therefore, separate according to change. Here is a more useful interpretation of the original statement: "a circle is a kind of ellipse, so long as you don't change it as an ellipse." This is perhaps more constrained than is strictly required, i.e., geometrically resizing an ellipse on both of its axes is also safe for circles, but the constraint makes the issues and their solution clearer.

Given that statement, what we want is something like the following:

class ellipse
{
    ...
};

class circle : public const ellipse // not legal C++
{
    ...
};

Other than that one minor nit — that the code isn't legal C++ — we have a solution that allows circles to be substituted for ellipses where they cannot be changed. Where a function expects to change an ellipse object via reference or pointer, a circle would not work, but references and pointers to const ellipse would allow circle objects to be passed.

However, the good news is that the wished-for version does give us a hint as to how we might organize our classes and functions. Factoring the const member functions into a separate interface class gives the following:

class const_ellipse
{
public:
    virtual double semi_major_axis() const = 0;
    virtual double semi_minor_axis() const = 0;
    virtual double area() const = 0;
    virtual double eccentricity() const = 0;
    ...
};

class ellipse : public const_ellipse
{
public:
    using const_ellipse::semi_major_axis;
    using const_ellipse::semi_minor_axis;
    virtual void semi_major_axis(double) = 0;
    virtual void semi_minor_axis(double) = 0;
    ...
};

class const_circle : public const_ellipse
{
public:
    virtual double radius() const = 0;
    ...
};

class circle : public const_circle
{
public:
    using const_circle::radius;
    virtual void radius(double) = 0;
    ...
};

For consistency with the standard library's naming conventions for iterators, const_ellipse and ellipse are named rather than ellipse and mutable_ellipse, but your mileage may vary. For consistency within the same design, circle is also split into a const_circle and a circle class.

Conclusion

Substitutability is not always about "is a kind of," and not all forms of specialization fit an inheritance hierarchy out-of-the-box. Natural language is often not a very good guide at this level, so we often need more phrasing options to reveal the useful relationships in our system, such as "implements" and "can be used where a ... is expected."

There are many criteria for partitioning classes into base and derived parts. Taxonomic classification is the most familiar to OO developers, but we have also seen that interface-implementation separation is compelling and that separation with respect to mutability also has a place.

References

[1] Kevlin Henney. "From Mechanism to Method: Substitutability," C++ Report, May 2000, also available from http://www.curbralan.com.

[2] Kevlin Henney. "From Mechanism to Method: Valued Conversions," C++ Report, May 2000, also available from http://www.curbralan.com.

[3] Kevlin Henney. "From Mechanism to Method: Function Follows Form," C/C++ Users Journal C++ Experts Forum, November 2000, http://www.cuj.com/experts/1811/henney.html.

[4] Kevlin Henney. "From Mechanism to Method: Good Qualifications," C/C++ Users Journal C++ Experts Forum, January 2001, http://www.cuj.com/experts/1901/henney.html.

[5] Marshall Cline, Greg Lomow, and Mike Girou. C++ FAQs, 2nd edition (Addison-Wesley, 1999).

[6] James O. Coplien. Advanced C++: Programming Styles and Idioms (Addison-Wesley, 1992).

[7] James O. Coplien. Multi-Paradigm Design for C++ (Addison-Wesley, 1999).

[8] Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides. Design Patterns: Elements of Reusable Object-Oriented Software (Addison-Wesley, 1995).

[9] Scott Meyers. More Effective C++: 35 New Ways to Improve Your Programs and Designs (Addison-Wesley, 1996).

[10] John J. Barton and Lee R. Nackman. Scientific and Engineering C++: An Introduction with Advanced Techniques and Examples (Addison-Wesley, 1994).

[11] Kevlin Henney. "Vicious Circles," Overload 8, June 1995.

Kevlin Henney is an independent consultant and trainer specializing in C++, Java, OO design, patterns, and software architecture. He can be reached at [email protected]

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.