-1

I thought I understood Liskov Substitution Principle but then I thought of a case and I wanted to ask the community here if I get it right.

So I read somewhere to check if we have violated Liskov or not, amongst other things, we also need to inspect the client code which is using the classes in question.

So I have an example below, please help me understand if I have broken LSP.

class A {

 void doWork(){
    // superclass specific implementation
 }

 void doSomeOtherWork(){
    // superclass specific implementation
 }
}

class B extends A {
 void doWork(){
    // subclass specific implementation
 }
}

Client Code NOTE: Let's assume that the client code is fixed, and cannot be modified.

class WorkProcessor{

 void process(A a){
   a.doWork();
 }
}

Now in general it is true that superclass A has a behavior i.e doSomeOtherWork() which is not relevant to subclass B, so ideally this would violate LSP as subtype cannot be replaced by supertype.

But looking at the client code, and assuming hypothetically that the client code is fixed, we can see that a.doWork() is getting invoked and it will work even if we pass an instance of subclass B.

So in the context of the client, we do not violate the LSP.

Am I right in making the above statement?

4
  • 2
    B just uses the inherited doSomeOtherWork, how does that break LSP? On the other hand you have broken the ISP because the WorkProcessor is still exposed to a method it doesn't care about.
    – jonrsharpe
    Commented Nov 28, 2022 at 13:42
  • 2
    A has a behaviour i.e doSomeOtherWork() which is not relevant to subclass B, so ideally this would violate LSP Why. In what way does it break LSP? To me is just a flawed abstraction, but that's it.
    – Laiv
    Commented Nov 28, 2022 at 13:42
  • @Laiv If WorkProcessor was using a.doSomeOtherWork() then it does violate LSP. Because B although inheriting doSomeOtherWork() but let us say it is not relevant to it. But since WorkProcessor is calling a.doWork() then it does not violate LSP. So is LSP depends on the context in which the client code is using the classes in question. Commented Nov 28, 2022 at 13:53
  • 3
    LSP doesn't depend on the context (caller or callee). The example is quite meaningless and poor for us to explain (easily) why B is not breaking LSP. I would edit the question and add "what do I think LSP means" and why I think B break it. Maybe the issue is in the interpretation you give to LSP.
    – Laiv
    Commented Nov 28, 2022 at 14:17

2 Answers 2

2

A class doesn't violate LSP by merely having an extra method. The behaviors that LSP talks about are not methods, but rules and relationsips and constraints that the class is meant to follow/enforce as you poke it in different ways (the rules governing the client-observable transformations as you call various methods on the type). E.g., in the (in)famous Rectangle-Square example, the behaviors are not "I can set width" and "I can set height", the relevant behavior (the contract, what the type promises to be the case) is "I can set width and height independently". This requirement, for the purposes of that example, is not inherent in the interface of the Rectangle (although it might be natural to assume that it holds), this is imposed externally by the developer - and this is what gets broken in the Square "subtype" (which then makes it not be a real subtype by LSP definition).

That said, you are right in that a that class that derives or implements something else (or, in case of structural typing, has a structure compatible with something else) can violate LSP in one context, but not in a different one.

But it doesn't matter so much what the client code is doing, what matters is what it expects of the type it consumes. That is, it's more about the behavioral contract of the type it makes use of.

Now, in a statically typed language, you'd associate that contract with a role-based interface. That is, instead of:

class B extends A {
 void doWork(){
    // subclass specific implementation
 }
}

you'd have something like this

interface Worker {
  void doWork()
}

class B extends A implements Worker {
 void doWork(){
    // subclass specific implementation
 }
}


class WorkProcessor{

 void process(Worker a){
   a.doWork();
 }
}

But in a dynamically typed / duck typed language, there wouldn't be such an interface. You'd have to look at what's expected of that parameter in that context - and you'll find that in the documentation. (Most programming languages, loosely or strongly typed, are not capable of fully expressing those expectations in the interfaces/classes themselves).

E.g. look at the sort() in JavaScript; it can take in a compareFn - something that tells it how to compare two values, that it can then use to sort an array of values it otherwise doesn't understand.

Now, as Laiv pointed out in a comment, it's hard to meaningfully break LSP for a general-purpose sort, but suppose you were writing your own library that passed a compareFn to a sort it used internally, and required that compareFn was such that it established what mathematicians would call a total order on the elements you were iterating over - that is, imagine your code relies on the fact that there is a sensible way to order the elements, otherwise, it might return unpredictable results, or even throw. Clients of your library would supply a compareFn to it, but what they pass in must satisfy certain behavioral requirements.

The signature of compareFn tells you that it takes two values, and returns a number: (a, b) -> Number. A signature is like a formal type of the function.

But the behavior of the function, the thing that fully defines a type in the LSP sense, would be specified like so.

First, it would have to follow this specification from the documentation of the sort() function.

compareFn(a, b) return value sort order
> 0 sort a after b
< 0 sort a before b
=== 0 keep original order of a and b

Second, because of your ordering requirements, the implementation would have to make sure that these things are true:

  • compare(a, a) must return 0
  • If compare(a, b) < 0 and compare(b, c) < 0, then compare(a, c) must also return a value < 0.
  • If compare(a, b) === 0, then compare(b, a) should also be === 0
  • compare(a, b) < 0 and compare(b, a) < 0 cannot both be true

Note that clients can relatively easily supply a function that violates any of these (perhaps in a non-obvious way - maybe they are doing some calculation to determine the order, or pulling values from some precomputed table), and if they do, this will break the expectations of your code.

Aside: the term "behavior" is really mathematical/academic jargon - it refers to these rules/constraints/invariants that govern what happens when you do something to an object (when you pass a certain set of parameters, or as it transitions from one state to another, etc).

So, the function supplied by the clients of your library needs to respect these constraints if they want to make us of it. If they don't do this, they are either doing something hack-y on purpose (accepting the risk that their code might break if internal details of the library change), or the implementation of the comparison function they provided is breaking LSP unintentionally, and they might end up with a bug or undefined behavior.

This example just showcases a behavioral specification of a single function, but you can imagine that you can do this for classes, and more importantly, for interfaces - and it doesn't have to be this abstract or mathy in nature.

The problem with your example is that it doesn't have any nontrivial client-observable behavior in the above sense - it's just a fire and forget function call.

5
  • Hm. I'm struggling with the idea of strategy pattern breaking LSP. How a function breaks sort()? If the given function alters any of the components so that the next comparison gives a different and unexpected result, doesn't break LSP. It's a weird sort indeed, but still a sort. Isn't? I'm also confused about breaking LSP in non-typed languages. If there's no type, then there's no LSP to break. There's no type replacing in the first place. Is LSP also relevant, lets say, in functional programming? Or in non-OO paradigms?
    – Laiv
    Commented Nov 28, 2022 at 15:40
  • @Laiv I guess a general purpose array sort is somewhat of a bad example, but I needed something simple that had a documented behavior. I might replace it with something better later. But, suppose you have your own domain-specific sorter function that takes in a compareFn, and requires all this, and in addition requires compareFn to produce a well-defined order on the element type - because your code relies on that property. So, no rock-paper-scissors loops, a < b and b < c has to imply a < c, etc. You'll break your library if you supply a compareFn that doesn't do that. Commented Nov 28, 2022 at 15:50
  • Also, it might not break sort itself, but it might break your code that calls sort and makes assumptions about what it returns. Commented Nov 28, 2022 at 15:52
  • @Laiv - I used the example from the comments in the answer, hopefully, that gets the point across a little better. But the difficulty you pointed out has to do with how general the sort/compareFn combination is, not with the strategy pattern. You can absolutely break LSP in the strategy pattern - it's just a matter of what the contract of the strategy is. Commented Nov 28, 2022 at 16:33
  • And yet I don't think a weird compareFn can break sort. You can implement different sorting strategies without breaking LSP. If the wrong order of items breaks the code that has nothing to do with LSP (IMO). It would be like implementing a wrong loop. The sort works and does what it's expected: "given a set of elements, they are sorted according to the logic defined in F". If you make sort parametrizable with a strategy pattern, you can not blame the implementation of the strategy if your code stops working. If a specific order is required, then you don't implement a strategy pattern.
    – Laiv
    Commented Nov 28, 2022 at 16:46
1

Now in general it is true that superclass A has a behavior i.e doSomeOtherWork() which is not relevant to subclass B, so ideally this would violate LSP as subtype cannot be replaced by supertype

This is more an issue of incorrect inheritance than anything to do with LSP.

LSP says, basically, that any code that works fine with the parent class should also work fine with a child class, that the code calling it shouldn't care.

So if you have a Mammal class that has method milkOffspring then any child class of Mammal, such as Dog or Cow should work just fine where ever you might put Mammal in. It should never break because code that expected to get a Mammal class instead got a Dog class (it get a bit more formal than that, but that is the gist of it)

In your example though if A has behaviour that is not relevant to B then B is not a type of A

It would be like saying there is a fly() method in Mammal that only some child classes care about, but Dog doesn't care about. That is broken inheritance.

As others have pointed out B gets doSomeOtherWork via inheritance but you seem to be saying that doSomeOtherWork is not a method B cares about

In which case your inheritance tree is incorrect and you probably want to switch over to composition or something.

So there is a more general principle you are violating here, which isn't LSP. It is that you are putting specialization into parent classes, specialization that some child classes don't care about.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.