Factoring Instead of Deriving


Another interesting and puzzling case of inheritance is the case of Line and LineSegment.[9] Consider Listings 10-7 and 10-8. At first, these two classes appear to be natural candidates for inheritance. LineSegment needs every member variable and every member function declared in Line. Moreover, LineSegment adds a new member function of its own, Length, and overrides the meaning of the IsOn function. Yet these two classes violate LSP in a subtle way.

[9] Despite the similarity of this example to the Square/Rectangle example, it comes from a real application and was subject to the real problems discussed.

Listing 10-7. Line.cs

public class Line {   private Point p1;   private Point p2;   public Line(Point p1, Point p2){this.p1=p1; this.p2=p2;}   public Point P1 { get { return p1; } }   public Point P2 { get { return p2; } }   public double Slope { get {/*code*/} }   public double YIntercept { get {/*code*/} }   public virtual bool IsOn(Point p) {/*code*/} }

Listing 10-8. LineSegment.cs

public class LineSegment : Line {   public LineSegment(Point p1, Point p2) : base(p1, p2) {}   public double Length() { get {/*code*/} }   public override bool IsOn(Point p) {/*code*/} }

A user of Line has a right to expect that all points that are colinear with it are on it. For example, the point returned by the YIntercept property is the point at which the line intersects the Y-axis. Since this point is colinear with the line, users of Line have a right to expect that IsOn(YIntercept) == true. In many instances of LineSegment, however, this statement will fail.

Why is this an important issue? Why not simply derive LineSegment from Line and live with the subtle problems? This is a judgment call. There are rare occasions when it is more expedient to accept a subtle flaw in polymorphic behavior than to attempt to manipulate the design into complete LSP compliance. Accepting compromise instead of pursuing perfection is an engineering trade-off. A good engineer learns when compromise is more profitable than perfection. However, conformance to LSP should not be surrendered lightly. The guarantee that a subclass will always work where its base classes are used is a powerful way to manage complexity. Once it is forsaken, we must consider each subclass individually.

In the case of the Line and LineSegment, a simple solution illustrates an important tool of OOD. If we have access to both the Line and LineSegment classes, we can factor the common elements of both into an abstract base class. Listings 10-9, 10-10, and 10-11 show the factoring of Line and LineSegment into the base class LinearObject.

Listing 10-9. LinearObject.cs

public abstract class LinearObject {   private Point p1;   private Point p2;   public LinearObject(Point p1, Point p2)   {this.p1=p1; this.p2=p2;}   public Point P1 { get { return p1; } }   public Point P2 { get { return p2; } }   public double Slope { get {/*code*/} }   public double YIntercept { get {/*code*/} }   public virtual bool IsOn(Point p) {/*code*/} }

Listing 10-10. Line.cs

public class Line : LinearObject {   public Line(Point p1, Point p2) : base(p1, p2) {}   public override bool IsOn(Point p) {/*code*/} }

Listing 10-11. LineSegment.cs

public class LineSegment : LinearObject {   public LineSegment(Point p1, Point p2) : base(p1, p2) {}   public double GetLength() {/*code*/}   public override bool IsOn(Point p) {/*code*/} }

Representing both Line and LineSegment, LinearObject provides most of the functionality and data members for both subclasses, with the exception of the IsOn method, which is abstract. Users of LinearObject are not allowed to assume that they understand the extent of the object they are using. Thus, they can accept either a Line or a LineSegment with no problem. Moreover, users of Line will never have to deal with a LineSegment.

Factoring is a powerful tool. If qualities can be factored out of two subclasses, there is the distinct possibility that other classes will show up later that need those qualities, too. Of factoring, Rebecca Wirfs-Brock, Brian Wilkerson, and Lauren Wiener say:

We can state that if a set of classes all support a common responsibility, they should inherit that responsibility from a common superclass.

If a common superclass does not already exist, create one, and move the common responsibilities to it. After all, such a class is demonstrably usefulyou have already shown that the responsibilities will be inherited by some classes. Isn't it conceivable that a later extension of your system might add a new subclass that will support those same responsibilities in a new way? This new superclass will probably be an abstract class.[10]

[10] [Wirfs-Brock90], p. 113

Listing 10-12 shows how the attributes of LinearObject can be used by an unanticipated class: Ray. A Ray is substitutable for a LinearObject, and no user of LinearObject would have any trouble dealing with it.

Listing 10-12. Ray.cs

public class Ray : LinearObject {   public Ray(Point p1, Point p2) : base(p1, p2) {/*code*/}   public override bool IsOn(Point p) {/*code*/} }




Agile Principles, Patterns, and Practices in C#
Agile Principles, Patterns, and Practices in C#
ISBN: 0131857258
EAN: 2147483647
Year: 2006
Pages: 272

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net