Design Choices: Return Values and Mocks
Join the DZone community and get the full member experience.
Join For Free
let's suppose you have a graph of collaborating objects, such as this:
to perform some action, you have to send a message to the facade object, that will collaborate with the rest of the graph to produce a result. there are at least two basic ways in which the output of this computation can exit this object graph:
- through the return value of facade.
- by being sent to a target object, passed in the initial call.
the second choice works like this:
facade.dosomething(target target, ...) // somewhere in the graph: target.accept(result);
in this article i want to investigate which contexts call for the first solution, and which for the second one.
equivalence
first of all we can say there is not much difference between the solution in some basic scenarios: it's just a matter of style. the first style reminds me of functional programming, while the second of the callback style of javascript applications, since its non-blocking nature makes the target objects and functions a necessity.
the cost of the solution in terms of code is similar. in the first case you have:
- a return statement on the facade
- a return statement on the intermediate objects
- a return statement on the leaf objects
in the second:
- a target interface
- an additional parameter on the facade
- an additional parameter on the intermediate objects
- an additional parameter on the leaf objects
not that in some languages both the return statements and the interface construct may be implicit, so i'm not talking about lines of code and their length here; i'm counting the cost in time that we have to pay every time we read the code: a method with 3 parameters is relatively more difficult to understand with respect to the same method without one of them; in the same way, a
void
method is easier to reason about and modify than a method that returns a result.
dimensions: who produces the result
the functional approach certainly wins when the final value cannot be directly produced by the leaf objects.
for example, if you are representing a mathematical expression such as (1+2)*(3+4) with a composite pattern, the value of the whole expression can only be computed in the facade object. in this case, you only have the choice of putting a return statement there.
consider instead the case where a leaf has to be chosen, and is capable by itself to produce the result; for example, you're choosing which url to redirect the user to, and each leaf generates a different one while the rest of the graph chooses the leaf to ask. here passing a target object to the leaf is possible.
dimensions: unit testing
in the second scenario above, unit testing of the facade and the intermediate object is influenced. in the case of return statements, we have a stub and an assertion:
leaf leaf = mock(leaf.class); intermediate intermediate = new intermediatea(leaf); allow(leaf.dosomething()).andreturnvalue(23); assertequals(23, intermediate.dosomething(input, ...);
testing delegation is verbose (forgive my rusting mockito skills). consider instead using the target object:
target target = mock(target.class); leaf leaf = mock(leaf.class); intermediate intermediate = new intermediatea(leaf); intermediate.dosomething(input, ...); verify(leaf).dosomething(target);
and we can just test that
target
is passed down.
dimensions: flexibility of the result
moreover, this ease of unit testing persists even for maintenance, as you can change the target interface without facade and intermediate objects having to know. you can refactor from:
interface target { public void accept(string result); }
to:
interface target { public void accept(string result, int anotherfield); }
by changing only the leaf objects. the way you can achieve this in the return-based solution is by introducing an additional value object called result, and wrap the string in there so that other fields can be added later without intervention on the return types of facade and intermediate.
dimensions: producing a result at all
in some cases you may want to
not
produce a result. in the return-based solution, this requires a null value or a null object:
result = facade.dosomething(...); if (result != null) { // use it }
while this is easier with the target object:
class leaf1 { public void sosomething(target target, ...) { // call target.accept(), or do not call it } }
and this form also scales to multiple results of the same type:
public void dosomething(target target, ...) { target.accept(23); target.accept(42); }
conclusions
we have seen that different contexts call for different forms of the code to accomodate the requirements; moreover, changes we want to support are better dealt with one of the solutions with respect to the other. design means searching for the appropriate form of your code, not sticking with the one you know better: as you see in the multiple results example, disruptive changes for one form are very easy to make in another design. don't choose blindly...
Opinions expressed by DZone contributors are their own.
Comments