CPSC 210

D2: Liskov Substitution Principle (LSP)

Learning Goals

  • To state the Liskov Substitution Principle (LSP)
  • To identify violations of the LSP in existing code
  • To ensure the LSP is satisfied when designing type hierarchies

LSP

English Explanation

If I use a variable of type Superclass properly in my code..

..it will work right no matter what Subclass object it actually holds.

LSP - Nutshell Explanation

  • When a subtype is substituted for its super type:
    • the subtype must provide the expected behaviours of the super type
       
    • any test based on the super type must pass on the subtype
@BeforeEach
public void setup() {
  this.bird = new Eagle();
}

@Test
public void testBirdFlies() {
  // THIS TEST SHOULD PASS 
  // REGARDLESS OF THE ACTUAL TYPE
  assertTrue(this.bird.fly());
}
Bird.fly()
Eagle.fly()
bird.fly()
eagle.fly()

Tests use the
Supertype properly!

LSP - Emotional Explanation (1)

  • Subtype method must NOT:
    • narrow the range of inputs that it accepts (specified in requires clause / precondition)
    • widen the range of outputs that it produces (specified in effects clause / postcondition)

wider

narrower

narrower

wider

Preconditions

Postconditions

Note: There are ways to violate LSP without violating this rule.

LSP - Emotional Explanation (2)

  • Subtype method must NOT:
    • narrow the range of inputs that it accepts (specified in requires clause / precondition)
    • widen the range of outputs that it produces (specified in effects clause / postcondition)

narrower

wider

Preconditions

Postconditions

Reject nothing that "used to be" allowed!

Produce nothing that "used to be" forbidden!

Note: There are ways to violate LSP without violating this rule.

LSP - Scientific Explanation

Barbara Liskov

Strong behavioral subtyping:

a semantic rather than syntactic relation

LSP - Example (1)

Superclass

Subclass

// REQUIRES: input >= 0
// EFFECTS: sets the input
public void setInput(int input) {
    this.input = input;
}
// REQUIRES: input >= 100
// EFFECTS: sets the input
@Override
public void setInput(int input) {
    super.setInput(input);
}

Y

N

Y

Is precondition narrower?

Is postcondition wider?

Is LSP violated?

A

B

C

LSP - Example (2)

Superclass

Subclass

// REQUIRES: input >= 0
// EFFECTS: sets the input
public void setInput(int input) {
    this.input = input;
}
// REQUIRES: input >= -100
// EFFECTS: sets the input
@Override
public void setInput(int input) {
    super.setInput(input);
}

N

N

N

Is precondition narrower?

Is postcondition wider?

Is LSP violated?

A

B

C

The implementation should be correct,
but the spec is what we care about for LSP.

 


 

Just for fun: what's wrong
with this implementation?

LSP - Example (3)

Superclass

Subclass

// EFFECTS: returns an integer
// in the range [0, 100)
public int produceValue() {
    return input % 100;
}
// EFFECTS: returns an integer 
// in the range [0, 200)
@Override
public int produceValue() {
    return input % 200;
}

Y

N

Is precondition narrower?

Is postcondition wider?

Is LSP violated?

A

B

C

Y

LSP - Example (4)

Superclass

Subclass

// EFFECTS: returns an integer
// in the range [0, 100)
public int produceValue() {
    return input % 100;
}
// EFFECTS: returns an integer 
// in the range [0, 50)
@Override
public int produceValue() {
    return input % 50;
}

N

N

Is precondition narrower?

Is postcondition wider?

Is LSP violated?

A

B

C

N

LSP - Example (5)

Superclass

Subclass

// REQUIRES: input is -1, 0, or 1
// EFFECTS: sets the input
public void setInput(int input) {
    this.input = input;
}
// REQUIRES: input >= 0
// EFFECTS: sets the input
@Override
public void setInput(int input) {
    this.input = input;
}

N

Is precondition narrower?

Is postcondition wider?

Is LSP violated?

A

B

C

Y

"Narrower" really means
"rejects anything that used to be allowed"

Y

LSP

Another Example

LSP - Another Example (1)

public class Square extends Rectangle {
  @Override
  public int setDimensionsReturnSurfaceArea(int height, int width) {
    if (height != width) {
      throw new RuntimeException("For a Square implementation height must match width");
    }
    return super.setDimensionsReturnSurfaceArea(height, width);
  }
}
public class Rectangle {
  protected int height;
  protected int width;
  
  public int setDimensionsReturnSurfaceArea(int height, int width) {
    this.height = height;
    this.width = width;
    return height * width;
  }
}

Valid inputs become more limited with subtype!

LSP - Another Example (2)

public class ShapeTest {
  private Rectangle rectangle;
  @BeforeEach
  public void setup() {
    this.rectangle = new Square();
  }
  @Test
  public void testSetDimensionsReturnSurfaceArea() {
    // THIS WILL THROW AN EXCEPTION!
    int surfaceArea = this.rectangle.setDimensionsReturnSurfaceArea(10, 20);
    assertEquals(200, surfaceArea);
  }
}

Test based on the super type does not pass on the subtype!

The Real World

Is full of violations!

public interface DraftDao {
  Draft getDraft(long draftId);
  // ...
}
@Override
  public Draft getDraft(long draftId) {
  // Drafts are stored in session by 
  // pageId (not by their own id)
  throw new UnsupportedOperationException(
    "the SessionDraftDao does not support Draft getDraft(long id)"
  );
}

That's true for all design principles..

..we should understand the tradeoff.

Lecture Ticket

Lecture Ticket (1)

Is precondition narrower?

Is postcondition wider?

Is LSP violated?

A

B

C

Superclass

Subclass

// REQUIRES: a sorted tree
// EFFECTS: returns true if the element is
// in the tree, returns false otherwise
// REQUIRES: any tree
// EFFECTS: returns true if the element is
// in the tree, returns false otherwise

N

N

N

Lecture Ticket (2)

Superclass

Subclass

// REQUIRES: any tree	
// EFFECTS: returns true if the element is
// in the tree, returns false otherwise
// REQUIRES: a sorted tree
// EFFECTS: returns true if the element is
// in the tree, returns false otherwise

Is precondition narrower?

Is postcondition wider?

Is LSP violated?

A

B

C

Y

N

Y

Lecture Ticket (3)

Superclass

Subclass

// REQUIRES: a sorted tree
// EFFECTS: returns 1 if the element is
// in the tree, returns 0 otherwise
// REQUIRES: a sorted tree
// EFFECTS: if the element is in the
// tree, returns the number of elements 
// in the tree that are larger, 
// otherwise returns 0

Is precondition narrower?

Is postcondition wider?

Is LSP violated?

A

B

C

N

Y

Y

Lecture Lab (1)

class Pump {
  public static final int COST_PER_LITRE = 1.45;

  // REQUIRES: 10 * COST_PER_LITRE  <= amount 
  //		   <= 30 * COST_PER_LITRE 
  // EFFECTS: payment is recorded 
  void prePay(int amount) {
     // stub
  }

  // EFFECTS: returns at least 10 litres of gas
  int dispense() {
     return 0;  // stub
  }
}
class APump extends Pump {
  // REQUIRES: 5 * COST_PER_LITRE  <= amount
  //		   <= 30 * COST_PER_LITRE 
  // EFFECTS: payment is recorded 
  void prePay(int amount) {
  // stub  
  }

  // EFFECTS: returns at least 5 litres of gas
  int dispense() {
     return 0;  // stub   
  }
}
class BPump extends Pump {
  // REQUIRES: 20 * COST_PER_LITRE  <= amount
  //		   <= 30 * COST_PER_LITRE 
  // EFFECTS: payment is recorded 
  void prePay(int amount) {
   // stub 
  }
  // EFFECTS: returns at least 20 litres of gas
  int dispense() {
     return 0;  // stub   
  }
}

Is precondition narrower?

Is postcondition wider?

Is LSP violated?

A

B

C

N-N-N

N-Y-Y

Y-N-Y

N-N-N

?-?-?

?-?-?

?-?-?

?-?-?

Lecture Lab (2)

//REQUIRES: choice is one of "a", "b", "c" or "d"
//EFFECTS: (omitted)
public void handleChoice(String choice) {
  // stub
}

Is precondition narrower?

//REQUIRES: choice is one of "A", "B", "C" or "D"

1

//REQUIRES: choice is one of "a", "b", "c", "d", "e"

2

//REQUIRES: choice is one of "b", "c", "d", "e", "f", "g"

3

//REQUIRES: choice is one of "a", "A", "b", "B", "c", "C", "d" or "D"

4

//REQUIRES: choice is one of "a", "b", "c"

5

N

Y

N

Y

Y

Lecture Lab (3)

//EFFECTS: returns a speed of 1.0, 1.5, 2.0, 2.5 or 3.0
public double computeSpeed() {
  return 1.0;// stub
}

Is postcondition wider?

N

N

Y

Y

//EFFECTS: returns a speed of 1.0, 2.0 or 3.0

1

//EFFECTS: returns a speed in the range 1.0 to 1.5 or in the range 2.5 to 3.0

2

//EFFECTS: returns a speed of 0.0, 1.0, 2.0 or 3.0

3

//EFFECTS: returns a speed of 1.5 or 2.5

4

D2: Liskov Substitution Principle

The End - Thank You!

CPSC210 - D2: Liskov Substitution Principle

By Steven Wolfman

CPSC210 - D2: Liskov Substitution Principle

  • 261