CodePark/ questions / Dependency injection & inversion
START PRACTICINGPRACTICE
Backend question · Software design & patterns › Dependency injection & inversion

Compare constructor injection with the Service Locator pattern. Why is constructor injection usually preferred?

// Service locator
class InvoiceService {
  async issue(id: string) {
    const repo = Container.get<InvoiceRepo>("InvoiceRepo");
    const mailer = Container.get<Mailer>("Mailer");
    // ...
  }
}
// Constructor injection
class InvoiceService {
  constructor(private repo: InvoiceRepo, private mailer: Mailer) {}
}

Constructor injection is usually preferred because it makes a class's dependencies visible and easy to replace in tests.

Both patterns decouple a class from the concrete classes it uses. Decouple means the class doesn't choose or create those classes itself. The difference is who does the asking:

  • With a service locator, the class pulls its dependencies from a global registry. A registry is a shared lookup table of services.
  • With constructor injection, code outside the class pushes the dependencies in through the constructor. The class never sees the container.

Why constructor injection is usually better:

  • The dependencies are explicit in the constructor's signature. You can see at a glance what a class needs, and you notice when it needs too much.
  • Nobody can create the object half-configured.
  • Tests just call new InvoiceService(fakeRepo, fakeMailer). They don't need to set up a global registry.
  • The class does not depend on the container itself.

Problems with a service locator:

  • It hides dependencies.
  • When something is missing, it fails at runtime instead of at wiring time (when the objects are connected together).
  • Tests share global state.

Martin Fowler notes that a service locator can be acceptable inside an application you fully control. But people generally consider it the weaker choice.

  • A service locator makes the class pull dependencies from a global registry. Constructor injection pushes dependencies in from outside.
  • Constructor injection makes dependencies explicit, and they are all present when the object is created.
  • Tests pass fakes directly, without any global setup.
  • A service locator hides dependencies, ties the class to the container and fails late, at runtime.