A very common design decision is whether to pass a dependency to the class via constructor (so the class uses the dependency to get a required value) or pass that value directly to the method. As always, there is no right or wrong way. However, there is a trade-off you must understand to make the best decision.
Let’s use the ChristmasDiscount
example, as it fits this discussion perfectly. The following listing shows the code again.
Listing 7.13 ChristmasDiscount
class, one more time
public class ChristmasDiscount {
private final Clock clock;
public ChristmasDiscount(Clock clock) { ❶
this.clock = clock;
}
public double applyDiscount(double rawAmount) {
LocalDate today = clock.now(); ❷
double discountPercentage = 0;
boolean isChristmas = today.getMonth()== Month.DECEMBER
&& today.getDayOfMonth()==25;
if(isChristmas)
discountPercentage = 0.15;
return rawAmount - (rawAmount * discountPercentage);
}
}
❶ We can inject a stubbed version of Clock here.
❷ Calls the now() method to get the current date
The ChristmasDiscount
class needs the current date so it knows whether it is Christmas and whether to apply the Christmas discount. To get the date, the class uses another dependency, which knows how to get the current date: the Clock
class. Testing ChristmasDiscount
is easy because we can stub Clock
and simulate any date we want.
But having to stub one class is more complex than not having to stub one class. Another way to model this class and its expected behavior is to avoid the dependency on Clock
and receive the data as a parameter of the method. This other implementation is shown in listing 7.14. Now the applyDiscount()
method receives two parameters: rawAmount
and today
, which is today’s date.
Listing 7.14 ChristmasDiscount
without depending on Clock
public class ChristmasDiscount {
public double applyDiscount(double rawAmount, LocalDate today) { ❶
double discountPercentage = 0;
boolean isChristmas = today.getMonth()== Month.DECEMBER
&& today.getDayOfMonth()==25;
if(isChristmas)
discountPercentage = 0.15;
return rawAmount - (rawAmount * discountPercentage);
}
}
❶ The method receives one more parameter: a LocalDate.
This method is also easily testable. We do not even need mocks to test it, as we can pass any LocalDate
object to this method. So, if it is easier to pass the value via method parameter rather than a dependency via its constructor, why do we do it?
First, let’s explore the pros and cons of passing the value we want directly via a method parameter, avoiding all the dependencies. This is often the simplest solution in terms of both implementation (no need for dependencies via constructor) and testing (passing different values via method calls). But the downside is that all the callers of this class will need to provide this parameter. In this example, ChristmasDiscount
expects today
to be passed as a parameter. This means the clients of the applyDiscount()
method must pass the current date. How do we get the current date in this code base? Using the Clock
class. So, while ChristmasDiscount
no longer depends on Clock
, its callers will depend on it. In a way, we pushed the Clock
dependency up one level. The question is, is this dependency better in the class we are modeling now or in its callers?
Now, let’s explore the idea of passing a dependency that knows how to get the required parameter. We did this in the first implementation of the ChristmasDiscount
class, which depends on Clock
; the applyDiscount()
method invokes clock.now()
whenever it needs the current date. While this solution is more complicated than the previous one, it enables us to easily stub the dependency.
It is also simple to write tests for the classes that depend on ChristmasDiscount
. These classes will mock ChristmasDiscount
’s applyDiscount(double
rawAmount)
method without requiring the Clock
. The next listing shows a generic consumer that receives the ChristmasDiscount
class via the constructor, so you can stub it during testing.
Listing 7.15 Generic consumer of the ChristmasDiscount
class
public class SomeBusinessService {
private final ChristmasDiscount discount;
public SomeBusinessService(ChristmasDiscount discount) { ❶
this.discount = discount;
}
public void doSomething() {
// ... some business logic here ...
discount.applyDiscount(100.0);
// continue the logic here...
}
}
❶ We inject a ChristmasDiscount stub here.
Listing 7.16 shows the tests for this SomeBusinessService
class. We stub the ChristmasDiscount
class. Note that this test does not need to handle Clock
. Although Clock
is a dependency of the concrete implementation of ChristmasDiscount
, we do not care about that when stubbing. So, in a way, the ChristmasDiscount
class gets more complicated, but we simplify testing its consumers.
Listing 7.16 Example of the test for the generic consumer class
@Test
void test() {
ChristmasDiscount discount = Mockito.mock(ChristmasDiscount.class); ❶
SomeBusinessService service = new SomeBusinessService(discount);
service.doSomething();
// ... test continues ...
}
❶ Mocks ChristmasDiscount. Note that we do not need to mock or do anything with Clock.
Receiving a dependency via constructor adds a little complexity to the overall class and its tests but simplifies its client classes. Receiving the data via method parameter simplifies the class and its tests but adds a little complexity to the clients. Software engineering is all about trade-offs.
As a rule of thumb, I try to simplify the work of the callers of my class. If I must choose between simplifying the class I am testing now (such as making ChristmasDiscount
receive the date via parameter) but complicating the life of all its callers (they all must get the date of today themselves) or the other way around (ChristmasDiscount
gets more complicated and depends on Clock
, but the callers do not need anything else), I always pick the latter.
Leave a Reply