magva
magva

Reputation: 101

How does the Liskov Substitution Principle works with type specialization in concrete implementations of abstract classes?

Suppose we want to model different kinds of food; pizzas and pies. Both pizzas and pies do have a sort of topping, but their toppings are different.

We implement an abstract class Baked, and two concrete classes Pizza and Pie. We also implement an abstract class Topping, and two concrete classes PizzaTopping and PieTopping.

By definition, any concrete implementation of Baked must have a Topping, so Baked should have an attribute of type Topping. Also, this specific topping attribute should be of type PizzaTopping in Pizza, and PieTopping in Pie.

If I understand this previous answer correctly, the LSP is violated in this situation, because if we set aside the fact that Baked is abstract, it could be possible to instantiate a Baked holding a PieTopping, but that is not possible if we substitute Baked with Pizza.

My question is; is there a way to implement this model without violating the LSP?

Upvotes: 1

Views: 50

Answers (2)

nik0x1
nik0x1

Reputation: 1461

Let's try to carefully analyze your question.

Liskov substitution principle (LSP):

If a class S is a subclass of class T, an object of class T should be replaceable by an object of class S without altering the desirable properties of the program.

In your case, this means the following: if you have some code (for example, a method f) that takes Baked as parameter, then if you pass instance of Pizza or Pie as parameter to it, it should work correctly.

It follows from this statement that we cannot talk about compliance with the LSP without understanding method f, as well as the implementations of methods Baked in classes Pizza and Pie. Because this information directly affects the understanding of the "correctness of the program".

Upvotes: 1

Mark Seemann
Mark Seemann

Reputation: 233347

I'll address the immediate concern first, but subsequently, I'll have something to say about the overall assumptions implied by the question.

Using C#-like pseudocode, you could define Pizza like this:

public class Pizza : Baked
{
    private readonly PizzaTopping topping;

    public Pizza(PizzaTopping topping) : this(topping)
    {
        // Consider saving `topping` to a private or protected class field
        // so that you don't have to downcast any `topping` field from the
        // base class:
        this.topping = topping;
    }
}

Then do the same for the Pie class.

This may now prompt the natural question: Why even have an abstract Topping class?

Indeed, based on the information in the OP, we don't have enough information to answer such a question. Does Topping define some polymorphic behaviour that derived classes can implement? If so, what is it? And does any of this even relate to the Liskov Substitution Principle (LSP)?

It's unfortunate that universities, bootcamps, and online courses alike keep insisting on teaching object-oriented design from the notion that it's about modelling concrete objects in the real world. That may be how Simula started out, but rarely turns out to be the best way to structure code in languages like Java or C#.

The LSP isn't about structure, but about behaviour. The modern understanding of the LSP is actually captured quite well by the Wikipedia entry, which discusses that

  • preconditions cannot be strengthened in the subtype
  • postconditions cannot be weakened in the subtype
  • invariants cannot be weakened in the subtype

All this (pre-, postconditions, and invariants) are, in the sense of Bertrand Meyer's view of OOD, part of a type's contract. Only the designer of a type can say what the contract is, and since the OP doesn't state that, we can't really answer whether or not a given design violates the LSP.

Upvotes: 1

Related Questions