JAL Computing

C++COMProgramming .NET Mac Palm CPP/CLI Hobbies

 

Home
Up

Chapter 17 "Designing Extendable Classes"

In Chapter 7, we investigated the conundrum of using implementation inheritance versus containment. Some authors have argued that composition or containment by ownership is generally preferable to implementation inheritance. Joshua Block argues in "Effective Java" to "favor composition over (implementation) inheritance." He quotes Snyder that "Unlike method invocation, inheritance breaks encapsulation." He concludes that since "a subclass depends on the implementation details of its super class for its proper function... the subclass may break...unless the super class's authors have designed and documented it specifically for the purpose of being extended." Joshua Block goes on to recommend that one "design and document for inheritance or else prohibit it."

A concrete example of the subtle complexity of using inheritance is the creation of a type safe collection. Let us assume that you have extended a non type safe collection by providing only type safe getter and setter method implementations. Now let us assume that developer of the base class decides to add new non type safe accessors to the base class. Suddenly your type safe collection is no longer type safe! Instead, you should consider wrapping the non type safe collection in a new class and provide type safe getters and setters. If new non type safe accessors are added to the base class they will not be visible outside of your wrapper class! So, if a class has not been designed for inheritance, prefer composition.

In Chapter 7, we created a type safe, null safe collection by extending the abstract base class System.Collections.CollectionBase. CollectionBase was specifically designed and documented to be extended. In this chapter we are going to explore the challenges of designing an extendable class.

Note: Implementation inheritance differs from interface inheritance as discussed in Chapter 9 "Interfaces". Composition, or containment by ownership, differs from containment by reference as discussed in Chapter 12 "Unified Modeling Language".

Again, implementation inheritance implies an IS_A relationship from a generalization to a specialization. However, a well designed class hierarchy must do more than just represent an abstraction hierarchy. Specifically, a class hierarchy should be designed such that the behavior of the base class holds for all subclasses of the class. One way to look at this is to say that the subclass may expand the behavior of the base class. The corollary is that a subclass should not restrict the behavior of the base class. 

Here are two links that may prove illuminating.

bullethttp://c2.com/cgi/wiki?LiskovSubstitutionPrinciple
bullethttp://www.parashift.com/c++-faq-lite/proper-inheritance.html

Do Not Restrict The Behavior of the Subclass

http://www.parashift.com/c++-faq-lite/proper-inheritance.html

[21.1] Should I hide member functions that were public in my base class?

Never, never, never do this. Never. Never!

Attempting to hide (eliminate, revoke, privatize) inherited public member functions is an all-too-common design error. It usually stems from muddy thinking.

Resist the temptation to extend a class by overriding a public method in the base class and just throwing a MethodNotSupported exception in the body of the method in the subclass. Throwing an exception instead of providing an implementation unilaterally revokes the contract between the caller and the base class. Just as importantly, this contractual failure may not be realized until runtime.

The Subclass May Only Expand The Behavior of the Base Class

http://c2.com/cgi/wiki?LiskovSubstitutionPrinciple

The Liskov Substitution Principle

What is wanted here is something like the following substitution property: If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T." - BarbaraLiskov, Data Abstraction and Hierarchy, SIGPLAN Notices, 23,5 (May, 1988).

The subclass may add behaviors to the base class, but the behaviors of the base class must still hold true for the subclass. It has been argued that the Liskov Substitution Principle may not apply to all environments.

Is a Square a Specialization of Rectangle?

To understand the subtleties of extendable class design, we must address the classic question "Is a Square a specialization of a Rectangle?" At first glance this seems like a reasonable assumption, Square should inherit from Rectangle. A Square IS_A Rectangle. However, this class hierarchy fails to adhere to the Liskov Substitution principle because the behavior of a Square differs from the behavior of a Rectangle. The behavior of a square is a restricted subset of the behavior of a Rectangle. More specifically, the caller of the Square class cannot independently set the height and width properties of a Square. Setting the height of a Square has an unexpected side effect of setting the width of the Square. This may be reasonable for a Square, but is unexpected behavior for the base class Rectangle. So Square and Rectangle should both inherit from Shape. More at:

http://www.objectmentor.com/resources/articles/lsp.pdf

Keep Private Fields Private

One advantage of encapsulation is that it hides the underlying data fields behind getter and setter methods or properties. Sometimes it may be tempting to make a data field protected so that a sub class can directly access the data field in the base class. The following communication argues that the use of protected data results in bugs and maintenance problems. 

http://www.c-view.org/tech/pattern/cpptips/prot_data

In this communication Bjarne Stroustrup states "Fortunately, you don't have to use protected data in C++; `private' is the default in classes and is usually the better choice. Note that none of these objections are significant for protected member functions. I still consider `protected' a fine way of specifying operations for use in derived classes."

So resist the temptation to make a private field protected!

Mimic C++ Const in C# Using Inheritance

In Chapter 7 I argued to use containment to adapt a read write interface to a read only interface.  I do think it is possible to mimic C++ const SomeClass *p in C# using a carefully designed inheritance hierarchy and by passing a reference to a base interface. In Chapter 6, I noted that when you declare a reference variable, the type of the reference variable restricts access to one of the object's public contracts. By passing a reference to a read only base interface, you can restrict access to the read only interface of the object. 

In this example the ReadWriteClass extends the ReadClass and I believe it satisfies the Liskov Substitution principle. The trick is to create an object of class ReadWriteClass and then pass a reference of type ReadClass, not ReadWriteClass, to the client! The downside to passing a reference of type ReadClass that refers to an object of class ReadWriteClass is that the caller can still cast the ReadClass reference variable to a ReadWriteClass reference variable. (But hey, you can do this in C++ also with const_cast.) So my twisted proposal is for a new C# type called const_ref. The compiler could insure that a const_ref cannot be cast up or down the class hierarchy. That's it!

Note: If you want polymorphic behavior then you would need to use a virtual getter method, not a property. Alternatively, you could use C# v2.0s support for independent get/set access modifiers. C# does support read only value types.

using System;
namespace TestConst
{
	/// <summary>
	/// Summary description for TestConst
	/// In that I argue that const SomeClass *p
	/// can be mimicked in C# using a careful class
	/// hierarchy and a new type called const_ref
	/// The compiler could enforce that a type of const_ref
	/// could not be cast up or down the class hierarchy
	/// Thats it! 
	/// Even with out const_ref, this class design
	/// mimics const SomeClass *p. Sure you can cast
	/// up to the read/write interface, but you can also
	/// do const_cast in C++. Of course, const_cast
	/// requires willful code unlike (ReadWriteClass)rc
	/// JAL
	/// </summary>
	class ReadClass
	{
		protected int i=0;
		public int Integer 
		{
			get {return i;}
		}
		public ReadClass(int i) 
		{
			this.i= i;
		}
	}
	class ReadWriteClass : ReadClass 
	{
		public new int Integer
		{
			get
			{
				return base.Integer;
			}
			set
			{
				i= value;
			}
		}
		public ReadWriteClass(int i):base(i)
		{
		}
	}
	class Test 
	{
		ReadClass rc= null;
		//public Test(const_ref ReadClass rc)
		public Test(ReadClass rc) 
		{
			this.rc= rc;
			//this.rc.Integer= 4; // fails as expected
			// the compiler could disallow the following
			// if rc is declared const_ref
			ReadWriteClass rwc= (ReadWriteClass)(this.rc);
			// so we could not do
			rwc.Integer= 4;
		}
		/// <summary>
		/// The main entry point for the application.
		/// </summary>
		[STAThread]
		static void Main(string[] args)
		{
			//
			// TODO: Add code to start application here
			//
			ReadWriteClass rwc= new ReadWriteClass(2);
			rwc.Integer= 3;
			ReadClass rc= (ReadClass)rwc;
			System.Console.WriteLine(rc.Integer);
			//rc.Integer= 4; fails to compile as expected
			Test t= new Test(rc);
			System.Console.WriteLine(rc.Integer);
			System.Console.ReadLine();
		}
	}
}

Have fun!

 

Send mail to [email protected] with questions or comments about this web site. Copyright © 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009 © 
Last modified: 08/04/09
Hosted by www.Geocities.ws

1