The Liskov Substitution Principle

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:

Original Code

 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);
  }  
    
};

Brand-C Support Code Enhancement

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
   }  
    
};

Problems (Violates Liskov Substitution Principle)

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.

 Related Links

 

   What's New    EventStudio 2.5    Real-time Mantra    Thought Projects    Contact Us 
Copyright © 2000-2005 EventHelix.com Inc. All Rights Reserved.