DEV Community

Cover image for Use InjectableFactory instead of Dependency Injection
jianwu
jianwu

Posted on

Use InjectableFactory instead of Dependency Injection

Dependency Injection has almost become a religious belief that people mistakenly thought it's the ultimate solution for all dependency coupling and testing problems. With DI frameworks, people try to abuse DI with DI everything approach. For the purpose of unit testing, people try to expose all the internal dependencies which causes huge accidental complexities. While DI is suitable for exposing explicit dependencies, it's not right tool for exposing implicit dependencies which are implementation details and supposed to be hidden as deep as possible.

In backend and static type language communities, I have seen so many organizations use DI framework just for the seek of DI as a belief and forgot what are the actual problems it tries to solve. Developers suffer a lot but are still stuck with it without a second thought. DI frameworks have become a huge productivity killer and causes of obscured, fragile and less maintainable code. In the frontend and dynamic type language communities such as ruby, javascript, it's much better, people try to use more pragmatic and creative approach to solve unit testing problem. But some frontend framework such as Angular still stuck with DI concept in its core which cause unnecessary complexity that prevents its adoption.

As a component designer, we should always design for user instead of testing. We don't want to pollute the usage boundary just for the seek of testing. For testability and customization, there are better alternatives other than DI. Here I'll introduce one alternative InjectableFactory / InjectableInstance for static typed languages. This pattern is also related to following patterns: Factory, Singleton, Service Locator, Mutable Singleton, Mutable value holder and Ambient context. These are all considered anti-patterns according to DI books. But InjectableFactory is designed in a way to avoid their caveat and solve our problem very well in practice.

Here is the pattern diagram:
Injectable Factory Pattern

Here is an example on how to use InjectableInstance. TimeProvider is an abstraction for system Date, so that applications can get system time by calling TimeProvider.get() instead of using new Date() to avoid direct dependency on system date. So in the unit test, we can easily mock the TimeProvider to simulate different moments.

public interface TimeProvider {
  InjectableInstance<TimeProvider> instance = InjectableInstance.of(Impl.class);  //【1】
  static TimeProvider get() { return instance.get(); }                            //【2】

  class Impl implements TimeProvider {                                            //【3】
    @Override public Date getDate() { return new Date(); }
    @Override public long getTimeMillis() { return System.currentTimeMillis(); }
  }

  @Accessors(chain = true)
  class Mock implements TimeProvider {                                            //【4】
    @Setter @Getter long timeMillis = System.currentTimeMillis();
    @Override public Date getDate() { return new Date(timeMillis); }
    public void add(long offset) { timeMillis += offset; }
  }

  Date getDate();
  long getTimeMillis();
}

// OrderService will use TimeProvider
public class OrderService {
  public final static InjectableInstance<OrderService> instance
      = InjectableInstance.of(OrderService.class);
  static OrderService get() { return instance.get(); }

  public CreateOrderResponse createOrder(CreateOrderRequest request) {
    // ....
    Order order = new Order().setCreated(TimeProvider.get().getDate());           //【5】
    // ...
    return new CreateOrderResponse().setOrder(order);
  }

  public final static long EXP_TIME = 24 * 3600 * 1000; // 24 hours
  public boolean isOrderExpired(Order order) {
    return TimeProvider.get().getTimeMillis() - order.getCreated().getTime() > EXP_TIME;
  }
}

// In Unit test
public class OrderServiceTest {
  private final static TimeProvider.Mock timeMock = new TimeProvider.Mock();     //【6】

  @Before public void before() {
    TimeProvider.instance.setInstance(timeMock);                                 //【7】
  }

  @Test public void testOrderExpired() {
    CreateOrderResponse resp = OrderService.get().createOrder(new CreateOrderRequest());
    assertEqual(timeMock.getDate(), resp.getOrder.getCreated());

    timeProviderMock.add(3600 * 1000);  // 1 hour passed                         //【8】  
    assertFalse(OrderService.get().isOrderExpired(resp.getOrder()));

    timeProviderMock.add(48 * 3600 * 1000);  // 2 days passed
    assertTrue(OrderService.get().isOrderExpired(resp.getOrder()));
  }
}

The InjectableFactory and InjectableInstance are pretty easy to implement from scratch. We have a reference implementation in our open source project jsonex, they are just very simple classes without any external dependencies. For the full sample code on how to use it, please refer to the github repo

For the more detailed information and the philosophy behind, you can take a look at my medium blog: Dependency Indirection with Injectable Factory

Top comments (0)