Re: LSP and subtype
- From: "Andreas Huber" <ahd6974-spamgroupstrap@xxxxxxxxx>
- Date: Tue, 18 Oct 2005 01:58:59 +0200
H. S. Lahman wrote:
Um, why is the "happiness" of some *particular* client code important? As long as all foo overrides respect the invariants, pre- & postconditions that Base documents/relies on then *any* imaginable client code will run just fine with *any* subclass object of Base, won't it? IOW, the client just needs to be happy with the documented behavior (again invariants, pre- & postconditions) of Base and never needs to know that there are in fact subclasses and overrides.
The relationship between the Client and the member of the tree needs to be defined for each client. When evaluating the collaboration the developer must determine where in the tree the Client will access objects. That decision must be made on a client-by-client basis.
For the tree above it is entirely possible that for a given class of Client objects the only behavior that is acceptable is Sub2.foo(). If that is the case the developer must establish a relationship between the Client class and only the Sub1 class. That is the only way ot capture the constraint on the collaboration that the Base.foo() behavior is not acceptable to the Client.
Do you have a real-world example for such a case?
That was pretty much my point. The pitfall from an LSP perspective is that one cannot arbitrarily add new subclass implementations without validating them against the existing client access contexts.
Same disagreement as above, I think you can leave client access contexts out of the discussion.
If Base.foo() and Sub1.foo() are acceptable to the existing clients one still has to make sure that Sub3.foo() is also acceptable. This is a really basic maintenance issue anytime one updates a subclassing tree. If there are overrides it is possible to break the existing clients when an instance of the new subclass is presented to them. To put it another way, validating the clients' access is the only /practical/ way to ensure one is not creating an LSP problem when adding the new behavior to the tree.
That is contrary to my experience. With the usually flat inheritance trees I design and use I very rarely run into an LSP problem anyway. In the few cases when I did in fact run into such a problem it always turned out to be either:
1. a doc problem in the base class 2. The method granularity wasn't right (one method tried to do too much or too little), i.e. the base class interface wasn't thought out well to begin with 3. a violation of the LSP in the newly added derived class
Apropos of your point above about DbC conditions, there is really no way to write such conditions in practice to deal with OO inclusion polymorphism. That's because one is not really substituting different implementations of the same semantics; one is substituting different behavior semantics. So the post conditions between subclasses will usually be different. IOW, adding a new subclass behavior almost always changes the base class' ORed postcondition specification. The only way to effectively deal with that is by raising the level of abstraction of the base class condition specification. So if we have: 1 attacks 1 [Predator] ------------------ [Prey] A | +-----------+ | | [Gazelle] [Impala] + run() + run ()
we can implement a Prey.run behavior for the base class and things will Just Work from an LSP perspective. But now suppose we add:
1 attacks 1 [Predator] ------------------ [Prey] A | +-----------+----------+ | | | [Gazelle] [Impala] [Pheasant] + run() + run () + takeFlight()
Now Prey.run() doesn't cut it. We can fix that by abstracting the postcondition and providing a Prey.flee() behavior. However, it we add: 1 attacks 1 [Predator] ------------------ [Prey] A | +-----------+----------+-----------------+ | | | | [Gazelle] [Impala] [Pheasant] [Brontosaurus] + run() + run () + takeFlight() + stomp() we have a new problem. We can raise the level of abstraction again and upgrade the [Prey] postcondition to Prey.respondToAttack().
This just shows that run() was wrong to begin with. I'm suspicious whether even respondToAttack is right, because technically a predator doesn't/shouldn't really tell its prey to respond to its attack. I think the attack should more adequately be seen as an event to which Prey subclasses will react. That is, in a real-world design there's usually one more level of indirection. But I guess that's what you're hinting at below anyway.
run() is only wrong when one does maintenance and adds new subclasses; in the first situation it was Just Fine.
I don't think so. If I really ever had to develop a game with predators and prey I'm sure I personally would never have thought that run() is a good interface (at least not after going through the minimal experience you get during CS studies).
To ensure that the Base postconditions will /always/ be valid for LSP one would have to be prescient about every possible requirements change that might lead to providing new behaviors.
Well, it totally depends on how much the requirements change. If they really change so much that you can't add a new subclass without breaking the existing Prey interface then yes there's no way around checking every client.
Ok, I guess I'm starting to see why you keep saying that one needs to check existing clients when one adds new subclasses. I agree that you need to do that in the run --> respondToAttack scenario, because you have effectively changed the behavior/interface of Prey. There's no way around that.
I believe that is generally true in OO subclassing because we do routinely provide different behaviors. Base behaviors like "sort" where one can truly just substitute different implementations without affecting the semantics tend to be quite rare. [In fact, I have a hard time thinking of a practical example where such a pure implementation substitution isn't a response to just nonfunctional requirements at the OOD/P level.
What about Streams? The location where a byte-stream goes or comes from is certainly a functional requirement.
[BTW, this problem exists in no small part because the OOPLs do not separate message and method because they are all implemented with type systems and they all employ procedural message passing. If one only needed to define the messages that a [Prey] would accept, most of the LSP problems evaporate.
I don't think that the problems evaporate just because you define messages instead of methods. Even in a message scenario "run" is simply the wrong message. Sending an attackImminent message would much better convey what is happening. In a more complex scenario one would probably more adequately send appropriate sound & vision events, which Prey subclass objects use to establish that an attack is imminent. As mentioned above, Predator should probably not interact with Prey directly. I also think that this has only little to do with procedural vs. message-oriented. In a procedural language there's "only" more work to do. I.e. you have to implement a message-oriented system yourself or use a library.
I think they evaporate because if one truly separates message and response, then the message is just an announcement of something the sender did. That means the sender implementation does not to know about the response, much less depend in any way on what the responder does, so LSP becomes academic.
Right, but as I said, you'd never think of sending a "run" message in such an environment. So "message-basedness" alone doesn't do the job, the problems evaporate because the prey interface was improved. You no longer impratively say "run". If you had chosen respondToAttack() as Prey interface right in the beginning then there wouldn't have been as many problems with adding additional subclasses.
That's because separation of message and method decouples the contexts so there is no expectation of specific behavior on the part of the client. Thus there is no contract with the client per se. The message simply becomes an announcement of something the client did. The developer could then map that announcement to the proper response in the interface. That does involve contract enforcement but it is at a quite different level of abstraction (e.g., a UML Interaction Diagram).]
Right, but that's just an artifact of the rather special relationship Predator <--> Prey. Procedural interaction is usually just fine between everyday objects like FileStream and Whatever, because Whatever can (and must be able to) rely on the documented behavior of FileStream. Predator and Prey are much more loosely coupled.
I could argue that FileStream is a computing space artifact and it is unlikely to change significantly because it is really mathematically defined. Customer problems spaces aren't though, which is what makes constructing software interesting. That is, the OO paradigm is focused on properly abstracting customer problem spaces in a manner that is maintainable in the face of volatile requirements. One technique for doing that is implementation independence, which is what drives the conceptual separation of message and method. One only gets into trouble at the OOP level where they aren't separated.
You keep saying that. You only get into trouble when you don't see that the problem at hand requires messages-based interaction instead of method-based interaction. An architect not seeing that is a bad architect. Surely, a OOPL makes it easy for newbies to fall into this trap but an experienced architect won't.
Then it becomes must easier to make caller implementations dependent on callee responses because of expectations arising from encoding procedural-style Do This calls.
Which are just fine most of the time. It is only for the rather special and rare Predator <--> Prey style relationships where you need and benefit from message-based interaction. Message-based interaction has problems of its own you only want to deal with when you must.
[Base] + foo() A | +---------+--------+ | | [SubA] [Sub3] A + foo() | +-----+------+ | | [Sub1] [Sub2] + foo() + foo()
Not only do we have to perform more surgery on the tree, we have to change the calling context in Client to invoke SubA.foo() rather than Base.foo(). Thus the Client has to understand the tree to invoke the correct property implementations
How could this ever fix the problem above? Sub3.foo() is buggy, no amount of rearranging the inheritance tree will fix that.
It fixes the problem because the Client can unambiguously access the tree at the [SubA] level so it never gets the unacceptable [Sub3] behavior.
Right, that fixes the problem by effectively not calling the buggy code. I'm not sure whether that's very helpful in practice because someone somewhere implemented Sub3 for a reason.
The OOP implementation can then ensure the relationship
But it shouldn't be "buggy" code if that is the way the problem space taxonomy works. The fact is that different subsets of Base objects have three different behaviors (four if Base is not abstract) and not all of those behaviors may be appropriate for particular clients. Going back to the Prey example, consider:
[Prey] A | +---------+---------------+ | | [Bird] [Antelope] + takeflight() + run() A | +---------+-------+---... | | [Gazelle] [Impala]
Now let's say I have two flavors of [Predator], [Hawk] and [Lion] and assume lions can't catch birds that take flight. If the goal of the collaborations is for the predator to satisfy its hunger, then hawks are not going to attack antelopes and lions are not going to attack birds. (It's just an example so let's not pick on the exceptions.) In that case the behavior of takeFlight for [Lion] and run for [Hawk] are inappropriate and could lead to LSP problems. So neither predator should have a direct relationship with [Prey]; instead their relationships should be with [Bird] or [Antelope].
This is a totally different situation than the original one. Please have a look at the quoted context to refresh your memory. We started with a hierarchy with *one* method "foo", where you claimed that some clients calling foo() might only be "happy" with the implementations of Sub1 and Sub2 but not with the one of Sub3. You then went on that rearranging the tree and having the client reference SubA fixes the problem. I said it doesn't because the Sub3.foo() implementation is buggy (violates the LSP). Now you present a totally unrelated example with *two* different methods to back up your original claim. I therefore maintain my position that there must be *some* bug in the software when you have the original hierarchy:
[Base]
+ foo()
A
|
+---------+--------+
| |
[SubA] [Sub3]
A + foo()
|
+-----+------+
| |
[Sub1] [Sub2]
+ foo() + foo()and a client is only happy with Sub1.foo() and Sub2.foo() but not with Sub3.foo(). Note that the crucial point is that we're only talking about 1 method that is overridden in subclasses.
Bottom line: the fact that certain clients should not access certain members of the taxonomy does not invalidate the taxonomy.
That is true in general but not in the context of the original example. Again: We've only talked about one method.
is instantiated only with [SubA] members of [Base]. The downside is that the Client must understand where to access the tree so if the tree changes the Client may become broken.
Agreed, but let's be clear that this is not because something is wrong with subclassing non-abstract classes or overriding methods per se.
I never said there was anything wrong with subclassing; it is one of
Note that I said "subclassing *non-abstract* classes", doesn't that imply implementation inheritance?
the most important tools in the OO arsenal. What I do contend is that implementation inheritance in general and implementation overrides in particular are not good OO practice (in most situations). That's because they open the door to maintainability problems later.
So what's a language that's not poorly formed?
I must have missed that answer...
But OOPLs are crippled by being 3GLS and are necessarily bound closely to hardware computational models.
Oh, are they? How is the Java language bound to a hardware computational model? Or would you say Java is 4GL?
-- Andreas Huber
When replying by private email, please remove the words spam and trap from the address shown in the header.
.
- Follow-Ups:
- Re: LSP and subtype
- From: H. S. Lahman
- Re: LSP and subtype
- From: Dmitry A. Kazakov
- Re: LSP and subtype
- References:
- LSP and subtype
- From: Tony Johansson
- Re: LSP and subtype
- From: H. S. Lahman
- Re: LSP and subtype
- From: Andreas Huber
- Re: LSP and subtype
- From: H. S. Lahman
- Re: LSP and subtype
- From: Andreas Huber
- Re: LSP and subtype
- From: H. S. Lahman
- LSP and subtype
- Prev by Date: Re: Introducing the concept "state" to object-oriented programming
- Next by Date: Re: Data driven people arguments
- Previous by thread: Re: LSP and subtype
- Next by thread: Re: LSP and subtype
- Index(es):