Re: Do Postconditions Include External State Changes?

From: H. S. Lahman (h.lahman_at_verizon.net)
Date: 01/05/04


Date: Mon, 05 Jan 2004 19:23:48 GMT

Responding to Ken...

>>Just to confuse things a bit, one doesn't really need to even include
>>the messages generated in the contracts in the second version.
>
>
> But wouldn't the client of A.method want to know that A's state will
> get changed, ableit indirectly via C.doIt? And wouldn't the client of
> C.doIt (and for that matter B.doIt) want to know that A's state will
> get changed? In fact, I would think that Aref might need to be passed
> in to B.doIt and C.doIt as a parameter.

That would be true if one were doing procedural or functional
development where one views the solution primarily in terms of sequences
of operations.

Remember that the whole notion of 'client' and 'service' is highly
abstracted in an OO context. More precisely, one thinks of 'client'
differently in an OO context. In a procedural/function context,
especially with hierarchical functional decomposition, the client
expects an entire sequence of operations. In the first example the
client would, indeed, expect the entire processing of A, B, and C and
its contract with A would reflect that.

But in an OO context interactions are peer-to-peer and the only contract
is with the receiver of the message. Any collaboration between that
receiver and any other object is an entirely different client/service
relationship. That is, there are never any hierarchical dependencies on
specification. In addition, behavior responsibilities at the object
level are, themselves, encapsulated to be atomic, intrinsic behaviors.

My point here is that one constructs the solution differently because
one thinks about the scope of contracts differently. In an OO solution
the client contract is with the specific behavior that responds to the
message. Whoever sends the method message contracts only with the
behavior. Someone else contracts with A.setAttr1 when that message is sent.

Basically, what it comes down to is that whoever invokes A.method should
not be aware of the whole sequence of operations in that fragment of the
overall problem solution. The only operation it should be aware of is
the immediate one represented by the A.method behavior.

>
> I guess the problem is that doing this would lead to a lot of
> redundent contracts, since A, B and C's methods would all need to say
> that the state change to A is a postcondition.
>
> To avoid this redundancy the client of A.method could look at the
> contract AND the collaboration diagrams for A.method. But then it
> would seem the client shouldn't care about who A.method calls to get
> the job done, since that is implementation. I would think the client
> would just care that A's state changes when A.method is called.

Methodologically the crucial difference lies in the way one views
messages. When sending messages one should think about them as
announcements (I'm Done) rather than imperatives (Do This). In OOA/D
notations message and method are separated for exactly this reason; the
separation allows one to associate the message with a condition of the
application independently of the rules and policies that apply as a
result of that condition prevailing.

That is, the message simply announces that whatever intrinsic behavior
the caller did has been completed. One can then decide who cares about
that at a different level of abstraction where one considers the flow of
control of the overall solution (e.g., a UML Interaction Diagram).

Conceptually one can think of every behavior generating a broadcast
message. One then has an Observer pattern that registers who cares
about that message in the flow of control. One "registers" the
receiver(s) who responds to the message via the Interaction Diagram.
The Observer then dispatches the message to those that care. (The
broadcast view has some problems in detail for conditional messages, but
the basic idea is OK.)

Another analogy is building a shape with Tinker Toys. The analogy
mapping might be:

Wheel <=> object
Wheel Color <=> implementation
Wheel Hole <=> atomic behavior responsibility
Spoke <=> message
Shape <=> problem solution

where one solves the problem with an existing set of wheels by
connecting them with spokes. If one wants to solve a somewhat different
problem (create a somewhat different shape), one connects the spokes up
among the indivisible responsibilities differently. The equivalent
analogy for procedural/functional is the same except some of the holes
have superglue (i.e., some chains of sequences are defined through
hierarchical dependencies).

Like any analogy, it will eventually fall apart if one looks closely
enough. However, the key idea that one starts by identifying objects
and indivisible responsibilities first in OO and then worries about
sequences of those operations to solve the problem. That is in contrast
to the procedural/function approach where one starts constructing
sequences of operations first thing. (Bottom-up sequence definition is
the functional programming view while top-down sequence definition is
the traditional procedural view.)

Yet another way to look at the construction issue is through a variation
on DbC itself. In the scheme of the overall solution there is some
condition that must exist prior to executing any behavior. That may be
as simple as ensuring that a prior use case step has been completed.
Whatever the condition is, it represents a precondition for executing
the behavior in hand.

That precondition will prevail as a result of some other activity being
completed in the application. The completion of that activity can be
represented by a postcondition of that activity. So to determine where
the message triggering a behavior should be generated one just needs to
match the precondition for the behavior to the postcondition of some
other behavior. That's exactly what one does when creating a UML
Interaction Diagram.

So the entire sequence of operations of the whole solution can be
defined piecemeal by simply looking at every method and matching its
precondition to the proper postcondition. More to the point, it can be
done /after/ one has defined all the classes and methods. [As a
practical matter one rarely thinks about it this way; one just does the
Interaction Diagram. But the precondition/postcondition test is very
useful in tricky asynchronous situations.]

Of course for that to work, the behaviors have to be logically
indivisible at the level of abstraction of the subject matter. They
also have to be cohesive and self-contained (i.e., intrinsic properties
of the underlying entity). And their contracts have to be testable
without the implementation of other behaviors. But that is pretty much
what OO encapsulation and abstraction is all about. Ain't is grand when
a Plan comes together?

Bottom line: OO construction involves a different mindset. If you
properly encapsulate intrinsic behaviors, then most of your concerns
above go away. One reason the second version works better is because
the original A.Method is represented by two distinct responsibilities
(A.method and the setter). That allows one to arbitrarily connect the
dots among the objects. The cost of doing so is that the collaborations
become more detailed. In compensation, the contracts for those
collaborations become simpler.

Rules of thumb like never allowing behavior methods to return a value
are just mechanical ways to avoid common procedural dependencies.
Similarly, thinking in terms of I'm Done rather than Do This is just a
mnemonic trick to preserve the right perspective. As a practical matter
most OO developers generate messages to the right object as they define
the caller's behavior.

But if they are doing it correctly, they won't have any dependencies.
BTW, another reviewer trick is to look at a behavior implementation and
envision what would happen if all the calls to other object methods were
put in random order at the end of the calling method. If it won't
affect the results of the caller's execution, then there aren't any
dependencies. [Return values aren't the only way for dependencies to
sneak in. Because methods can update state variables, the order of the
invocation may be a dependency. Any such ordering needs to be expressed
as a collaboration, not embedded in a method implementation.]

*************
There is nothing wrong with me that could
not be cured by a capful of Drano.

H. S. Lahman
hsl@pathfindermda.com
Pathfinder Solutions -- Put MDA to Work
http://www.pathfindermda.com
(888)-OOA-PATH



Relevant Pages

  • Re: implementing roles in OOP......
    ... relationship instantiation from relationship navigation. ... every Element subclass. ... The Client provides the Visitor reference when it invokes ... >>encapsulated as knowledge responsibilities in a single class. ...
    (comp.object)
  • Re: A Design Problem
    ... there are a flock of reasons why one should be worried about the overall design and should look for another way to allocate responsibilities and/or implement relationships when managing collaborations. ... then the client needs to navigate a relationship that is directly to the ... In an OOPL that would be enforced in the type system by making the pointer type be specifically a Button type rather than a Control type. ... But to make that decision the sender method must understand its context in the overall solution. ...
    (comp.object)
  • Re: implementing roles in OOP......
    ... entity level one relies on cohesion -- the individual responsibilities ... must be clearly related at the level of abstraction. ... Car is abstracted as a monolithic entity. ... When Client collaborates with Context it invokes Context.Operation. ...
    (comp.object)
  • Re: delegation vs. inheritance
    ... I intended not to describe implementation inheritance but specification ... dependence between client messages that affects substitutability. ... must also be intrinsic responsibilities that do not depend upon context. ...
    (comp.object)
  • Re: Programming to an Interface
    ... I think the problem you are having with it is related to the point Daniel T. made that you are thinking about it at too low a level of abstraction (i.e., the mechanics of the OOPL de jour rather than as a design issue). ... I think the point here is that one designs the interface first and then the implementation. ... I believe the point here is that interfaces allow one to define responsibilities in terms of invariants, often at a higher level of abstraction than particular implementations. ... One has identical implementation behaviors for both classes but the interface that the client sees is much more generic in the second case. ...
    (comp.object)