Re: Message-based vs. method-based interaction [was: Re: LSP and subtype]
- From: "H. S. Lahman" <h.lahman@xxxxxxxxxxx>
- Date: Fri, 21 Oct 2005 19:41:06 GMT
Responding to Huber...
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.
But why worry about it?
You need to worry about it no matter whether you use message-based or method-based interaction. I make one last attempt: When the hierarchy only contains Gazelle & Impala and a short-sighted developer thinks that "run" would be an appropriate name for a message to send to these subclasses then this message is no longer appropriate when you add the other animals to the hierarchy. It's not just the name that is wrong, in a more complex scenario the message might be carrying parameters that become inappropriate when additional subclasses are added. That is, you have exactly the same problem as in the method-based scenario. More on this below.
I was imprecise. I should have said, "Why worry about it when designing the client?"
Separation of message and method allows one to design both the client and the service independently. Then the entire LSP issue comes down to determining where in the subclassing tree the client should access. (See the other message today for why it is important to always specify the tightest contract conditions possible at each level of the tree when defining the tree.)
We only need to have the imperative in the interface because the OOPLs force us to do so.
You keep repeating that too and I really have a hard time to see why. Clearly it must have occurred to you that no OOPL forces you to do anything in any way. This also goes for method-based vs. message-based interaction. You can implement a message-based communication system in just about any OOPL so you are never forced to do anything method-based.
The OOPLs do not separate message and method; the message in the interface is always the method signature. That forces us to decide how to name it. We can name it for the owner context of what it does or we can name it for the client context (i.e., what the client has done to trigger the response), but we can't do both. The convention, which is a carryover from procedural development, is to name it by what it does and that creates an imperative mindset that implies an expectation of what will happen on the part of the client.
That view is, unfortunately, both insidious and pervasive. It allows a very convenient mapping of procedural development paradigms onto OO development. All too much bad OO code exists because client objects are implemented with an expectation of that will happen. The only way to prevent traditional spaghetti code is to decouple the implementations. To do that one needs the OOA/D mindset that clients generate messages, services respond to messages, and the developer is responsible for connecting the dots at a quite different level of abstraction.
When one separates the message and defines it as "attacks", that reflects the client context quite nicely. When one defines a response of "run", "takeFlight", or "stomp", that reflects the service view quite nicely (and rigorously). On the service side one can use conditions to define the LSP context but they really don't matter to the client's responsibilities.
Those conditions only matter to the developer in deciding where the client needs to access the tree. IOW, once message and method have been <conceptually> separated in OOA/D, the LSP resolution becomes a matter of deciding which subset of objects the message can go to and defining a relationship to implement that participation. Implementing that relationship then becomes a mechanical task in the OOPL (e.g., dealing with statically bound type specifications).
That raises defining Base methods to an art form where one must consider all possible present and future contexts.
If I separate the message from the method, then I can name the methods run,
Ok so far. Keep in mind that at this point you have already defined a *message* with the name "run"...
takeFlight, stomp, or whatever is appropriate as subclasses are added.
... which becomes inappropriate here and you have exactly the same problem as in the method-based approach.
How is it inappropriate? It allows me to define each behavior in a very descriptive manner according to its unique semantics. Note that I can't do that in an OOPL because every subclass must implement its method with the same name as the superclass name. [Note that in UML I can have unique, descriptive method names in the subclasses for the superclass' interface. In fact, in UML the object interface is a separate model element from the class definition, which is not the case in the OOPLs where the interface type definition is also the object definition.]
Then I make the message that Base responds to "chases" or
"attacks", which is meaningful as a sender announcement, and I can map that to the relevant behavior.
Nope, see above.
Sorry, I just don't know what you are objecting to here.
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.
But that's why OOA/D is message-based (and behavior access is assumed to be asynchronous). If they are separated, then one can't write client implementations so that they depend on what the response is.
Ah really? I think you ignore the situation where the callee object (e.g. Sub3) is expected to respond with a message of its own.
And I think this is a classic procedural mapping onto OOP. B-) There should be no expectation of a response. For example, the classical procedural overlay would be:
ClassX::method1 (x)
tmp = myClassB->doIt(x + 3)
this.attr1 = tmp * 5This is actually very poor OO code because it creates an implementation dependence in ClassX.method1 on what ClassB.doIt does. For example, one cannot unit test ClassX.method1 without a working implementation of ClassB.doIt. (Stubbing the test harness to return an appropriate value for the provided 'x' is just self-delusion; one is just testing the test harness.)
In a properly formed OO application this method would have to be broken up until each responsibility was logically indivisible. Then one can connect the collaboration dots properly between those logically indivisible behaviors with messages:
ClassX::method1 (x)
myClassB->doIt(x + 3) // pure message w/ data packet
// message ID is doIt
// address is myClassB reference that
// instantiates a relationshipClassX::method2 (x) this.attr1 = x * 5
ClassB::doIt (x)
// manipulate 'x' to produce 'y'
myClassX->method2 (y) // pure message w/ data packet
// message ID is method2
// address is myClassX reference that
// instantiates a relationshipNow when ClassB.doIt computes the new value that ClassX needs, it sends a message to ClassX via calling ClassX.method2 with the appropriate value. [At the OOA/D level the messages would not need to be expressed as method calls; they could be pure messages of {message ID, address, [data packet]}. IOW, they look just like FSM events.]
[Note that to unit test ClassX::method1 all one needs to prove is that a message was issued to the right object with the right ID and data. IOW, once the message is issued, ClassX's responsibilities are over.]
That implementation independence is the real goal and enables maintainability. Once the OOA/D has been structured so that is true, it doesn't matter that the OOPLs use procedural message passing -- the software /structure/ is already decoupled. IOW, in OOA/D the "architect" doesn't have a choice about thinking message-based. (In fact, the translation methodology I use describes /all/ object behavior with state machines so client and service are always decoupled through events.)
While that might be of value for some situations (e.g. Predator <-->
Prey) I really don't see why anyone would ever *implement* "everyday"
objects like FileStream, Stack, List, Dictionary, etc. message-based. As
I mentioned before, message-based communication is not without problems.
IMO the worst problem is that all type-checking is done at runtime what
leaves errors hidden longer than necessary. Moreover, compile-time type-checking is invaluable for maintainability. More on this below.
The separation is far more ubiquitous than that. For example, having behaviors return a value as in the example above is unfortunately all too common in supposedly OO applications. If one separates message and method doing that becomes conceptually impossible and the attending spaghetti code implementation dependencies are eliminated. (Only knowledge responsibility getters return values because knowledge access is inherently synchronous.)
Note that Stack and List are computing space data holders with no intrinsic business rules or policies unique to the problem in hand. They are pretty typical of nuts & bolts computing space objects and they don't need to be message based because they have no problem behavior responsibilities. FileStream is basically just a message API to external software (e.g., a GoF Facade pattern) so invoking it is just sending a message off to the nether regions, which is about as message-based as one can get. I don't know your context for Dictionary, but I would expect something similar to apply.
Note that type checking is a pure OOP issue. The messaging can be expressed at the OOA/D level in a sufficiently generic manner so that it can be implemented in a variety of ways. For example, full code generators for translation can generate code properly for either statically or dynamically typed/bound languages from the same OOA model.
[BTW, the reason the OOA/D assumes asynchronous behavior communication is because that is the most general form; synchronous communication is just a special case of asynchronous. If the end environment is inherently synchronous the asynchronous OOA/D model can be unambiguously converted to a synchronous solution during OOP. However, if one has a synchronous OOA/D model and the target environment is inherently asynchronous, one cannot always convert the model unambiguously.
Another justification is that an asynchronous solution can always be implemented concurrently during OOP while that would not always possible for a synchronous OOA/D solution. Bottom line: the application developer does not need to worry about issues like concurrency or whether the actual implementation will be synchronous or asynchronous at the OOA level and can focus on just the resolution of functional requirements.]
To put it another way, in a well-formed OO application Filestream should be just as "loosely coupled" with its clients as Prey. If that is not the case, then the problem lies with the Filestream implementation.
That brings us to the question how you measure or define coupling. Imagine two equivalent systems, composed of the same objects. One is implemented with message-based interaction and one implemented method-based interaction. Why and how is coupling in the message-based system smaller than in the method-based one?
One example is having behaviors return values, as above. However, there are other ways of creating implementation dependencies, especially when one has lots of state variables (attributes). All of those problems can be avoided provided one designs object behaviors without regard to context. Separating messages from methods goes a long way towards that mindset because one does not even think about what other objects are doing if all one does is generate announcement messages in the implementation of the object in hand.
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.
On the contrary, one gets the benefit of it for /all/ object collaborations -- provided the OOA/D is done properly.
If that were true than we would all be programming with languages like Smalltalk as that seems to come closest to this purist view. Smalltalks great days have long gone and today's languages (like Java & C#) have recently made huge steps towards more compile-time checking. Why would language designers invest so much time into this if message-based interaction were the single-best way to do everything?
Actually, Smalltalk just does a better job of disguising the procedural message passing because one doesn't have syntactic sugar like "()". But the message ID is still the method name and the message is still just a procedure signature (albeit without conventional procedure syntax). When the client sends a message it is still an imperative for the service to do something. (Smalltalk also has a global facility for aliasing method names, which allows at least some separation, but it doesn't work just for individual classes.)
[OTOH, that disguising in one reason why Smalltalk is generally recommended for newbies rather than OOPLs like C++ and Java where the procedural message passing is more blatantly familiar in the syntax.]
Again, I don't think the issue has anything to do with static vs. dynamic type checking. It is about isolating the semantics of sender and receiver and allowing the developer to deal with collaborations, LSP, and all the rest of that at a different level of abstraction than individual object implementations.
************* There is nothing wrong with me that could not be cured by a capful of Drano.
H. S. Lahman hsl@xxxxxxxxxxxxxxxxx Pathfinder Solutions -- Put MDA to Work http://www.pathfindermda.com blog: http://pathfinderpeople.blogs.com/hslahman (888)OOA-PATH
.
- Follow-Ups:
- Re: Message-based vs. method-based interaction [was: Re: LSP and subtype]
- From: Andreas Huber
- Re: Message-based vs. method-based interaction [was: 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
- Re: LSP and subtype
- From: Andreas Huber
- Re: LSP and subtype
- From: H. S. Lahman
- Message-based vs. method-based interaction [was: Re: LSP and subtype]
- From: Andreas Huber
- LSP and subtype
- Prev by Date: Re: LSP and subtype
- Next by Date: Re: 3GL vs. 4GL [was: Re: LSP and subtype]
- Previous by thread: Message-based vs. method-based interaction [was: Re: LSP and subtype]
- Next by thread: Re: Message-based vs. method-based interaction [was: Re: LSP and subtype]
- Index(es):
Relevant Pages
|
Loading