Liskov Substitution Principle (LSP) is a principle in
Object-Oriented Programming which states that
derived types must be completely substitutable for their base types.
Functions that receive a reference to a base class object should also
be able to take a reference to a derived class object without being
aware of it. This is required because a reference to a base type
object may actually point to a derived type object at run-time and may
cause code written specifically for the base type to misbehave or
produce wrong results. Adding a derived type to the system should
not require changes to existing code. This principle is often violated
by naive class hierarchies based on the is-a relationship. The
canonical example is a square class derived from a rectangle. A
square is a rectangle which imposes the additional constraint that
both its width and height must be equal. So it might seem
natural to derive square from rectangle and impose the constraint
by overriding the modifiers. Let us first implement the rectangle
class. Then we will derive square from it and see how it violates
the Substitution Principle. After that we will device a better design
by introducing contracts that control how methods might be
overridden by a derived class. I choose
CLOS as the
implementation language because it provides a simple mechanism for
adding method contracts.
Here is the definition of the rectangle class:
(defclass rectangle ()
((width :accessor width :initform 0)
(height :accessor height :initform 0)))
(defgeneric set-width (rect w))
(defgeneric set-height (rect h))
(defmethod set-width ((rect rectangle) w)
(setf (width rect) w))
(defmethod set-height ((rect rectangle) h)
(setf (height rect) h))
There is a method to impose a system-wide policy that the area of a rectangle is always 200:
(defmethod check-area ((rect rectangle))
(assert (= (* (width rect) (height rect)) 200))
t)
> (defvar r (make-instance 'rectangle))
> (set-width r 100)
> (set-height r 2)
> (check-area r)
> (set-height r 200)
> (check-area r)
Later it was required that a square type be added to the system. Its
definition is based on the assumption that a square is-a
rectangle. As we proceed with the implementation, it is observed
that a square requires only a single field to represent both its
width and height. The problem is solved by overriding the modifier
methods and setting both fields simultaneously to the same value. (A
good programmer will take this duplication of data as the first
indication that something is broken in the design. He will not try to
work around it!)
(defclass square (rectangle) ())
(defmethod set-width ((sqr square) w)
(setf (height sqr) w)
(setf (width sqr) w))
(defmethod set-height ((sqr square) h)
(setf (width sqr) h)
(setf (height sqr) h))
The above class definition has violated the Substitution Principle.
This is proved by the following test. check-area will fail, unless
it is modified to take special care of square objects:
(set-width r 100)
(set-height r 2)
(check-area r)
How can we prevent the creation of derived types that violate LSP?
Programming by Contract, where preconditions and post-conditions
are established for class methods, will help in solving this problem.
The precondition must be true for the method to execute. Upon
completion, the method guarantees that the post-condition will be
true. We will redefine rectangle with pre/post conditions that makes sure that changing one field will not affect the other. The :before and :after method qualifiers in CLOS allows us to add these conditions:
(defclass rectangle ()
((width :accessor width :initform 0)
(height :accessor height :initform 0)
(old-width :accessor old-width)
(old-height :accessor old-height)))
(defmethod set-width :before ((rect rectangle) w)
(assert (> w 0))
(setf (old-height rect) (height rect)))
(defmethod set-width ((rect rectangle) w)
(setf (width rect) w))
(defmethod set-width :after ((rect rectangle) w)
(assert (= (height rect) (old-height rect))))
(defmethod set-height :before ((rect rectangle) h)
(assert (> h 0))
(setf (old-width rect) (width rect)))
(defmethod set-height ((rect rectangle) h)
(setf (height rect) h))
(defmethod set-height :after ((rect rectangle) w)
(assert (= (width rect) (old-width rect))))
Once these conditions are established, the overridden modifiers of
square refuse to work, as the post-condition assertion fails:
The implementor of square has to unlink it from rectangle and
rethink his design. By a rule of Programming by Contract, a derivative
may only replace the precondition of a method by a weaker one, and its
post-condition by a stronger one. (As qualified method calls are
propagated up the class hierarchy in CLOS, it is difficult to break
this rule here/).
An even better design that will allow both rectangle and square to
stay within the same hierarchy is to define a common base class for
all shapes and derive rectangle and square from it:
(defclass shape () ())
(defgeneric set-width (shape w))
(defgeneric set-height (shape h))
(defclass rectangle (shape)
((width :accessor width :initform 0)
(height :accessor height :initform 0)
(old-width :accessor old-width)
(old-height :accessor old-height)))
(defmethod set-width :before ((rect rectangle) w)
(assert (> w 0))
(setf (old-height rect) (height rect)))
(defmethod set-width ((rect rectangle) w)
(setf (width rect) w))
(defmethod set-width :after ((rect rectangle) w)
(assert (= (height rect) (old-height rect))))
(defmethod set-height :before ((rect rectangle) h)
(assert (> h 0))
(setf (old-width rect) (width rect)))
(defmethod set-height ((rect rectangle) h)
(setf (height rect) h))
(defmethod set-height :after ((rect rectangle) w)
(assert (= (width rect) (old-width rect))))
(defmethod check-area ((rect rectangle))
(assert (= (* (width rect) (height rect)) 200))
t)
(defclass square (shape)
((side :accessor side :initform 0)))
(defmethod set-width ((sqr square) w)
(setf (side sqr) w))
(defmethod set-height ((sqr square) h)
(setf (side sqr) h))