// 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.