Wednesday, September 09, 2009

SOLID part 3: Liskov Substitution Principle

This is the third part of the series about the SOLID principles, which governs good object-oriented development. You may want to subscribe to the feed to be updated on new issues of this series.
Check out the previous parts if you missed them.

Every function or method which expects an object parameter of class A must be able to accept a subclass of A as well, without knowing it.
The meaning of this principle is that every time you write a subclass, you have to make sure it is substitutable in every place where you use an instance of the original class. The subclass must respect the contract of the superclass, without changing a behavior in such a way that would be impossible to recognize the new instance as belonging also to the superclass.
The name of this principle came from Barbara Liskov, professor at MIT.

The principle is about bad use of inheritance: long chains of inheritance will probably break it as the leaves of the class tree will likely try to reuse code without being proper subclasses. Let's see an example.

In your application you are writing the (overused example) Car class, and suddenly you feel the need for a Motorcycle class to use along for urban traffic simulation. Since there is much in common in these two classes, like the Engine, Brakes and the correlated calculations and wiring in Car's code, you write Motorcycle as a subclass of Car, redefining the methods where its behavior obviously disagree with Car's one, such as getTires() since it has only two tires instead of four.
This redefinition is a violation of LSP: a feasible method checkUp(Car $c) will be broken if it expects four tires to blow up. The language will allow us to pass a Motorcycle instance since it is an instance of Car also, but it is not really a subtype of Car since its contract is less restrictive of Car's one.

A common rule to discover is the subclassing choice is right is the instanceof operator consistency, available in many object-oriented languages. This operator will return true if the variable under test is built from the chosen class or from a subclass ($a instanceof Car). This means in our example $yamaha instanceof Car will return true, which we know it's a bad behavior of the application since its domain model slides away from reality.
The refactoring choice to fix this design it's to abstract away the common behavior of Car and Motorcycle in a Vehicle base (and possibly abstract) class, or to stop using inheritance altogether. Inheritance is widely overrated in the object-oriented programming and composition should be favored in it.

Inheritance is the right choice when an Is-a relationship is present, and in the majority of design should be limited to two or three levels without harm. The original paper from Uncle Bob uses Square and Rectangle as examples. It's obvious that a Rectangle could not be subclassed from a Square, so the reverse is tried. But the contract of Rectangle say that we can change height and width independently (setHeight() and setWidth()), while even if we redefine the methods we cannot in a Square as changing a side will change the other to maintain Square's properties.
There are other designs which will solve this particular problem, like writing immutable objects, but my choice would be to write an helper class which contains common logic and do not chain Square and Rectangle in inheritance at all.
From a more theoretical point of view, preconditions of methods cannot be strenghtened by a subclass while postconditions cannot be weakened: the Square redefinition of setHeight() to modify also the width does not respect the stronger post conditions of Rectangle's method to leave width unchanged; thus, a subclassing is not feasible. This is a bit of design by contract which helps us to detect a bad inheritance strategy: in a particular sense, a Square is not a Rectangle, although it indeed is in a geometric definition; since a Rectangle is identified by an entity whose sides couples can vary indipendently, a Square that forces all four to remain equals is not an instanceof Rectangle.

I hope you're starting to grasp the principles and see the connections between them: to follow religiously one of these first three you're going to apply also the others.
Stay tuned for the next principle explanation, the Interface Segregation Principle.

The image on the top is a photograph of Loris Capirossi on the Ducati Desmosedici, during a MotoGp race. Is a RacingMotorcycle a Motorcycle?

1 comment:

  1. abstract class Vehicle
    {
    private $service;
    public $engine;

    public function __construct(Service $service, $params=array())
    {
    $this->service = $service;

    foreach($params as $name => $param) {
    $this->$name = $param;
    }
    }

    protected function getService()
    {
    return $this->service;
    }

    public function repair()
    {
    return $this->service->repairVehicle($this);
    }
    }

    abstract class Service
    {
    protected $modelClass = 'Model';

    public function createModel($params)
    {
    $modelClass = $this->modelClass;
    return new $modelClass($this, $params);
    }

    public function repairVehicle(Vehicle $vehicle)
    {
    $repaired = true;
    if(!$this->checkPart($vehicle->engine)) {
    $repaired |= $this->repairPart($vehicle->engine);
    }

    //Do Repairs and then return
    return $repaired;
    }

    protected function checkPart(VehiclePart $part)
    {
    return $part->ok;
    }

    protected function repairPart(VehiclePart $part)
    {
    $part->ok = true;
    return true;
    }

    }

    class VehiclePart
    {
    public $type;
    public $ok;
    }

    class Motorcycle extends Vehicle
    {
    public $wheel1;
    public $wheel2;
    }

    class MotorcycleService extends Service
    {
    protected $modelClass = 'Motorcycle';

    public function repairVehicle(Motorcycle $model)
    {
    $repaired = parent::repairVehicle($model);

    if(!$this->checkPart($model->wheel1)) {
    $repaired |= $this->repairPart($model->wheel1);
    }

    if(!$this->checkPart($model->wheel2)) {
    $repaired |= $this->repairPart($model->wheel2);
    }

    return $repaired;
    }
    }

    //LISKOV feels violated...

    ReplyDelete