December 2010

Volume 25 Number 12

The Working Programmer - Multiparadigmatic .NET, Part 4: Object Orientation

By Ted Neward | December 2010

Ted NewardIn the previous article, we explored commonalities and variability as expressed through procedural programming, and discovered several interesting “sliders” by which variability can be introduced into designs. In particular, two design approaches emerged out of the procedural line of thought: name-and-behavior variability, and algorithm variability.

As the complexity of programs and their requirements grew, developers found themselves struggling to keep all of the various subsystems straight. Procedural abstractions, we discovered, didn’t “scale” as well as we might’ve hoped. With the advent of the GUI, a new style of programming began to emerge, one that many readers who learned the “plain old SDK” style of building Windows apps from the Windows 3 SDK and Charles Petzold’s classic “Programming Windows” (Microsoft Press, 1998) will recognize instantly. Ostensibly procedural in nature, this style followed a particularly interesting pattern. Each procedure in a closely clustered knot of related functionality centered around a “handle” parameter, most often taking it as a first (or only) parameter, or returning it from a Create call or the like: CreateWindow, FindWindow, ShowWindow and more, all centered around a window handle (HWND), for example.

What developers didn’t realize at the time was that this was actually a new way of programming, a new paradigm that would make itself felt within just a few years. Hindsight, of course, makes it obvious that this was object-oriented programming, and most readers of this column will be well-versed with its precepts and ideas already. Given that, why would we decide to spend precious column inches on the subject? The answer is that no discussion of multiparadigm design would be complete without incorporating objects within its purview.

Object Fundamentals

Object orientation is, in many ways, an exercise in inheritance. Implementation inheritance has dominated much of the object design discussion, with advocates suggesting that proper abstractions are built by identifying the entities in the system—the “nouns,” as it were—and where commonality emerges, elevating that commonality into a base class, thus creating an “IS-A” relationship. A Student IS-A Person, an Instructor IS-A Person, a Person IS-A Object and so on. Inheritance thus gave developers a new axis on which to analyze commonality and variability.

In the days of C++, the implementation-inheritance approach stood alone, but as time and experience progressed, interface inheritance emerged as an alternative. In essence, the introduction of interface inheritance into the designer’s toolbox allowed for a lighter-weight inheritance relationship, declaring that a type IS-A different type, but without the behavior or structure of the parent type. Interfaces thus provide a mechanism for “grouping” types along an inheritance axis without enforcing any particular restrictions on their implementation.

Consider, for example, the canonical object-oriented example, that of a hierarchy of geometric shapes that can be drawn (if only figuratively) to screen:

class Rectangle
{
  public int Height { get; set; }
  public int Width { get; set; }
  public void Draw() { Console.WriteLine("Rectangle: {0}x{1}", Height, Width); }
}
class Circle
{
  public int Radius { get; set; }
  public void Draw() { Console.WriteLine("Circle: {0}r", Radius); }
}

The commonality between the classes suggests that a superclass is in order here, to avoid repeating that commonality in every drawable geometric shape:

abstract class Shape
{
  public abstract void Draw();
}
  
class Rectangle : Shape
{
  public int Height { get; set; }
  public int Width { get; set; }
  public override void Draw() { 
    Console.WriteLine("Rectangle: {0}x{1}", Height, Width); }
}
class Circle : Shape
{
  public int Radius { get; set; }
  public override void Draw() { Console.WriteLine("Circle: {0}r", Radius); }
  }

So far, so good—most developers would take no issue with what’s been done thus far. Unfortunately, a problem lies in wait for the unwary.

Liskov Rides Again

The catch here is known as the Liskov Substitution Principle: any type that inherits from another must be completely substitutable for that other. Or, to use the words that originally described the principle, “Let q(x) be a property provable about objects x of type T. Then q(y) should be true for objects y of type S where S is a subtype of T.”

What that means in practice is that any particular derivation of Rectangle, such as a Square class, must ensure that it obeys the same behavioral guarantees provided by the base. Because a Square is essentially a Rectangle with the guarantee that both the Height and Width are always the same, it seems reasonable to write Square like the example in Figure 1.

Figure 1 Deriving a Square

class Rectangle : Shape
{
  public virtual int Height { get; set; }
  public virtual int Width { get; set; }
  public override void Draw() { 
    Console.WriteLine("Rectangle: {0}x{1}", Height, Width); }
}
class Square : Rectangle
{
  private int height;
  private int width;
  public override int Height { 
    get { return height; } 
    set { Height = value; Width = Height; } 
  }
  public override int Width {
    get { return width; }
    set { Width = value; Height = Width; }
  }
}

Notice how Height and Width properties are now virtual so as to avoid any kind of accidental shadowing or slicing behavior when overriding them in the Square class. So far so good.

Next, a Square gets passed in to a method that takes a Rectangle and “grows” it (what graphics geeks sometimes call a “transform”):

class Program
{
  static void Grow(Rectangle r)
  {
    r.Width = r.Width + 1;
    r.Height = r.Height + 1;
  }
  static void Main(string[] args)
  {
    Square s = new Square();
    s.Draw();
    Grow(s);
    s.Draw();
  }
}

Figure 2 shows the net result of calling this code, which is not what you might expect.

Figure 2 A Surprising Result with Grow Code

Figure 2 A Surprising Result with Grow Code

The problem here, as careful readers may have already surmised, is that each property implementation assumes it’s being called in isolation, and thus has to act independently to ensure the Height==Width constraint around Square at all times. The Grow code, however, assumes that a Rectangle is being passed in, and remains entirely ignorant of the fact that it’s a Square coming in (as intended!), and acts in a manner entirely appropriate for Rectangles.

The core of the problem? Squares aren’t rectangles. They have a lot of similarity, granted, but at the end of the day, the constraints of a square don’t hold for rectangles (which is also true, by the way, for ellipses and circles), and trying to model one in terms of the other is based on a fallacy. It’s tempting to inherit Square from Rectangle, particularly because it lets us reuse some code, but it’s a false premise. In fact, I’ll even go so far as to suggest that one should never use inheritance to foster reuse until the Liskov Substitution Principle for those two types has been proven to be true.

This example isn’t new—Robert “Uncle Bob” Martin (bit.ly/4F2R6t) discussed Liskov and this exact example back in the mid-90s when talking to C++ developers. Some problems like this can be solved partly by using interfaces to describe the relationships, but that doesn’t help this particular case, because Height and Width remain separate properties.

Is there a solution in this case? Not really, not while keeping the Square-as-derived-from-Rectangle relationship in place. The best answer comes from making Square a direct descendent of Shape, and abandoning the inheritance approach entirely:

 

class Square : Shape
{
  public int Edge { get; set; }
    public override void Draw() { Console.WriteLine("Square: {0}x{1}", Edge, Edge); }
}
class Program
{
    static void Main(string[] args)
    {
      Square s = new Square() { Edge = 2 };
      s.Draw();
      Grow(s);
      s.Draw();
    }
}

Of course, now we have the problem that Square can’t be passed in to Grow at all, and it does seem like there’s a potential code-reuse relationship there. We can solve this in one respect by providing a view of the Square as a Rectangle using a conversion operation, as shown in Figure 3.

Figure 3 Conversion Operation

class Square : Shape
{
  public int Edge { get; set; }
  public Rectangle AsRectangle() { 
    return new Rectangle { Height = this.Edge, Width = this.Edge }; 
  }
  public override void Draw() { Console.WriteLine("Square: {0}x{1}", Edge, Edge); }
}
class Program
{
  static void Grow(Rectangle r)
  {
    r.Width = r.Width + 1;
    r.Height = r.Height + 1;
  }
  static void Main(string[] args)
  {
    Square s = new Square() { Edge = 2 };
    s.Draw();
    Grow(s.AsRectangle());
    s.Draw();
  }
}

It works—but it’s a bit awkward. We might also use the C# conversion operator facility to make it easier to convert Squares to Rectangles, as shown in Figure 4.

Figure 4 The C# Conversion Operator Facility

class Square : Shape
{
  public int Edge { get; set; }
  public static implicit operator Rectangle(Square s) { 
    return new Rectangle { Height = s.Edge, Width = s.Edge }; 
  }
  public override void Draw() { Console.WriteLine("Square: {0}x{1}", Edge, Edge); }
}
class Program
{
  static void Grow(Rectangle r)
  {
    r.Width = r.Width + 1;
    r.Height = r.Height + 1;
  }
  static void Main(string[] args)
  {
    Square s = new Square() { Edge = 2 };
    s.Draw();
    Grow(s);
    s.Draw();
  }
}

This approach, while perhaps strikingly different from what’s expected, offers the same client perspective as before, but without the problems of the earlier implementation, as Figure 5 shows.

Figure 5 The Result of Using the C# Conversion Operator Facility

Figure 5  The Result of Using the C# Conversion Operator Facility

In fact, we have a different problem—where before the Grow method modified the Rectangle being passed in, now it appears that it’s doing nothing, largely because it’s modifying a copy of the Square, not the original Square itself. We could fix this by having the conversion operator return a new subclass of Rectangle that holds a secret reference back to this Square instance, so that modifications to the Height and Width properties will in turn come back and modify the Square’s Edge ... but then we’re back to the original problem!

No Happy Ending Here

In Hollywood, movies have to end in a fashion commensurate with audience expectations or face being rejected at the box office. I’m not a moviemaker, so I feel no compulsion to present the readers of this column with a happy ending in every case. This is one of those cases: trying to keep the original code in place and make it all work just creates deeper and deeper hacks. Solutions might be to move the Grow or Transform method directly on to the Shape hierarchy or just make the Grow method return the modified object rather than modify the object passed in (which is something we’ll talk about in another column), but in short, we can’t keep the original code in place and keep everything working correctly.

All of this is designed to showcase precisely one thing: object-oriented developers are comfortable with modeling commonality and variability with inheritance, perhaps too much so. Remember, if you choose to use the inheritance axis to capture commonality, you have to ensure this commonality holds across the entire hierarchy if subtle bugs like this are to be avoided.

Remember, too, that inheritance is always a positive variability (adding new fields or behaviors), and that modeling negative variability in inheritance (which is what Square tried to do) is almost always a recipe for disaster along Liskovian lines. Ensure that all inheritance-based relationships involve a positive commonality, and things should be good. Happy coding!


Ted Neward* is a principal with Neward & Associates, an independent firm specializing in enterprise .NET Framework and Java platform systems. He’s written more than 100 articles, is a C# MVP and INETA speaker and has authored and coauthored a dozen books, including “Professional F# 2.0” (Wrox, 2010). He also consults and mentors regularly. Reach him at ted@tedneward.com with questions or consulting requests, and read his blog at blogs.tedneward.com.*

Thanks to the following technical expert for reviewing this article: Anthony Green