DEV Community

Cover image for Can this pattern obsolete Dependency Injection?
Dario Mannu
Dario Mannu

Posted on • Edited on

Can this pattern obsolete Dependency Injection?

When using traditional dependency injection (DI) systems in UI applications, components are tightly coupled to their (injected) dependencies, requiring explicit declarations and a vast amount of intricate configuration.


The Limits of Dependency Injection

Dependency injection, while delivering on its promise for managing dependencies, introduces unnecessary complexity and repetition. In frameworks like Angular, components must declare dependencies in constructors or modules, creating apparently flexible, yet still rigid relationships. DI also doesn't solve cross-module communication, requiring additional services or state management solutions.
Testing still requires mocks and stubs of every injected dependency, which keeps the overall QA effort considerable to maintain, even though that's a fault of imperative programming in general.

@injectable(blah, blah)
method(dep1, dep2, dep3, dep4) {
  doSomethingWith(dep1, dep2, dep3, dep4);
}
Enter fullscreen mode Exit fullscreen mode

The Message Bus Pattern: not just for messages

The message bus pattern decouples components by routing messages through a centralized pub/sub hub. Components publish events or commands to the bus and subscribe to relevant messages, unaware of each other’s existence.

This is true loose coupling and it simplifies scalability, enhances modularity and cuts down on refactoring overhead.
Unlike DI, which binds components to specific implementations, the message bus enables dynamic, event-driven communication, making it ideal for modern, reactive applications.

Why the Messagebus Outshines Dependency Injection

Decoupling: Components interact via messages, not direct references, eliminating tight dependency bindings.
Flexibility: New components can subscribe or publish without altering existing code.
Scalability: The bus handles growing complexity without exponential configuration overhead.
Modularisation: features are siloed in their respective components, reducing the likelyhood of code conflicts between developers.
Testability: Components can be tested declaratively, in isolation or in group by simulating bus messages.

In practice, a message bus replaces DI’s injector with a publish-subscribe system. For example, instead of injecting a UserService into a component, the component subscribes to a UserUpdated event on the bus. This approach aligns with reactive programming principles, enabling seamless integration with libraries such as RxJS.

TOPS: The Observable Plugin System

To harness the message bus pattern’s potential, we're introducing a new library: The Observable Plugin System, TOPS for short, is the first stream-oriented messagebus for JavaScript. TOPS leverages RxJS observables and adds request/response capabilities to create a flexible, loosely-coupled architecture for modern applications that make use of reactive streams.

A simple implementation example

// modules/home-page.ts
import { match } from "../operators/match";
import { map } from "rxjs";
import { rml } from "rimmel";

const template = () => rml`
  <h1>Hello, World</h1>
  <p>Welcome to the home page!</p>
`;

export default ({ ROUTE, RENDER }, config) => {

  ROUTE.pipe(
    match('/'),
    map(template),
  ).subscribe(RENDER);

};
Enter fullscreen mode Exit fullscreen mode

The code above is a simple "home page" module. It listens to the ROUTE topic, an observable stream coming from the router.
On every route event, we filter match('/') the current path and if it matches, we pass it through a template, finally emitting the resulting HTML to the RENDER topic.
All topics are Observable Subjects, readable and writable. A "display" module listens to RENDER messages and renders the HTML on the page.

A module like this has no side effects, so it's trivial to test it using stream-oriented testing libraries like LeapingBunny.
We only feed it events and check what comes out in simple, declarative test streams.

TOPS is the first plugin manager library to enable module-to-module communication via observable streams. If you use RxJS in your application this is a perfect match.
Plugins can extend functionality by subscribing to or publishing events, fostering a modular ecosystem. Unlike DI, TOPS requires no predefined dependency declarations, making it great for dynamic, event-driven applications.

A Reference architecture

Enough words, let's see this in practice.
The following example is a simple webapp in stream-oriented style, where everything is a module/plugin, and everything is a stream.

View on Stackblitz

Conclusion

The message bus pattern can be used to replace DI systems. By enabling loose coupling through event streams, it simplifies application code by reducing boilerplate.
TOPS empowers developers to build modular, maintainable systems, rendering traditional DI systems unnecessary.

If you like what you've seen here please consider leaving a Star in Github ⭐ to help other people discover it!

Learn More

Top comments (0)