C# Coding Solutions—Designing Consistent Classes

Microsoft .NET Framework, ASP.NET, Visual C# (CSharp, C Sharp, C-Sharp) Developer Training, Visual Studio


Jump to: navigation, search
CSharp-Online.NET:Articles
C# Articles

C# Coding Solutions

© 2006 Christian Gross

Designing Consistent Classes

There are many reasons bugs can appear in code. One reason could be because the algorithm was implemented incorrectly. Another reason, and the focus of this solution, is that the consistency of a class was violated due to an incorrect method or property call. Immutable classes are consistent because they avoid the problem of having their consistency corrupted by inappropriate manipulations. Classes that can have their properties read and written can be corrupted. To illustrate the problem, this solution outlines how to create an inheritance of shapes, and in particular the square and rectangle. The question whether a square subclasses a rectangle, or a rectangle subclasses a square.

Let’s define a rectangle as a base class, as in the following example:

class Rectangle {
    private long _length, _width;
 
    public Rectangle( long length, long width) {
        _length = length;
        _width = width;
    }
    public virtual long Length {
        get {
            return _length;
        }
        set {
            _length = value;
        }
    }
    public virtual long Width {
        get {
            return _width;
        }
        set {
            _width = value;
        }
    }
}

The class Rectangle has a constructor with two parameters that represent the length and width. There are two properties—Length and Width—that are the length and width of the rectangle, respectively. Like in a typical coding scenario, the properties can be retrieved and assigned, meaning that they are read-write.

Extending this theory, a square is a specialized form of rectangle, where the length and width happen to be equal. Using the previously defined Rectangle as a base class, the class Square could be defined as follows:

class Square : Rectangle {
    public Square( long width) : base( width, width) {
    }
}

To define a square, the class Square only needs to redefine the base class Rectangle constructor to have a single parameter representing the dimensions. The constructor calls the base constructor and passes the same dimension to the length and width. When Square is instantiated, the type will have the correct dimensions. It seems all’s well in our object-oriented design. However, all is not well—there is a major design flaw, which is illustrated in the following test code:

[TestFixture]
public class TestShapes {
    [Test]
    public void TestConsistencyProblems() {
        Square square = newSquare( 10);
        Rectangle squarishRectangle = square;
 
        squarishRectangle.Length = 20;
        Assert.AreNotEqual( square.Length, square.Width);
    }
}

The variable square references a square with length and height of 10 units. The variable square is down cast to the variable squarishRectangle, which is of the type Rectangle. The consistency problem occurs when the squarishRectangle.Length property is assigned a value of 20. Even though it is legal to reassign the property, it is not consistent with the requirements of the type Square. A downcast square should not fail consistency tests. Changing one dimension of the rectangle should have changed the other dimension as well. Assert.AreNotEqual illustrates how the dimensions of the square are inconsistent and therefore we no longer have a square.

Polymorphism has caused consistency violations. One solution for making the Square consistent is the following source code:

class Square : Rectangle {
    public Square( long width) : base( width, width) {
 
    }
    public override long Length {
        get {
            return base.Length;
        }
        set {
            base.Length = value;
            base.Width = value;
        }
    }
    public override long Width {
        get {
            return base.Width;
        }
        set {
            base.Length = value;
            base.Width = value;
        }
    }
}

In the new implementation, Square overrides the properties Length and Width. Changing the rectangle’s length to 20 would have changed the width at the same time. This means that a square will always be a square, and the consistency is upheld. However, there is a price for this consistency, and that is complexity. The complexity is because both Rectangle and Square must implement all methods. So why use inheritance if a square and a rectangle are complete implementations?

Let’s flip things around and define the Rectangle as a type that subclasses Square:

class Square {
    private long _width;
 
    public Square( long width)  {
        _width = width;
    }
    public virtual long Width {
        get {
            return _width;
        }
        set {
            _width = value;
        }
    }
}
 
class Rectangle : Square {
    private long _length;
 
    public Rectangle( long length, long width) : base(width) {
        _length = length;
    }
    public virtual long Length {
        get {
            return _length;
        }
        set {
            _length = value;
        }
    }
}

The base class is Square and it defines the property Width, which represents the length and width of a square. Square does not define a Length property because it is not needed. Rectangle subclasses Square and adds its missing dimension, Length. Now, if a Rectangle is downcast to Square, the square will always have a consistent dimension, albeit the Length and Width of the Rectangle will change. Changing a single dimension does not violate the consistency of the Rectangle. Changing the one dimension does, however, change the state of the Rectangle.

The following test code illustrates how to use Square and Rectangle:

[TestFixture]
public class TestShapes {
    void TestSquare( Square square) {
        square.Width = 10;
    }
    [Test]
    public void TestSubclass() {
        Rectangle rectangle = new Rectangle( 30, 30);
        long oldLength = rectangle.Length;
        TestSquare( rectangle);
        Assert.AreEqual( oldLength, rectangle.Length);
    }
}

In the test code the method TestSubclass instantiates Rectangle. Prior to calling the method TestSquare the length of Rectangle is stored. When the method TestSquare is called the property Width is modified. After the call to TestSquare, the following Assert.AreEqual call tests to ensure that the length of the rectangle equals oldLength. If, on the other hand, the Width property of the Rectangle is modified, then when a downcast to Square is made the dimensions of the Square will still be consistent.

This solution is correct from an object-oriented perspective because each type is responsible for its own data. Yet the solution feels strange because our thinking is that a square has a length and a width, and they happen to be equal. So from our thinking a Square subclasses Rectangle. But from an object-oriented perspective it should be the reverse. This is what makes object-oriented design so difficult sometimes—what is correct might be illogical, and vice versa.

Part of the reason why inheritance has not been working is that the solutions are wrong. The problem is neither object-oriented design nor inheritance, but rather the original assumption that a square is a shape with two dimensions: length and width. Instead of an inheritance structure, we need an object-oriented structure that dovetails into our thinking. That structure would indicate that have two shapes with a length and a width, and that the base class should not be a square or a rectangle, but an interface that is defined as in the following code. Using an interface, we can define our intention of defining a shape with a length and width:

interface IShape {
    long Length {
        get; set;
    }
    long Width {
        get; set;
    }
}

The interface IShape does not indicate whether the shape is a rectangle or a square, but defines some implementation that has both a Length and Width. The implementation will determine if the shape is a square or rectangle using implemented constraints. An example implementation of both Square and Rectangle is as follows:

class Rectangle : IShape {
    private long _width;
    private long _length;
 
    public Rectangle( long width)  {
        _width = width;
    }
    public virtual long Width {
        get {
            return _width;
        }
        set {
            _width = value;
        }
    }
    public virtual long Length {
        get {
            return _length;
        }
        set {
            _length = value;
        }
    }
}
 
class Square : IShape {
    private long _width;
 
    public Square( long width) {
        _width = width;
    }
    public virtual long Length {
        get {
            return _width;
        }
        set {
            _width = value;
        }
    }
    public virtual long Width {
        get {
            return _width;
        }
        set {
            _width = value;
        }
    }
}

This time Square and Rectangle do the right things, and there are no consistency problems. Square exposes the properties Length and Width, but in the implementation there is only one data member: _width. Neither Square nor Rectangle share any implementation details and can be instantiated using a Factory pattern.

Finally we have a solution that is straightforward, maintainable, and extendable. So in a sense we have found the right solution to our problem. Thinking about the Rectangle and Square, let’s take a step back. Would there ever be another shape that could be defined using a length and width? Most likely not, because shapes are different from each other and contain information unique to each shape. A circle is defined by its diameter; a triangle is defined by the lengths of its the three sides; and a parallelogram (slanted rectangle) is defined by length, width, and angle. But we can develop groupings using interfaces, and then use consistency rules to make sure that the state of the implementation is correct.

Another approach does not use interfaces. Shapes are related (for instance, the square, the rectangle, and the parallelogram), and using inheritance that has a common base class would be useful for programming purposes. The solution is to make all shapes immutable so that once the dimensions have been assigned, they cannot be modified. The following is an example of the immutable Square and Rectangle implementation:

class Rectangle {
    private readonly long _length, _width;
 
    public Rectangle( long length, long width) {
        _length = length;
        _width = width;
    }
    public virtual long Length {
        get {
            return _length;
        }
    }
    public virtual long Width {
        get {
            return _width;
        }
    }
}
 
class Square : Rectangle {
    public Square( long width) : base( width, width) {
 
    }
}

The properties Length and Width are missing the set keyword, meaning that neither Square nor Rectangle can be modified once the properties are assigned. The result is that consistency is not violated as it was when a Square was downcast to a Rectangle. When Square is downcast to Rectangle the dimensions reflect a shape that is a square. The result of making the inheritance hierarchy immutable is that all pieces of the hierarchy make sense. There is no missing information, and the hierarchy is always consistent. This is the best solution in terms of patterns, object-oriented design, and consistency. Overall this solution is not better or worse than the interface solution, however—it is simply different and uses a hierarchy.

When designing classes, always consider consistency and remember the following:

  • Using inheritance is not a bad idea, but consistency is a concern. If a class is downcast to a base class, method calls on the base class do not violate the state in the derived classes.
  • Interfaces are very useful means of introducing groupings of types that share the same characteristics but different implementations.
  • Immutable types solve consistency problem by not allowing modifications that could break consistency.


Previous_Page_.gif Next_Page_.gif


Personal tools