Re: Tell, Don't Ask
- From: "H. S. Lahman" <h.lahman@xxxxxxxxxxx>
- Date: Tue, 16 May 2006 18:04:28 GMT
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
.
- Follow-Ups:
- Re: Tell, Don't Ask
- From: Daniel T.
- Re: Tell, Don't Ask
- References:
- Tell, Don't Ask
- From: Squeamizh
- Re: Tell, Don't Ask
- From: Daniel T.
- Re: Tell, Don't Ask
- From: H. S. Lahman
- Re: Tell, Don't Ask
- From: Daniel T.
- Tell, Don't Ask
- Prev by Date: Re: Programming to an Interface
- Next by Date: Re: A question on database access classes - a little long, sorry
- Previous by thread: Re: Tell, Don't Ask
- Next by thread: Re: Tell, Don't Ask
- Index(es):
Relevant Pages
|