Dependency via class constructor or value via method parameter?

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.


Comments

Leave a Reply

Your email address will not be published. Required fields are marked *