Composition and Inheritance
Inheritance is a powerful way of designing for changing circumstances or contexts. It can limit flexibility, however, especially when classes take on multiple responsibilities.
The Problem
As you know, child classes inherit the methods and properties of their parents (as long as they are protected or public elements). We use this fact to design child classes that provide specialized functionality. Figure 1 presents a simple example using the UML.
Figure 1: A parent class and two child classes
The abstract Lesson class in Figure 1 models a lesson in a college. It defines abstract cost() and chargeType() methods. The diagram shows two implementing classes, FixedPriceLesson and TimedPriceLesson, which provide distinct charging mechanisms for lessons.
Using this inheritance scheme, we can switch between lesson implementations. Client code will know only that it is dealing with a Lesson object, so the details of costing will be transparent.
What happens, though, if we introduce a new set of specializations? We need to handle lectures and seminars. Because these organize enrollment and lesson notes in different ways, they require separate classes. So now we have two forces that operate upon our design. We need to handle pricing strategies and separate lectures and seminars. Figure 2 shows a brute-force solution.
Figure 2: A poor inheritance structure
Figure 2 shows a hierarchy that is clearly faulty. We can no longer use the inheritance tree to manage our pricing mechanisms without duplicating great swathes of functionality. The pricing strategies are mirrored across the Lecture and Seminar class families. At this stage, we might consider using conditional statements in the Lesson super class, removing those unfortunate duplications. Essentially, we remove the pricing logic from the inheritance tree altogether, moving it up into the super class. This is the reverse of the usual refactoring where we replace a conditional with polymorphism. Here is an amended Lesson class:
abstract class Lesson
{
protected $duration;
const FIXED = 1;
const TIMED = 2;
private $costtype;
function __construct( $duration, $costtype=1 )
{
$this->duration = $duration;
$this->costtype = $costtype;
}
function cost()
{
switch ( $this->costtype ) {
CASE self::TIMED :
return (5 * $this->duration);
break;
CASE self::FIXED :
return 30;
break;
default:
$this->costtype = self::FIXED;
return 30;
}
}
function chargeType()
{
switch ( $this->costtype)
{
CASE self::TIMED :
return "hourly rate";
break;
CASE self::FIXED :
return "fixed rate";
break;
default:
$this->costtype = self::FIXED;
return "fixed rate";
}
}
// more lesson methods...
}
class Lecture extends Lesson
{
// Lecture-specific implementations ...
}
class Seminar extends Lesson
{
// Seminar-specific implementations ...
}
You can see the new class diagram in Figure 3.
Figure 3: Inheritance hierarchy improved by removing cost calculations from subclasses
We have made the class structure much more manageable, but at a cost. Using conditionals in this code is a retrograde step. Usually, we would try to replace a conditional statement with polymorphism. Here we have done the opposite. As you can see, this has forced us to duplicate the conditional statement across the chargeType() and cost() methods.
No comments:
Post a Comment