Re: Message-based vs. method-based interaction [was: Re: LSP and subtype]
- From: "Andreas Huber" <ahd6974-spamgroupstrap@xxxxxxxxx>
- Date: Tue, 1 Nov 2005 00:59:07 +0100
H. S. Lahman wrote:
Responding to Huber...
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.
I appears that you didn't get my point. I try one last time: *If* the problem at hand indeed requires separation of message and method then you can implement infrastructure to that effect in just about any OOPL (I don't have to prove that, do I?). Why exactly do you think that an OOPL *forces* us to do anything method-based?
I'm afraid you would have to prove it. You can only do that by overlaying some sort of developer structure on the 3GL syntax (e.g., creating state machines whose events are pushed/popped on a separate queue). Such separation is impossible to do with pure 3GL language syntax.
Oh, and how is this statement not a direct contradiction to what you said in another post:
<quote>
I suspect this won't lead anywhere. Let me rephrase my original question: What *exactly* can one do in a 4GL that cannot be done in a 3GL like C++, Java, C# augmented with appropriate libraries?
This is a variation on the argument that if you give me a big enough computer and enough time, I can model the universe in real time. B-) If you provide enough library augmentation for emulation, you can do pretty much whatever you want. </quote>
?????? (I can't make any sense of this)
I don't see any contradiction. In the first quote I am saying that one can only /emulate/ separation of message and method in the 3GLs by providing added infrastructure beyond what the 3GL provides.
In the second quote I am responding to you saying that you can do it by adding augmentation of the 3GL through libraries. Those libraries are added infrastructure that was created by some developer beyond what the language provides. The point I was facetiously making was that with enough such additional infrastructure one can do anything one wants with the language. But that doesn't change the fact that the language doesn't directly support whatever the infrastructure provides.
You again fail to say how this is relevant.
However, that is not the point. The problem with procedural message passing is that it implements a Do This imperative. That imperative reflects the procedural programming paradigm where software is built with in terms of organizing sequences of operations: Do This, then Do This, then... That paradigm led to the legendary spaghetti code because callers typically had an expectation built into their implementations about what needed to be done next in the overall solution. IOW, the caller could not be specified without also specifying the entire functional decomposition tree down the chain of calls it made. Nor could the procedure be tested without a full implementation of the descendant calling sequence. (Things were even more obvious for functions below.)
The primary contribution of the OO paradigm to maintainability was to break the spaghetti chain by separating message from response. That way the message sender implementation /can't/ depend upon the response; it simply sends a message to announce something that it has done and the developer connects the dots outside the context of individual method specification.
As I pointed out before, though, the real point is to avoid those specification dependencies in the implementation. IOW, it is about how one designs methods and collaborations. If one does that properly so one is not hard-wiring solution sequences in objects, then it doesn't matter if one uses procedural message passing in the OOPL code. The procedural calls are then benign because the calling implementation does not depend in any way on what the callee does.
State machines happen to be one way of doing that. But if the application is to be implemented serially in an inherently synchronous environment, one can eliminate the queue manager and replace event generation with direct procedure calls to target methods during OOP. Then looking at the code one would not be able to tell state machines had been used in the OOA/D and superficially the program would look exactly like any procedural program. One would have to look at the method specifications to realize that there were never any implementation dependencies between objects.
Which leads us to the question why a developer shouldn't implement such a system directly in a 3GL language, without the detour through a 4GL and state machines? I'm asking because I believe the direct 3GL way is often faster.
Because in the 3GL the developer must commit to serial/synchronous vs. concurrent/asynchronous to write the code. In the 4GL that commitment is deferred so that the developer doesn't have to worry about it when focusing on the functional requirements. [And if one is doing translation, the developer /never/ has to worry about it because the transformation engine handles the grunt work.]
As far as development speed is concerned, the 4GL is much faster. There are two basic reasons. First the notation is much more compact, by at least an order of magnitude. (A 250 KNCLOC C++ application's resolution of functional requirements can be fully described in about two dozen UML diagrams that are easily readable on 11x17 pages.) That compaction reflects raising the level of abstraction of the problem by a great deal. In addition UML allows one to focus on quite different views of the solution. Dealing with a much more compact and abstract view of the solution greatly increases the developer's efficiency.
1. I highly doubt that those two dozen diagrams contain *all* the information that is necessary to generate 250 KNCLOC C++ code. All the UML tools I've used required the developer to implement actions in 3GL code, which was never visible in any of the diagrams. Apparently UML2.0 with action semantics gets rid of that 3GL code requirement, but I guess the actions would still not be visible on those diagrams. 2. Even if the diagrams contain all the information then this only means that the code generator does not generate compact code and is most probably not using the more advanced C++ features (templates) and associated techniques (generic programming, meta programming). These would allow for an equally compact representation in code.
The second and more important reason is computing space design reuse. The 4GL can be compiled just like a 3GL, which is what translation tools do. All of the code that addresses nonfunctional requirements is the problem of a different union -- the people who build the transformation engine that automates the optimization. In a large application that can be a rather substantial amount of the code (e.g., for things like read-ahead caching). It also eliminates an enormous amount of technology boilerplate (e.g., one will never see a SQL statement in an OOA model).
3GLs nowadays hide such technology boiler-plate in frameworks (e.g. .NET framework).
Bottom line: model-based translation is the most agile software development technique available and productivity will be integer factors greater than 3GL-based development.
Again, that is contrary to my experience. Rhapsody did a really bad job at speeding up development (it even slowed us down).
There would be none because one cannot define a state action to depend on the response to any event the action generates because there is an arbitrary delay between when the event is issued and when it is consumed. That is painfully obvious with state machines. However, exactly the same thing is possible if one conceptually decouples message and method for /any/ behavior in the OOA/D. Again, superficially the resulting 3GL code would be indistinguishable from procedural code in the way calls were made.
Bottom line: one /always/ wants to build an OO program _as if_ message were separated from method.
Sorry, I don't buy that, primarily for the reason stated above.
Alas, it's not a choice if one is going to do OO development; it is basic ot the paradigm.
Ah yes, and that paradigm was handed down to us by god himself, wasn't it? Seriously, I will never believe people who claim that there is a single right/best/fastest way to construct software for *all* application domains. The problems are just too varied and the details too tricky for that to work out. There may be such a single best way for very narrowly defined domains, but I'm still suspicious. AFAICT, the OO paradigm was defined by *humans* as a reaction to problems they observed in software construction more than two decades ago. While I do believe that a lot of principles still apply today, I also believe that pure OO is really bad at certain things (e.g. containers, stick in a customer get out an object) and that current programming languages have found much better ways to implement those things (e.g. templates, generics). Abstraction in 3GLs did not stop with the advent of OOPLs (for example have a look at what is possible with generic programming in C++: www.boost.org). So really, OO is nice but OO is not everything in software construction.
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.
That approach may be of value for a certain class of problems, but I don't see why anyone would use it to implement everyday objects like e.g. Customer, Address, Order, etc.
It is of value in any OO application. It is fundamental to the paradigm. In fact, if one does not take that OOA/D view one is likely just creating C or FORTRAN programs with strong typing.
You'd have to prove that.
One can argue that the goal of OO development is to eliminate the implementation dependencies that plagued traditional procedural development. The crucial element of the OO paradigm for /ensuring/ that is to conceptually separate message and method in OOA/D.
Which is totally unnecessary most of the time.
So what do you want me to prove?
Well, you claimed that if one does not take that OOA/D view of yours then one likely just creates C or FORTRAN programs with strong typing. Links to case-studies that found that to be true would be a start.
That the primary goal of OO development is maintainability? That implementation level dependencies inhibit maintainability? That the mindset of separating message and method helps prevent such dependencies? It seems to me that I have already demonstrated the last two with the return-value example.
You haven't, see below.
I'm not sure how one would prove the first other than a really tedious literature search from the '70s and '80s.
The 70's & 80's are the stone age of OO. Good literature was really sparse, and good lead programmers with a flair for OO even more so. I wouldn't be surprised if you'd find a bunch of 80's case studies supporting your view. I don't think you'd find that many after say 2000.
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?
There was a misunderstanding. Now I see that in the message-based system you would have chosen "attack" as the message that is sent by the client right in the beginning, i.e. when the hierarchy only contains Impala & Gazelle. The question that remains then is: Why would the a developer choose "attack" as an appropriate message in the message-based approach but not think of respondToAttack in method-based approach? This of
Because that is what the sender context is. The Predator is just announcing what /it/ is doing.
Which is exactly what the Predator developer did when he defined the IPrey interface. If you want to be picky about names you can name that interface IPredatorMsgs or or whatever (which is alright with me).
Note how telling your first sentence is.
It tells that I didn't spend any time on thinking about naming in a quickly hacked example for a thread where I thought that naming would not be an issue. I named that thread "Message-based vs. method-based interaction" for a reason. Consequently, that example was intended as a *technology* *demo* that message-method separation is possible without resorting to fully message-based interaction.
Picture me jumping up and down screaming, "But that's the whole point!"
I almost can't believe what I'm reading here. Are you telling me that the whole point of the last 6-8 posts was about naming in a totally underspecified example for which we never discussed any requirements? Sorry, but I believe that you are either confused, dishonest or not reading my posts carefully enough. I would recommend that you go back and reread the whole thread again from the beginning. In my second post I said:
<quote> 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. </quote>
so I never really disagreed with you that this example *probably* (again: it is hard to tell without proper requirements) warrants a message-based system even on OOPL level.
Later, I challenged the following claim of yours ...
<quote> [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. </quote>
.... a few posts later with the Predator <--> IPrey example where I showed that in our specific example you don't need messages and get away with method-based interfaces on the OOPL level (that's why I also changed the subject line of the thread). Your direct response was over 90 lines long and only the last ~15 dealed with the naming. The other ~70 lines were really beside the point. If this has been about naming only, why exactly would you cloud this single most important argument of yours with such a lot of other stuff? In my admittedly very biased view, this much more looks like someone fighting a losing battle. The moment you saw that you can't uphold your original claim, you quickly changed the subject.
Why is the Predator developer concerned with the Prey interface at all? Why should the Predator developer even know there was a Prey?
While I agree that the naming is suboptimal, I really don't see why you only mention this after 70 lines of unrelated stuff? If the naming was all you had a problem with then why did you answer with more than 70 lines to my asking how the interface-based approach is worse than the message-based approach?
You are assuming it is the Predator developer's job to define the Prey interface. That implies an expectation of Prey behavior on the part of the Predator, which is a quintessential procedural view.
See above. This example was never intended as an example of how one properly names things. It was intended to show how separation between caller and callee can be achieved without resorting to message-based interaction.
course assumes that Prey + subclasses are implemented by the same developer as Predator. If you insist that they by implemented by separate developers who know nothing of each other when they start you still do get away with interfaces and do not need messages at all: public interface IPrey { void RespondToAttack(); }
public class Predator { public void Attack( IPrey prey )
The name confuses things. "Attack" is the message ID for the message sent out of this message. This method in Predator would be something like respondToHunger
This is beside the point. Without proper requirements there is no way to know how to name things.
{ prey.RespondToAttack(); } }
This is where the 3GL type systems get in the way. There is no way for Predator to send a message called "attack" while Prey responds with run, takeFlight, or respondToAttack unless one introduces other infrastructure like event queues.
I don't see how this matters here.
It matters to the implementation of Predator if one wants to decouple the implementations.
No, as you have showed yourself with IPredatorMsgs, event queues are not necessary to decouple Predator from Prey.
The above two classes are implemented by developer A. Simultaneously, developer B implements Impala & Gazelle as follows:
public class Gazelle { void run() { /* ... */ } }
public class Impala { void run() { /* ... */ } }
// and so forth for other Prey classes
Now when the two developers need to hook their stuff together then Developer B simply has his classes implement IPrey:
public class Gazelle : IPrey { public void RespondToAttack() { run(); }
void run() { /* ... */ } }
public class Impala : IPrey { public void RespondToAttack() { run(); }
void run() { /* ... */ } }
Why is this any worse than the message-based approach?
The problem is knowing how to define respondToAttack. In the original case the only relevant behavior for any prey in the problem context was run(). To define respondToAttack one must be prescient about how the tree will be modified in the future. That's because at every level of the tree one needs a clear idea of what the LSP constraints are. (Which is how this thread got started.) Therefore to introduce respondToAttack you have to be able to define its LSP constraints. In particular, you need to define how run() constraints are more restrictive.
What? The message-based approach has the same problem. That is, if the Prey base class promises certain behavior as a response to an "attack" message then run must observe the LSP in exactly the same way as above. If no such behvior is promised then run can do anything it wants.
We are back to documenting the LSP constraints again.
Why?
What LSP constraints does IPrey have in the original development? Are they the highly restricted constraints of run() or the less restrictive constraints of run() AND takeFlight()? That is, when you add:
public class Bird : IPrey { public void RespondToAttack() { takeFlight(); }
void takeFlight() {....}
can any existing client continue to use the IPrey interface if the instance in hand is a Bird? Let's assume the client is a Lion who can't deal with Birds. Can you then add this [Bird] class at all?
You can't because the original IPrey for Lion assumed the restrictive definition by implication because _that is the behavior [Lion] accessed_. You will have to introduce an [Antelope] class with IPrey and some other interface for [Bird] alone and yet another interface for the loose combination that allows [Pterodactyl] to chase all of them.
Bottom line: any time you reorganize implementation overrides in the tree you may have to define more interfaces and, possibly, modify which interfaces the existing clients use.
Now compare this to my solution:
<snip>
public class Antelope : IPredatorMsgs { public void class attack () { this.run(); } }
What is the difference? I use one interface, IPredatorMsgs, at every level of the [Prey] subclassing tree. That is, Prey, Antelope, and Bird all provide the same interface and all clients use it. Each class just implements that interface its own way.
So how do I resolve the LSP constraints? By the level at which I implement the relationship for the collaboration.
[Predator] A | +---------------------+------------------------+ | | | | [Pterodactyl] | | | 1 | | R1 | | | | attacks | | | 1 | [Lion] [Prey] [Hawk] | 1 A | 1 | | R2 | R3 | +------------+--------------+ | R4 | 1 | | 1 | +---- [Antelope] [Bird] -----+ attacks A attacks | R5 +--------+-------+ | | [Impala] [Gazelle]
When I instantiate the R1, R3, and R4 relationships I enforce the LSP constraints by making sure only the right critters participate in the relationship. That decision is based on the collaboration context implicit in the relationship and what the behavior conditions are at each level of the Prey structure, not the interface.
Why is that better? Because I can change my mind later. I can decide that I want Lions to be able to chase Birds too. To do that all I have to do is instantiate the relationship so that it accesses the tree at the [Prey] level like [Pterodactyl] does. When I do that I do not have to touch the [Lion] implementation because it still sends its message to the IPredatorMsgs interface. (In fact, the code that enforces the LSP rules on relationship participation is probably encapsulated in another object entirely!)
Summary: in my solution the collaboration decision that is driven by LSP compatibility is isolated and resolved outside the context of either the [Predator] or the [Prey] implementations. Therefore the implementations of various Predators cannot be affected by changes in the tree. So one can make changes in the tree without touching the [Predator] implementations. Finally, any changes in the tree conditions are isolated exactly where they should be: where collaboration participation is defined.
Why is handling LSP condition changes better isolated in relationship instantiation rather than multiple interfaces? Several reasons. The main one is that one /never/ has to touch the client implementation when the tree changes. The only implementation that ever gets touched is whoever instantiates the relationship and that is a responsibility that is well defined and deliberately isolated in the overall solution, which is the second benefit -- that one always knows where to look.
The third reason is that relationship participation is always defined in basically the same way. One doesn't have to be wary of exotic problem space circumstances; the instantiation will always be the same sort of logic and its correctness will be determined in terms of collaboration participation. The fourth reason is related to the third. Relationships are defined for the class members, not individual properties. So when one validates participation one must look at the overall context rather than individual behaviors, which is the real issue in substitution via inclusion polymorphism.
That is all very interesting but sadly it is still totally beside the point. What I find *very* telling is that you snipped my clarification of my original question:
<quote> Excuse me? Appart from naming and the additional complexity with Lions, Antelope, etc. this looks just like the implementation I proposed. I try to clarify. When I asked
<quote> Why is this any worse than the message-based approach? </quote>
in my last post, then I wanted to know how the method-based approach I presented is any worse than the *equivalent* message-based approach (i.e. with only Predator, Imapala & Gazelle as concrete participants). Your response however, lengthily explains *general* problems with inheritance, naming and so forth. That is, these problems exist in both the message-based and the method-based approaches. </quote>
Any comment on this?
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 * 5
This 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.)
That's why anyone who has ever written unit-tests uses interfaces, as follows:
public interface IWhatever { int doIt( int i ); }
public class ClassB : IWhatever { public int doIt( int i ) { // calculate } }
public class ClassX { IWhatever whatever;
public ClassX( IWhatever whatever ) { this.whatever = whatever; }
void method1( int x ) { int tmp = this.whatever.doIt( x + 3 ); } }
You are still implementing ClassB::doIt.
Nope (I'm rather surprised, as this is really basic testing knowledge): class Mock : IWhatever { readonly int expectedI; readonly int result;
public Mock( int expectedI, int result ) { this.expectedI = expectedI; this.result = result; }
This is just stubbing the expected value from ClassB::doIt.
Nope, it doesn't, see below.
public int doIt( int i ) { // Assert is a class utility of the test infrastructure Assert.AreEqual( this.expectedI, i ); return result; } }
void Test( int a, int b) { ClassX x = new ClassX( new Mock( a + 3, b ) ); x.method1( a ); Assert.AreEqual( b * 5, x.attr1 ); }
Yes, you'd have to call Test with different values to be sure that method1 is implemented correctly, but the same applies to the test harness you would write for your message-based variant. Note that we did *not* implement ClassB.doIt in any way (as you claimed). Nor does ClassX depend on ClassB in any way, it only depends on the IWhatever interface.
Here all you are doing is implementing ClassB::doIt in the test harness; it's just a form of stubbing. As I indicated originally, that is just self-delusion; all one is testing is the test harness. You are just emulating what ClassB::doIt is /supposed/ to do, not what it actually does.
No. Consider one possible implementation of ClassB.doIt:
public class ClassB : IWhatever
{
public int doIt( int i )
{
return i*42;
}
}Now, where exactly in my test harness do you see i*42, or even a hint that ClassB.doIt performs a multiplication? This is of course only possible because in the example ClassX.method1 only stores the result and doesn't depend on the returned value in any way. Now lets assume that it does depend on the value, e.g. it could require that only a positive integer is returned from doIt. Yes, in a very limited way it then does depend on ClassB's implementation. However, this is again beside the point in the discussion method-based vs. message-based because even in a message-based system ClassX would become dependent on ClassB sending back a result that is up to ClassX expectations.
To fully unit test ClassX::method1 you must have a working implementation of ClassB::doIt to ensure the correct value will be returned.
See above.
To put it another way, as soon as ClassB::doIt returns a value that the caller uses, ClassB::doIt's specification just becomes an extension of ClassX::method1's specification. That is, one cannot specify the value placed in 'attr1' for a given 'x' input without specifying what ClassB::doIt does. In that case one needs to validate against both specifications.
Right, but exactly the same applies to a message-based system.
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.
I don't buy that separation in computing_space & not_computing_space because I believe that you *will* have a hard time to make that distinction in just about any non-trivial application. If you think otherwise I would be interested to hear your algorithm that allows the unambiguous distinction for all classes. Moreover, if you say Stack, List & FileStream do not need to be message based for some reason then couldn't it be that objects like Customer & Order also don't need to be message-based?
As I explained when this originally came up, objects like Stack and List are just dumb data holders with no unique problem behaviors while FileStream is just an API to to external software so it /is/ just a bundle of messages. Also, as I think I mentioned, such objects probably would not even appear in an OOA model because they are primarily tactical OOP implementations.
Right, you did mention that, but you still have to say how you make that distinction. For example would OrderList belong to these dumb data holders?
Whether it is a data holder depends on the nature of its behavior. In the strict sense even a simple getter is a behavior, especially at the 3GL level. The key is whether the behavior reflects business rules and policies that are unique to the problem in hand and whether they affect anything except the object's knowledge. If the business rules and policies are from a more general problem space, such as mathematics, or they modify the application state outside the object, then they are behaviors. Otherwise they are just synchronous knowledge accessors and the object is just a dumb data holder. But I've already gone over this so we are going in circles.
Sorry, but this doesn't exactly look like a rigorous definition. When there are multiple developers there will definitely be disagreement over where certain classes belong. So, I still don't buy your separation in dumb data holders & others. All classes have a certain documented behavior. It is largely irrelevant whether a class is part of the business logic or not.
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.
I don't think that example shows less coupling in the message-based approach. ClassX still must know ClassB, ClassX still calls ClassB.doIt and ClassB still needs to respond back to ClassX. I ask again: How is there less coupling in this example?
In the first version of ClassX::method1 the ClassB::doIt method returned a value that was used in the implementation of the caller. That value creates the dependence on what ClassB::doIt does because in order to specify what value is placed in ClassX::attr1, one has to specify how that value is to be computed. Nor can one unit test ClassX::method1 without providing a correct implementation of ClassB::doIt.
Wrong, see above.
I'm afraid it is right because your solution is only testing part of ClassX::method1's specified behavior; the rest is emulated in the test harness.
See above. No matter how you put it, the message-based approach would have the same problem.
In the second version neither ClassX::method1 nor ClassX::method2 depend on anything that ClassB::doIt did. They could be both be fully specified with no reference to anything ClassB::doIt did.
The same applies to the interface-based variant (ClassX only knows IWhatever).
The problem is not with sending a message. The implementation dependency is with getting back a value computed by another object's behavior that is then used by the caller.
Consequently, they could both be unit tested without defining any implementation for ClassB::DoIt.
See above, the same applies to the interface-based variant.
How? In this simple example the specification of the first version of ClassX::method1 is expressed solely in terms of the value placed in attr1 for a given input value, 'x'. When ClassB::doIt returns a value the caller is dependent on that being the right value for its specification of what attr1 will contain for the given input 'x'. The value placed in attr1 will be wrong if ClassB::doIt does not compute its return correctly.
But in my second version ClassX::method2 receives the value as an /input/ argument. That completely eliminates the implementation coupling and allows ClassX::method2 to be fully unit tested solely in terms of its inputs and outputs.
Nonsense. If the message-based variant with method1 & method2 has the same behavior as the method-based variant with only method1 then method2 will depend on the parameter y in exactly the same way as method1 depends on the return value of ClassB.doIt().
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.
If it is simply a question of the mindset the why couldn't a sufficiently experienced developer do "proper OO" (your definition) with the method-based approach?
They could.
That's good to hear. There's hope for us 3GL "dinosaurs".
In fact, those examples of maintainable procedural applications usually reflected exactly this sort of mindset. Recognizing that correlation was crucial to formalizing the OOA/D methodologies. However, in a procedural environment there is no such explicit paradigm and supporting infrastructures to guide the developer.
So we developers are all idiots who need guidance from a 4GL tool, is it that what you are getting at?
OK, that's it. In the passed couple of messages you have gotten increasingly strident.
See below, that's a direct result of your way of discussing things.
Clearly you are no longer interested in discussing this reasonably.
Likewise. Otherwise you wouldn't constantly answer my questions with stuff that's beside the point. It is of course you choice whether and how you answer my posts, but don't be surprised when I follow-up on stuff that doesn't apply or doesn't make sense.
-- Andreas Huber
When replying by private email, please remove the words spam and trap from the address shown in the header.
.
- Prev by Date: Re: LSP and subtype
- Next by Date: Physical structure of source code
- Previous by thread: Re: LSP and subtype
- Next by thread: Physical structure of source code
- Index(es):