|
The Liskov Substitution Principle of object oriented design states:
In class hierarchies, it should be possible to
treat a specialized object as if it were a base class object.
The basic idea here is very simple. All code operating with a pointer or
reference to the base class should be completely transparent to the type of the
inherited object. It should be possible to substitute an object of one type with
another within the same class hierarchy. Inheriting classes should not perform
any actions that will invalidate the assumptions made by the base class.
This is best explained with an example. The following example explains a case
where enhancements to the code can violate the Liskov Substitution Principle.
The discussion is divided into three steps:
We will consider the design of software that manages the temperature in
various chambers in a system. The software periodically reads the temperature
from each chamber and then adjusts it to a reference temperature. The behavior
is modeled as a Temperature Controller base class. Temperature controllers in
different chambers differ in their programming interface. These differences are
handled by individual classes that inherit from Temperature Controller base
class.
The Temperature Controller base class supports the following methods:
- Get/Set Reference: These methods are used to
get and set the reference temperature for the chamber. This is not a virtual
method, as no device programming is involved.
- Get Temperature: Reads the temperature from
the device. Since registers for reading the temperature differ from one
device to another, this method is pure virtual.
- Adjust Temperature: Adjusts the temperature
by applying the adjustment specified in the parameter. Again, this method is
pure virtual as it involves device programming.
The following code also shows two classes inheriting from the base class.
| Temperature
Controller |
class TemperatureController
{
// The chamber needs to be maintained at the reference temperature
int m_referenceTemperature;
public:
int GetReferenceTemperature() const
{
return m_referenceTemperature;
}
void SetReferenceTemperature(int referenceTemperature)
{
m_referenceTemperature = referenceTemperature;
}
virtual int GetTemperature() const = 0;
virtual void AdjustTemperature(int temperature) = 0;
virtual void Initialize()
{
// Initialize the device address here
}
};
class Brand_A_TemperatureController
{
public:
int GetTemperature() const
{
return (io_read(TEMP_REGISTER));
}
void AdjustTemperature(int temperature);
{
io_write(TEMP_CHANGE_REGISTER, temperature);
}
};
class Brand_B_TemperatureController
{
public:
int GetTemperature() const
{
return (io_read(STATUS_REGISTER) & TEMP_MASK);
}
void AdjustTemperature(int temperature);
{
// Device requires shifting by 5 bits before writing to the change
// register
io_write(CHANGE_REGISTER, temperature << 5);
}
};
|
Now consider the case where the marketing department comes back and says they
need support for another type of Temperature Controller - Brand C. The
developers assume that this should be a simple change as all they need to do is
inherit from Temperature Controller base class and define the Get Temperature
and Adjust Temperature methods. On further inspection of the programming
interface, the developers realize that Brand C is quite different from the other
Temperature Controllers. It does not fit
well into their scheme of things. Brand C is an automatic device where the
reference temperature is programmed to the device and then on the device
automatically maintains the temperature to the desired level. It is clear that
that Brand C will not fit into the base class. Thus developers decide to change
the base class by making Get/Set Reference Temperature methods virtual (not pure
virtual). They figure this way all the other temperature sensors would work with
existing base class implementation. The Brand C would override the Get/Set
Reference Temperature methods. These methods would directly operate upon the
device. Another change needed
would be to override Adjust Temperature method with a blank implementation. As
this method has no role to play in Brand C (Brand C is automatic so it performs
temperature adjustments on its own.).
The final code is shown below:
| Temperature
Controller |
class TemperatureController
{
// The chamber needs to be maintained at the reference temperature
int m_referenceTemperature;
public:
// Get and Set methods for Reference Temperature have been
// made virtual to accomodate Brand C
virtual int GetReferenceTemperature() const
{
return m_referenceTemperature;
}
virtual void SetReferenceTemperature(int referenceTemperature)
{
m_referenceTemperature = referenceTemperature;
}
virtual int GetTemperature() const = 0;
virtual void AdjustTemperature(int temperature) = 0;
};
class Brand_C_TemperatureController
{
public:
// Get/Set Reference Temperatures now read and write the device directly
int GetReferenceTemperature() const
{
return (io_read(REFERENCE_REGISTER);
}
void SetReferenceTemperature(int referenceTemperature)
{
io_write(REFERENCE_REGISTER, referenceTemperature);
}
int GetTemperature() const
{
return (io_read(TEMP_MONITORING_REGISTER));
}
void AdjustTemperature(int temperature);
{
// Adjust temperature has no role in brand C, as temperature
// control is automatic
}
};
|
The problems with the above design are:
- It is a band-aid solution to the problem. A more natural solution would be
to define a base class for Automatic Temperature Sensors.
- It violates the Liskov Substitution Principle. We can no longer substitute
one class from the class hierarchy with another.
One can easily see the following violations of the Liskov Substitution
Principle. Consider the code below that is operating with a pointer to the
Temperature Controller. The code first sets the reference temperature and then
intializes the controller. This code would work fine if pTempCtrl was pointing
to a Brand A or B temperature controller. The code breaks when the pointer is
Brand C. This happens because of the override of SetReferenceTemperature now
accesses the device using a io_write call. But the code actually calls
initialize only in the following statement. Thus all temperature controllers are
not perfectly substitutable. The SetReferenceTemperature method for Brand A and
B does not access the device. The same method for Brand C accesses the device.
|
SetReferenceTemperature violates Liskov Substitution Principle |
. . .
TemperatureController *pTempCtrl = GeNextTempController();
pTempCtrl->SetReferenceTemperature(10);
pTempCtrl->Initialize();
. . .
|
Code calling Adjust Temperature may break too. If the original code was
being used to set the temperature to any thing other than the reference
temperature, it will not have the desired effect with Brand C. This method has
been overriden for Brand C to perform no action.
If Liskov Substitution Principle is followed, code using a base class pointer
will never break after another class is added to the inheritance tree.
|