How to not violate the Liskov substitution principle when methods have optional parameters?

1.5k views Asked by At

I have a design problem in a program, caused by the fact that an abstract base class has one method with one positional (and thus optional) argument.

Let's say this class is A, and the method is void f(x, [y]);. Now, y is optional because I already know that some of the subclasses of A will use it, some of them will not.

The actual problem is a violation of the Liskov substitution principle: in the subclasses that require y I have to throw an exception if y is not provided, whereas in A.f (which is unimplemented) I'm not throwing any exception.

The design of A is also bad because I'm providing a method f which some subclasses really need it, some of them need a slightly different version. Clearly, I should design my interfaces as small as possible (interface segregation).

This is actually a general question and not only related to Dart.

So, how to handle optional parameters in order not to violate the Liskov substitution principle? And, in particular, how would you handle my situation, so that I don't also violate the interface segregation principle?

The only plausible solution (to my particular problem) I see right now is to make current subclasses that extend A and that actually require y in f to actual extend (or implement, if A is actually an interface) another base class with a method f(x, y), that is where both parameters are required. But the question/problem of how to handle optional parameters still remains!

3

There are 3 answers

4
Sergio On

I don't really see the connection between the LSP and the optional parameters. You'll violate the principle depending on your code, and what you do with the params not because of the options the language provides to you.

In your example I'd say that it's at least arguable you'd be violating the LSP since A is abstract (thus you cannot instantiate it) and you cannot call f directly since it's not implemented.

Let's omit that part and say you have class A and subclass B (both concrete) and both implement method f(x, [y]).

Then asume you have a as instance of A and b as instance of B. Given any value for [x,y] if anywhere you use a.f(x,y) you can use ((A)b).f(x,y) and you get the exact same result then you are not violating the LSP.

Anyway, if you feel you might be violating LSP, then you have to ask yourself if you really need the hierarchy at all. Instead of declaring B as subclass of A, maybe you can have an interface implemented by A and B with the common methods and move code you have in A used by B to another class you can call from A and B (see https://en.wikipedia.org/wiki/Composition_over_inheritance).

0
lrn On

As I read it, the problem is that you want subclasses that are not actually substitutable for the superclass. You want two classes, A and B, to both implement the same API, even if the classes are not really interchangable. One of them only uses one argument (and arguably, should only accept one argument), and the other requres two arguments. Those two classes are just not compatible, so adding a common superclass that somehow abstracts over the incompatbile opreations is destined to fail.

That is, if you already know that some subclasses of A will not use the second argument to foo, then why are they subclasses of A? Because as subclassses of A they should accept any argument that A accepts, and use it in a way consistent with the contract that A.foo documents.

The problem isn't optional parameters, it's optional parametes in the superclass. If a parameter is optional in a superclass, it is necessarily also optional in all subclasses, since the subclasses need to be callable in the same ways as the superclass. A function that takes (x, [y]) cannot be replaced by one that takes exactly one or two arguments, it's the other way around. Subclasses must allow more than the superclass, not less, and going from an argument being optional to not optional is allowing less.

If you have classes

class X { foo(x) {} }
class Y { foo(x, y) {} }
class Z implements X, Y { foo(x, [y]) {} }

then it works because Z allows more than either X or Y. Using Z as a superclass instead of a subclass won't work, it's the opposite direction of what is sound and safe.

0
solidak On

The Liskov Substitution Principle's basic dictum is:

Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.

In other words: - Classes should be modelled based on behaviour rather than properties; - Data should be modelled based on properties rather than behaviours.

So in your case, I'd say that it is a violation since the optional parameter would not be part of the behaviour of the rest of the subclasses. It would be best to have it as part of the subclass that would use it.

Here's a nice image from the amazing SOLID principles!

enter image description here