Here's the actual principle:
Let q(x)
be a property provable about objects x
of type T
. Then q(y)
should be provable for objects y
of type S
where S
is a subtype of T
.
And the excellent wikipedia summary:
It states that, in a computer program, if S is a subtype of T, then objects of type T may be replaced with objects of type S (i.e., objects of type S may be substituted for objects of type T) without altering any of the desirable properties of that program (correctness, task performed, etc.).
And some relevant quotes from the paper:
What is needed is a stronger requirement that constrains the behavior of sub-types: properties that can be proved using the specification of an object’s presumed type should hold even though the object is actually a member of a subtype of that type...
A type specification includes the following information:
- The type’s name;
- A description of the type's value space;
- For each of the type's methods:
--- Its name;
--- Its signature (including signaled exceptions);
--- Its behavior in terms of pre-conditions and post-conditions.
So on to the question:
Do I understand correctly that Liskov Substitution Principle cannot be observed in languages where objects can inspect themselves, like what is usual in duck typed languages?
No.
A.class
returns a class.
B.class
returns a class.
Since you can make the same call on the more specific type and get a compatible result, LSP holds. The issue is that with dynamic languages, you can still call things on the result expecting them to be there.
But let's consider a statically, structural (duck) typed language. In this case, A.class
would return a type with a constraint that it must be A
or a subtype of A
. This provides the static guarantee that any subtype of A
must provide a method T.class
whose result is a type that satisfies that constraint.
This provides a stronger assertion that LSP holds in languages that support duck typing, and that any violation of LSP in something like Ruby occurs more due to normal dynamic misuse than a language design incompatibility.
This particular implementation, yes. If you make the states concrete classes rather than abstract implementors then you will get away from this.
However the state pattern you're referring to which is effectively a state machine design is in general something I disagree with from the way I've seen it grow. I think there is sufficient grounds to decry this as a violation of single responsibility principle because these state management patterns end up being central repositories for knowledge of the current state of many other parts of a system. This state management piece being centralized will more often than not require business rules relevant to many different parts of the system to reasonably coordinate them.
Imagine if all the parts of the system which care about state were in different services, different processes on different machines, a central status manager that details the status for each of those places effectively bottlenecks this whole distributed system and I think that bottleneck is a sign of a SRP violation as well as generally bad design.
In contrast, I would suggest one make objects more intelligent as in the Model object in the MVC pattern where the model knows how to handle itself, it doesn't need an outside orchestrator to manage it's internal workings or reason for it.
Even putting a state pattern like this inside of an object so it is only managing itself, it feels like you would be making that object too large. Workflows should be done through composition of various self-responsible objects I would say, rather than with a single orchestrated state which manages the flow of other objects, or the flow of intelligence within itself.
But at that point it's more art than engineering and so it is definitely subjective your approach to some of those things, that said the principles are a good guide and yes the implementation you list is an LSP violation, but could be corrected not to be. Just be very careful about the SRP when using any pattern of this nature and you're likely to be safe.
Best Answer
Neither "transitive dependency towards the concrete elements" nor "supertype knowing about the subtypes" leads to a conclusion of LSP being violated. The first one is harmless. The second one is merely a smoking gun.
Visitor pattern can be used for several purposes. Judging from the way this question is asked, this answer will put the focus on the case of Double Dispatch.
Visitor pattern might also be used for other purposes. The possibility of those other purposes violating OCP or LSP is practically nil; therefore they will not be discussed in this answer.
In cases where the Visitor pattern is used to implement Double Dispatch (or equivalently to get around the lack of double dispatch in OOP languages), it is often said to be incompatible with Open/Closed Principle (OCP), although generally we can't say much about whether it violates Liskov Substitution Principle (LSP) unless there is an implementation mistake.
This article on Double Dispatch explains it better than I do.
An example of double dispatch is that you have one inheritance hierarchy of
Shape
, and then a different inheritance hierarchy ofDisplay
(orISurface
in the article linked above). The project requires an implementation of a polymorphic method, whose behavior varies with both the concreteShape
subtype and the concreteDisplay
subtype. If you have 3 concreteShape
subtypes and 5 concreteDisplay
subtypes, this requires 15 concrete implementations of the polymorphic method, i.e. it is a Cartesian product of implementations.The most widely used OOP languages do not support Double Dispatch out-of-the-box. There are several ways to get around this; using the Visitor pattern is one way.
However, using the Visitor pattern to achieve Double Dispatch requires you to lock down one of the two inheritance hierarchies: you will not be able to add support for new subtypes to the locked-down inheritance hierarchy anymore (without modifying the supertype/interface). Using the above example, you have to choose either: (1) lock down the
Shape
hierarchy, or (2) lock down theDisplay
hierarchy. This breaks the "open for extension" part of the OCP.If the nature of one of the inheritance hierarchy is that it is "complete", i.e. there will never be a future necessity to add any new subtypes to that hierarchy, then locking it down is not an issue, so it does not violate OCP.
There is an alternative implementation of Double Dispatch that does not break OCP, i.e. that will allow adding new subtypes to both the
Shape
andDisplay
inheritance hierarchies, without modifying existing code. However, this alternative implementation does require checking the concrete types of both arguments viainstanceof
.Some would argue that checking the concrete argument types with
instanceof
is itself an antipattern, or a smell, or something undesirable. This is somewhat an exaggeration of the original opinions held by Java mentors, which was to prefer polymorphism over instanceof and downcasting, not to outright forbid it even when necessitated by software requirements.This brings up another discussion. In light of agile methodology, it is often the case that new software requirements necessitate code changes to certain parts of the project that were once considered immutable. In other words, breaking changes are sometimes needed.
From this perspective, the Open/Closed Principle (OCP) itself often came up as being too rigid/inflexible.
In the past, violation of OCP will cause harm to compiled libraries (binaries) which are then packaged and shipped. Propagating breaking changes in upstream dependencies often has a ripple effect that result in a large number of compiled libraries to be updated.
Agile methodology intends to solve this issue with:
Agile methodology also views violations of Liskov Substitution Principle in a different way. Instead of branding a violation as a sign that the system or design is unsafe or having questionable correctness, it is merely seen as being a pain point. So, a LSP violation is not a death knell, but is a bad mark that adds up.
There are plenty of new software requirements which require the violation of LSP. In the past, such feature requests will be rejected on the grounds of correctness. Nowadays, it will be given business considerations. If a violation of LSP is accepted, then it shall be properly documented.
The additional cost of documenting an LSP violation, the displeasure of a programmer caught by the astonishment of software breakage (due to not heeding the documented warning), and the software maintenance and support overhead stemming from such breakage, are seen as business costs that are weighted against the business gains from implementing the requested feature.