Re: Tell, Don't Ask



Responding to Daniel T....

But the client is expecting a element to be returned by Stack. I assert that client already needs to be prepared for the stack being empty. That is, when Stack::empty returns TRUE, then the client needs to do something other than processing an element. However, we can deal with that by having Stack::pop return NULL if the Stack is empty. Then client can test the /result/ for Stack::pop to determine what to do.


Such an expectation flies in the face of command/query separation. I envisioned a Stack with a push, pop, and top method. 'pop' doesn't return the top element in this interface, it simply removes it.

Wow. Not your garden variety stack. B-)

But I don't think that changes anything. It is really about defining the responsibilities of Stack. One can define 'pop' to mean "remove the top element of the Stack, if there is one" and 'top' to mean "return the top element of the Stack, if there is one". Those definitions change the nature of the collaboration so that the client doesn't need to query anything. The client simply needs to know that the condition for needing to collaborate with the Stack prevails.

Of course the client has to deal with the new way of collaborating but that is an implementation issue once the collaboration is defined. There is a chicken-and-egg issue, though, around whether one defines the collaboration before the responsibility details or vice versa. My point here was simply that defining the collaboration first so that the client has minimal knowledge of Stack will lead to a different specification of Stack's responsibilities.

Another example:

class Range:
"invariant: self.getLow() < self.getHigh()"
def getLow(self):
return self.low
def getHigh(self):
return self.high

def setHigh(self, value):
"require: value > self.getLow()"
self.high = value

def setLow(self, value):
"require: value < self.getHigh()"
self.low = value

Sure the invariant will be kept, but not because of any effort on the part of the Range class, users of Range are the ones that have to make sure the invariant holds... But it's not supposed to be their job, that's the job of Range.

Indeed. But easily fixed if one moves the business rules and policies about the relative values of Low and High into Range. For example (ignoring quibbles about equality):

def setHigh(self, value)
if (value > self.low)
self.high = value

Assuming the invariant is all that we are worried about, we really don't need the precondition on value. In that case, even better would be:

def setValue(self, value)
if (value > self.low)
self.high = value
else
self.high = self.low
self.low = value

This is a bit more flexible than your last solution. However, this approach fails when something else in the overall solution context says that the client really never should supply a value less than the current self.low. That is, the precondition is really a correctness check on business rules and policies implemented elsewhere.

For example, the low and high values might need to be consistent as a pair but they are provided from different contexts whose order of invocation is undefined. Then one can't use the second example for sure and may not be able to use the first because 'value' may be out of synch in time (i.e., two high values before a low). I submit that in those sorts of situations the preconditions and your initial solution are reasonable because it really is someone else's job to ensure synchronization of updating the pair of values.

Having said that, I think there are trade-offs that can still be made about the responsibilities of external clients, at least in some situations. Let's assume my example where values must be set in pairs but an arbitrary order prevails. We can add two flags, newHighProvided and newLowProvided, along with two temporary value stores, newHighValue and newLowValue, to Range. Then we might have:

def reset(self)
self.newHighProvided = FALSE
self.newLowProvided = FALSE

def setHigh(self, value)
"require self.newHighProvided = FALSE"
"require self.newLowProvided = FALSE ||
(self.newLowProvided = TRUE &&
value > self.newLowProvided)
self.newHighValue = value
self.newHighProvided = TRUE
if (newLowProvided)
self.setValues()


That precondition forces the client to (a) keep track of what order the messages are sent to Range and (b) help ensure the Range object's invariants. The whole point was for Range to ensure its own invariant. I don't think it's right to foist that off on Range's client. "Tell, don't Ask" remember? With the above, the client of range must first ask the object if it's OK to call a particular function, and if the value being passed in is OK.

No, the purpose of the flag complexity is to eliminate any external knowledge of ordering for (a). (There is a symmetric precondition for setLow that allows either one to be invoked first.) The context simply needs to ensure that values are provided in pairs.

As for (b), the context needs to ensure consistency with the semantics of Range when supplying values. But I argue that is already necessary as soon as the interface is defined as setHigh and setLow. The new invariant...

def setLow(self, value)
// symmetric with setHigh

def setValues
"invariant self.newLowValue < self.newHighValue"
self.low = self.newLowValue
self.High = self. newHighValue

....here acts as a correctness assertion on the overall processing. That is, in the hypothesized problem there could be different clients for setHigh and setLow. Then ensuring consistency in the setup of a Range potentially depends upon complex collaborations in the external context among multiple objects.

How does one capture that via DbC? The most logical place is where those factors come together in Range. The invariant, indeed, represents an intrinsic characteristic of the notion of 'range'. But it is also a constraint one how the client context must work. IOW, the client context must share the same semantic view of Range's responsibilities that the invariant enforces.

Bottom line: I disagree somewhat with the implication that clients only care about preconditions. Both the discipline of the context collaborations and the Range invariant are rooted in the same shared semantics of 'range'. Thus the clients must obey the invariant in their own way just as much as Range. Coming full circle back to interfaces, that is manifested (in part) by the setHigh and setLow interface methods.


This changes teh client contracts in subtle ways. Now all we need for the context to understand is that Range::reset must be invoked before new values are determined and that values must be supplied in pairs. The synchronization of producing consistent pairs is enforced by the precondition, not the relative value. Indirectly the context still needs to know that the high value should be greater than the low value. But that is implicit in Range::setHigh and Range::setLow already. The invariant is really a correctness check that /pairs/ are produced correctly. Overall I think this is probably a better way to capture the constraint nuances of the external processing than checking relative values _given the stipulated problem_.


I would be quicker to make a group setter:

def setRange(self, new_low, new_high):
"require: new_low < new_high"
self.low = new_low
self.high = new_high

I agree. Enforcing consistency and pairing is much easier this way. But as I indicated, the implications of setHigh and setLow are that one can't do that conveniently (e.g., the high and low values are determined in different places).

*************
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
Pathfinder is hiring: http://www.pathfindermda.com/about_us/careers_pos3.php.
(888)OOA-PATH



.



Relevant Pages

  • Re: Tell, Dont Ask
    ... That precondition forces the client to keep track of what order the messages are sent to Range and help ensure the Range object's invariants. ... The whole point was for Range to ensure its own invariant. ... But there is a big difference between a contract that requires the context to provide values in a specific order and a contract that requires the context to provide pairs of values in any order. ... Here my solution was constrained by separate setHigh and SetLow interface methods in the original example. ...
    (comp.object)
  • Re: ObjectInputStream$HandleTable$HandleList
    ... > After a few thousand objects, the client experiences a java.stack overflow. ... > causing the stack overflow. ... > instance count at 143579 before the stack overflow. ... If the heap gets too large, shouldn't it throw a outOfMemory error? ...
    (comp.lang.java.programmer)
  • Re: Tell, Dont Ask
    ... that client already needs to be prepared for the stack being empty. ... That is, when Stack::empty returns TRUE, then the client needs to do ... def getHigh: ... Sure the invariant will be kept, but not because of any effort on the ...
    (comp.object)
  • Re: Killing the module from the own module code
    ... The SVC stack, which we are considering, is not a region of memory which is expected to be used for the execution of code, and thus is not protected by clients which expect to modify it with Instructrion Memory Barrier. ... The clients which perform this operation may be limited, but do exist and a particular example (TaskWindow) is not 'rare'. ... Any client which performs a similar operation to that of TaskWindow in regard to a process swap may do the same. ...
    (comp.sys.acorn.programmer)
  • Re: STA cannot prevent multiple client calls accessing at the same time??
    ... Your call stack clearly shows that C yields. ... to flag the deletion request and do nothing further if you are ... And I don't use windows message loop in the dll, only the Client ...
    (microsoft.public.vc.atl)