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 return0
- If
compare(a, b) < 0
andcompare(b, c) < 0
, then compare(a, c) must also return a value< 0
. - If
compare(a, b) === 0
, thencompare(b, a)
should also be=== 0
compare(a, b) < 0
andcompare(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.