Title: EECE 310: Software Engineering
1EECE 310 Software Engineering
- Type Hierarchies and the Substitution Principle
2Objectives
- Apply the Liskov Substitution Principle (LSP) to
the design of type hierarchies - Decide when to favor composition over inheritance
and vice versa
3NonEmptySet Type
- Consider a subtype of IntSet called non-empty
set, with the stipulation that it must never be
empty. i.e., it has at least 1 element always - Constructor takes the element as an argument and
adds it to the els vector (the rep) - insert, size, isIn work as before (no change)
- remove must make sure it never leaves the set
empty, otherwise it throws an EmptySetException
4NonEmptySet Remove
- public class NonEmptySet extends IntSet
-
- public void remove(int x) throws
EmptySetException - // EFFECTS If set has at least two elements,
- // then remove x from the set
- // Otherwise, throw the EmptySetException
- .
-
5RemoveAny procedure
- public static boolean removeAny(IntSet s)
- // EFFECTS Remove an arbitrary element from
- // the IntSet if the set is not empty, return
true - // Otherwise do nothing and return false
- if (s.size() 0) return false
- int x s.choose()
- s.remove(x)
- return true
6Usage of removeAny
- IntSet s new IntSet()
-
- // Add elements to s
- while ( removeAny(s) )
-
-
- // s is empty at this point
7What about this one ?
- IntSet s new NonEmptySet(3)
-
- // Add elements to s
- while ( removeAny(s) )
-
-
- // control never reaches here !
Can potentially throw an EmptySet exception !
8Liskov Substitution principle
- Intuition
- Users can use and reason about subtypes just
using the supertype specification. - Definition
- Subtype specification must support reasoning
based on the super-type specification according
to following rules - signature rule
- methods rule
- properties rule
9Signature Rule
- Every call that is type-correct with the
super-type objects must also be type-correct with
the sub-type objects - Sub-type objects must have all the methods of the
super-type - Signatures of the subtypes implementations must
be compatible with the signatures of the
corresponding super-type methods
10Signature Rule in Java
- Subtypes method can have fewer exceptions but
NOT throw more exceptions - Arguments and return type should be identical
(stricter than necessary) - Foo clone()
- Foo x y.clone()
- Object clone()
- Foo x (Foo) y.clone()
- Enforced by the compiler at compile-time
11NonEmptySet Remove
- public class NonEmptySet extends IntSet
-
- public void remove(int x) throws
EmptySetException - // EFFECTS If set has at least two elements,
- // then remove x
- // Otherwise, throw the EmptySetException
- .
-
Violates signature rule will not compile
12Will this solve the problem ?
- public class NonEmptySet extends IntSet
-
- public void remove(int x)
- // EFFECTS If set has at least two elements,
- // then remove x
- // Otherwise, do nothing
- .
-
-
13What will happen in this case ?
- IntSet s new NonEmptySet(3)
-
- // Add elements to s
- while ( removeAny(s) )
-
-
- // control never reaches here !
Will loop forever because the set never becomes
empty (why ?)
14Whats the problem here ?
- The remove method of NonEmptyIntSet has a
different behavior than the remove method of the
IntSet ADT (its parent type) - In the IntSet ADT, after you call remove(x), you
are assured that x is no longer part of the set
(provided the set was non-empty prior to the
call) - In the NonEmptyIntSet ADT, after you call
remove(x), you do not have this assurance anymore
which violates the substitution principle
15Methods rule
- A sub-type method can weaken the pre-condition
(REQUIRES) of the parent method and strengthen
its post-condition (EFFECTS) - Pre-condition rule presupergt presub
- Post-condition rule presuper postsub gt
postsuper - Both conditions must be satisfied to achieve
compatibility between the sub-type and super-type
methods
16Remember
- Weakening of pre-condition REQUIRES less
- Example Parent-type requires a non-empty
collection, but the sub-type does not - Example Parent-type requires a value gt 0,
sub-type can take a value gt0 in its required
clause - Strengthening of post-condition DOES more
- Example Sub-type returns the elements of the set
in sorted order while parent-type returns them in
any arbitrary order (sorted gt arbitrary)
17Example of methods rule
- Consider a sub-type of IntSet LogIntSet which
keeps track of all elements that were ever in the
set even after they are removed - public void insert(int x)
- // MODIFIES this
- // EFFECTS Adds x to the set and to the log
- Does this satisfy the methods rule ?
18Is the methods rule satisfied here ?
- Consider another sub-type PositiveIntSet which
only adds positive Integers to the set - public void insert(int x)
- // MODIFIED this
- // EFFECTS if x gt 0 adds it to this
- // else does nothing
19Back to the NonEmptySet Type
- public class NonEmptySet // Not derived from
IntSet - // A Non-empty IntSet is a mutable set of
integers - // whose size is at least 1 always
- public void removeNonEmpty(int x)
- // EFFECTS If set has at least two elements,
- // then remove x
- // Otherwise, do nothing
- .
-
-
20Regular IntSet
- public class IntSet extends NonEmptySet
- // Overview A regular IntSet as before
- public void remove(int x)
- // MODIFIES this
- // EFFECTS Removes x from this
-
-
21What happens in this code ?
- public static void findMax (NonEmptySet s)
- int max s.choose()
- iterator g s.elements()
- while (g.hasNext() )
-
-
Can throw an exception if IntSet is passed in as
argument
22Whats the problem here ?
- The IntSet type has an operation remove which
causes it to violate the invariant property of
its parent type NonEmptySet - Calling code may be able to make the set empty by
calling remove and then pass it to findMax - Not enough if the derived methods preserve the
parent-types invariant, the new methods in
sub-type must do so as well
23Properties Rule
- Subtype must preserve each property of the
super-type in each of its methods - Invariant properties (always true)
- Evolution properties (evolve over time)
- Examples
- Invariant property The set never becomes empty
- Evolution property The set size never decreases
24Putting it together Substitution Principle
- Signature rule If program is type-correct based
on super-type specification, it is also
type-correct with respect to the sub-type
specification. - Methods rule Ensures that reasoning about calls
of super-type methods is valid even if the call
goes to code that implements a subtype. - Properties rule Reasoning about properties of
objects based on super-type specification is
still valid even when objects belong to the
sub-type.
25In-class exercise
- public class Counter
- // Overview Counter should never decrease
- public Counter( )
- // EFFECTS Makes this contain 0
- public int get( )
- // EFFECTS Returns the value of this
- public void incr()
- // MODIFIES this
- // EFFECTS Increases the value of this
26In class exercise (contd..)
- Now consider a type Counter2 with the following
methods. Can this be a valid sub-type of Counter? - public Counter2( )
- // EFFECTS Makes this contain 0
- public void incr( )
- // MODIFIES this
- // EFFECTS Makes this contain twice its value
27In class exercise (contd..)
- What if you had another sub-type Counter3 with
two extra operations. Does it satisfy the LSP ? - public Counter3(int n)
- // EFFECTS makes this contain n
- public void incr(int n)
- // MODIFIES this
- // EFFECTS If n gt 0, add n to this
28Summary of LSP
- Liskov Substitution Principle (LSP) is a unifying
way of reasoning about the use of sub-types - Signature rule Syntactic constraint and can be
enforced by compiler - Methods rule and properties rule Pertain to
semantics (behavior) and must be enforced by
programmer - LSP is essential for locality and modifiability
of programs using types and sub-types
29Objectives
- Apply the Liskov Substitution Principle (LSP) to
the design of type hierarchies - Decide when to favor composition over inheritance
and vice versa
30Why do we use sub-types ?
- Define relationships among a group of types
- SortedList and UnsortedList are sub-types of List
- Specification reuse (common interface)
- Using code simply says give me a list
- Implementation reuse (code sharing)
- SortedList need not re-implement all of Lists
methods - Modifiability of parent type
- Client need not change if parent class
implementation changes (if done through public
interface)
31Why not to use sub-types ?
- Sub-types are not appropriate in many cases
- Sub-type must satisfy Liskov Substitution
Principle. In other words, it must not cause
existing code to break. - Subtypes implementation must not depend on the
implementation details of the parent type - Common rule of thumb Sub-types must model is a
special kind of relationship - Not always as simple as we will soon see
32Example Rectangle
- // A vanilla Rectangle class.
- public class Rectangle
- private double width
- private double height
- public Rectangle(double w, double h)
- width w
- height h
-
- public double getWidth() return width
- public double getHeight() return height
- public void setWidth(double w) width w
- public void setHeight(double h) height h
33Example Square Sub-type ?
- Should we model a square as a sub-type of
rectangle (isnt square a type of rectangle ?) - We wont need two instance variables, height and
width, but this is a minor irritant - Need to override setHeight and setWidth
operations so that width and height cannot be
changed independently - Remember, you cannot change the Rectangle class
34Example Square
- public class Square extends Rectangle
- private double width
- private double height
- public Square(double s)
- super(s, s)
-
- public void setWidth(double w)
- super.setWidth(w) super.setHeight(w)
-
- public void setHeight(double h)
- super.setWidth(h) super.setHeight(h)
-
-
35What is the problem here ?
- void testRectangle(Rectangle r)
-
- r.setWidth(4)
- r.setHeight(5)
- assert( r.getHeight() r.getWidth() 20 )
-
- testRectangle( new Square(3) )
36Problem
- Although Square is a type of rectangle in the
real world, a square object is NOT a sub-type of
a rectangle object because it is more constrained
than the rectangle object - Which rule of LSP does it break ?
- We should NOT model square as a sub-type of
rectangle because behaviorally, a square object
cannot be substituted for a rectangle object.
37So how do you fix this ?
- Square and rectangle should not be in an
inheritance relationship with one-another - They are really doing two different things, just
so happens they share some (minimal) features - Do not satisfy the LSP (behavioral substitution)
- But, how to share code between two ADTs which are
not in inherited from each other ?
38One Solution Common base class
- public abstract class Quadrilateral
- // Represents a generic square or rectangle
- protected Quad() // do we need this ?
-
- public int getHeight()
- public int getWidth()
-
- public abstract void setWidth()
- public abstract void setHeight()
39Class Exercise
- Distributed as a handout in class
40Fragile Base Class Problem
- LSP is not the only problem with inheritance.
Even if LSP is satisfied, there are other issues - Assume that you add a new method to IntSet
- public void addAll(Collection c)
- // EFFECTS Adds all elements of c to IntSet
- for (int i C ) this.add( i )
41InstrumentedIntSet
- Consider an example InstrumentedIntSet which
keeps track of the number of elements ever added
to the IntSet ADT (different from its size).
Assume this type inherits from IntSet - Must add a new field to keep track of count
- Override the add method to increment count
- Override the addAll method to increment count
42InstrumentedIntSet Inheritance
- public class InstrumentedIntSet extends IntSet
- private int addCount // The number of attempted
element insertions - public InstrumentedIntSet() super() addCount
0 - public boolean add(Object o)
- addCount
- return super.add(o)
-
- public boolean addAll(Collection c)
- addCount c.size()
- return super.addAll(c)
-
- public int getAddCount()
- return addCount
-
43Whats the problem here ?
- Consider the following code
- IntSet s new InstrumentedIntSet()
- // Assume that array a has 3 int elements
- s.addAll( a )
- int i s.getAddCount( ) // What does it return
?
- How will you fix this problem ?
- 1. Modify addAll to not do the increment, but
what if base class does not call the add method? - 2. Write your own version of addAll in the
derived class to do the iteration (no reuse)
44Solution Use Composition
- Instead of making InstrumentedIntSet a sub-type
of IntSet, make it contain an IntSet - In Java, it holds a reference to an IntSet rather
than a copy, so be careful to not expose it - Do not have to worry about substitution principle
(though that is not a problem in this example) - Make both classes implement a common Set
interface if you want to use one in place of
another (why not use abstract base class ?).
45InstrumentedIntSet Composition-1
- public class InstrumentedIntSet implements Set
-
- private IntSet s
- private int addCount
- public InstrumentedIntSet( )
- addCount 0
- s new IntSet()
-
46InstrumentedIntSet Composition-2
- public class InstrumentedIntSet implements Set
-
- public void add(int element)
- addCount addCount 1
- s.add(element)
-
-
- public void addAll(Collection c)
- addCount addCount c.size()
- s.addAll( c )
-
47Inheritance Vs. Composition
- Every A-object is a B-object.
- Calling A-objects methods automatically
executes Bs code, unless overridden (implicit
code reuse). - Call to overridden method from inherited method
executes As version. - As method code can see Bs protected internals.
- Every A-object has a B-object.
- A must implement all methods, but may delegate
actual work to the internal B-object (explicit
code-reuse). - Call to non-delegated method from delegated
method runs Bs version. - Bs internals hidden from A
- Interface may help if you want to substitute one
for another
48Should B be a subtype of A ?
Do we need to use B in place of A ?
Start
NO
YES
Does B satisfy the LSP ?
Do B and A need to share any code ?
NO
NO
YES
YES
Make B and A independent types (common interface
if necessary)
Make B a sub-type of A, but try to use the public
interface of A in B if possible and efficient
Make B contain an object of type A (common
interface if necessary)
49Class Exercise
- Consider the NonEmptySet type that we saw
earlier. Can you rewrite it to use an IntSet
rather than be derived from an IntSet ? - How will you make them inherit from a common base
class ?
50Summary of Sub-typing
- Inheritance is often over-used without regard for
its consequences (e.g., Java class library) - Not always straightforward to ensure behavioral
substitutability of parent-type with sub-type - Subtle dependencies of sub-type on parent types
implementation details can cause fragility - Use composition instead of inheritance whenever
possible (with interfaces if necessary)