EECE 310: Software Engineering

Download Report

Transcript EECE 310: Software Engineering

EECE 310: Software Engineering
Type Hierarchies and the Substitution
Principle
What have we done so far ?
• Defined our own
– Operations: Procedural abstractions
– Data-types: Data abstractions
– Loop constructs: Iteration abstraction
Theme: Mechanisms that let you “extend” the
language to do what you need.
[See: ‘Growing a language’ by Guy Steele]
What are we going to do next ?
• Build our own hierarchy of types
• Many programming languages already do this
– For example, float and int are both “sub-types” of
number, though they both have different
implementations and semantics
– As another example, you can have a plain
InputStream, from which you derive both
BufferedInputStream and DataInputStream
Outline
• Basic concepts of type hierarchies
• Implementation of type hierarchies
• Liskov Substitution Principle
• Solutions to the LSP
Object Assignment
• Consider the following code:
Object
Object o = new String(“hello”);
String
Apparent type
Actual type
o.equals(“hello”); // OK, as Object has equals
o.length();
// Error, as length is not in Object
Type Assignment
Furniture f1 = new desk(“room1”, 4, 5);
Furniture f2 = new chair(“room1”, 2, 3);
Furniture
+ move(x, y)
Desk
f1.move(3, 4); // OK
Chair
+ fold()
f2.fold(); // Error
Type-checking
• Compiler checks if the apparent type of the
object has a method call in its signature
– Does not use the actual types to check this, even if the
actual type has the corresponding method
– Responsible for ensuring that derived types (actual
types) implement all the methods in the signature of
their parent (apparent) types
• Runtime system is responsible for actually
performing the method call to the actual type
Dispatching
Furniture f1 = new desk(“room1”, 4, 5);
Furniture f2 = new chair(“room1”, 2, 3);
Furniture
+ move(x, y)
Desk
Chair
+ fold()
f1.move(3, 4); // Which move method is called?
Dispatching
Compiler uses a dispatch vector to find method
dispatch
vector
Method 1
Object p
Method call
Method 2
Method 3
Outline
• Basic concepts of type hierarchies
• Implementation of type hierarchies
• Liskov Substitution Principle
• Solutions to the LSP
Derived Type: MaxIntSet
• Need a new operation called max that returns
the biggest integer in the set
– Extends the IntSet ADT discussed earlier
• Implemented by keeping a separate data
member called biggest in the class
– Returned directly by the max operation
– Updated by both insert and remove operations
MaxIntSet: Specification
public class MaxIntSet extends IntSet {
// OVERVIEW: MaxIntSet is a subtype of IntSet
// with an additional method max, to find
// the maximum element of the set
public MaxIntSet( );
public MaxIntSet(int x);
public int max( ) throws EmptyException;
}
MaxIntSet: Rep and Constructor
public class MaxIntSet extends IntSet {
private int biggest; // the biggest element
// if the set is not empty
// els is inherited from the IntSet ADT
public MaxIntSet() { super( ); }
public MaxIntSet(int x) {
super(); super.insert(x);
biggest = x;
}
MaxIntSet: Insert
public void insert(int x) {
// EFFECTS: Updates biggest to the
// maximum element of the set before
// inserting the new element
if ( size() == 0 || x > biggest)
biggest = x;
super.insert( x );
}
MaxIntSet: Max
public int max ( ) throws EmptyException {
// EFFECTS: if this is empty throw EmptyException
// else return the largest element of this
if (size() == 0) {
throw new EmptyException(“MaxIntSet”);
}
return biggest;
}
MaxIntSet: remove
public void remove(int x) {
// EFFECTS: Remove the element from the
// IntSet and if it was the biggest element,
// update the biggest element in the set
super.remove(x);
if (size() !=0 && biggest==x)
updateBiggest(); // Member of MaxIntSet
}
How to implement updateBiggest ?
• Option 1: Make the els instance variable
public so that all outside code can access it
• Option 2: Make the els instance variable
protected so that derived classes can access it
• Option 3: Use the existing public interface of
IntSet, but use its iterator method
MaxIntSet: Rep. Invariants (Option 3)
• Rep Invariant only needs to define the
constraints on the biggest variable
– Everything else is inherited from IntSet
– Cannot change this as it only has public access
IMaxIntSet( c ) = (c.size == 0) ||
( (c.biggest in AFIntSet(c) ) &&
(for all x in AFIntSet(c), x <= c.biggest) )
MaxIntSet: Rep. Invariants (Option 2)
• Rep Invariant needs to define the constraints
on both the biggest and els variables
– Since it can potentially modify the els variable
– May use the RI of the IntSet ADT in its definition
IMaxIntSet( c ) = IIntSet( c ) && ( (c.size == 0) ||
( (c.biggest in AFIntSet(c) ) &&
(for all x in AFIntSet(c), x <= c.biggest) )
)
In class exercise
• What if the IntSet ADT maintained its els
vector in sorted order (smallest to biggest).
How would you implement the max
operation in its sub-type MaxIntSet ?
– Using protected access ?
– Through its public Interface ?
Explain the rationale for your choice and show the
implementation using your preferred method.
Outline
• Basic concepts of type hierarchies
• Implementation of type hierarchies
• Liskov Substitution Principle
• Solutions to the LSP
NonEmptySet 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
22
NonEmptySet: 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
….
}
}
23
RemoveAny 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;
}
24
Usage of removeAny
IntSet s = new IntSet();
…
// Add elements to s
while ( removeAny(s) ) {
…
}
// s is empty at this point
25
What 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 !
26
So what’s the problem ?
• Subtype defines new behavior that is not
compatible with the parent type
• Users of parent type should be correct even if
they do not know about the sub-type (locality)
• Sub-type objects can be substituted with the
parent type objects without causing existing code
that uses the parent type to break (modifiability)
27
Liskov 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:
1. signature rule
2. methods rule
3. properties rule
28
Signature Rule
• Every call that is type-correct with the supertype 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 subtype’s implementations must
be compatible with the signatures of the
corresponding super-type methods
29
Signature Rule in Java
1. Subtype’s method can have fewer exceptions
but NOT throw more exceptions
2. Arguments and return type should be identical:
(stricter than necessary)
Foo clone();
Foo x = y.clone();
Object clone();
Foo x = (Foo) y.clone();
3. Enforced by the compiler at compile-time
30
NonEmptySet: Remove
public class NonEmptySet extends IntSet {
Violates signature rule
…
– will not compile
public void remove(int x) throws
EmptySetException {
// EFFECTS: If set has at least two elements,
// then remove x
// Otherwise, throw the EmptySetException
….
}
}
31
Will 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
….
}
}
32
What 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 ?)
33
What’s the problem here ?
• The remove method of NonEmptyIntSet has a
different behavior than the remove method
of the IntSet ADT (it’s 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
34
Methods rule
• A sub-type method can weaken the precondition (REQUIRES) of the parent method
and strengthen its post-condition (EFFECTS)
– Pre-condition rule: presuper=> presub
– Post-condition rule: presuper && postsub => postsuper
• Both conditions must be satisfied to achieve
compatibility between the sub-type and
super-type methods
35
Remember …
• 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 > 0, subtype can take a value >=0 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 => arbitrary)
36
Example 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 ?
37
Is 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 >= 0 adds it to this
//
else does nothing
38
Back 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
….
}
}
39
Regular IntSet
public class IntSet extends NonEmptySet {
// Overview: A regular IntSet as before
public void remove(int x) {
// MODIFIES: this
// EFFECTS: Removes x from this
…
}
}
40
What happens in this code ?
public static void findMax (NonEmptySet s) {
int max = s.choose();
Can throw an
iterator g = s.elements();
exception if IntSet is
passed in as argument
while (g.hasNext() ) {
…
}
}
41
What’s 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-type’s invariant, the new methods
in sub-type must do so as well
42
Properties 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
43
Putting 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.
44
In-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
45
In 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
46
In class exercise (contd..)
• Signature Rule: Satisfied by counter2 ?
• Methods rule: Satisfied by counter2 ?
– Pre-condition rule: presuper=> presub
– Post-condition rule: presuper && postsub => postsuper
• Properties rule: Satisfied by counter2 ?
– Invariant property ?
– Evolution property ?
47
Summary 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
48
Outline
• Basic concepts of type hierarchies
• Implementation of type hierarchies
• Liskov Substitution Principle
• Solutions to the LSP
Why 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 List’s methods
• Modifiability of parent type
– Client need not change if parent class implementation
changes (if done through public interface)
Why 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.
– Subtype’s 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
InstrumentedIntSet
• Consider an example InstrumentedIntSet
which keeps track of the number of elements
ever added to the IntSet ADT (different from
its size). Should this type inherit 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
InstrumentedIntSet: Inheritance
public class InstrumentedIntSet extends IntSet {
private int addCount = 0; // The number of attempted element insertions
public InstrumentedIntSet() { super(); }
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;
}
What’s 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( );// Returns 6 !!!
How will you fix this problem ?
1. Modify addAll to not do the increment, but what if
base class does not call add (implementation specific)
2. Write your own version of addAll in the derived
class to do the iteration (no reuse, duplication)
Solution: Use Composition
• Instead of making InstrumentedIntSet a subtype 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 ?).
InstrumentedIntSet: Composition-1
public class InstrumentedIntSet implements Set
{
private IntSet s;
private int addCount;
public InstrumentedIntSet( ) {
addCount = 0;
s = new IntSet();
}
InstrumentedIntSet: 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 );
}
Inheritance Vs. Composition
Inheritance
Every A-object is a B-object.
–Calling A-object’s methods
automatically executes B’s
code, unless overridden
(implicit code reuse).
–Call to overridden method
from inherited method
executes A’s version.
–A’s method code can see B’s
protected internals.
Composition
Every A-object has a B-object.
–A must implement all
methods, but may delegate
actual work to the internal Bobject (explicit code-reuse).
–Call to “non-delegated”
method from delegated
method runs B’s version.
–B’s internals hidden from A
–Interface may help if you want
to substitute one for another
Should B be a subtype of A ?
Start
Do we
need to
use B in
place of
A?
NO
YES
Does B
satisfy
the LSP
?
NO
YES
Make B a sub-type of A,
but try to use the public
interface of A in B if
possible and efficient
Do B and
A need to
share any
code ?
NO
YES
Make B contain an
object of type A
(common interface
if necessary)
Make B and A
independent
types (common
interface if
necessary)
Summary 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 type’s
implementation details can cause fragility
• Use composition instead of inheritance whenever
possible (with interfaces if necessary)