Container Pattern
The Container
is the heart of the dependency injection (DI) system provided by @elsikora/cladi
. DI is a fundamental design pattern that promotes loose coupling and testability by allowing components to receive their dependencies from an external source (the container) rather than creating them internally.
Think of the container as a central hub where you register all the building blocks (services, configurations, etc.) of your application. When a component needs a building block, it asks the container for it.
Key Features & Examples
-
Registration (
register
/registerMany
): Store dependencies using a uniquesymbol
as a key (token). You can use predefinedEServiceToken
for core services or create your ownSymbol.for('YourToken')
for application-specific dependencies.src/container-setup.tsimport { createContainer, EServiceToken, type ILogger } from '@elsikora/cladi'; import { SomeService, AnotherService } from './services'; import { ApiConfig } from './config'; // Assume logger is created elsewhere declare const logger: ILogger; const container = createContainer({ logger }); // Create instances of your services/configs const apiConfig = new ApiConfig(); const someService = new SomeService(apiConfig); // Manual injection for this example const anotherService = new AnotherService(logger); // Manual injection // Define custom tokens const Tokens = { ApiConfig: Symbol.for('ApiConfig'), SomeService: Symbol.for('SomeService'), AnotherService: Symbol.for('AnotherService'), }; // Register dependencies container.register(EServiceToken.LOGGER, logger); container.register(Tokens.ApiConfig, apiConfig); container.register(Tokens.SomeService, someService); container.register(Tokens.AnotherService, anotherService); logger.info("Dependencies registered in the container."); export { container, Tokens }; // Export for use elsewhere
-
Retrieval (
get
/getMany
): Fetch dependencies using their registered token. The type parameter<T>
ensures type safety.src/some-module.tsimport { container, Tokens } from './container-setup'; import { type ILogger, EServiceToken } from '@elsikora/cladi'; import type { SomeService } from './services'; import type { ApiConfig } from './config'; function doSomething() { // Retrieve needed services from the container const logger = container.get<ILogger>(EServiceToken.LOGGER); const service = container.get<SomeService>(Tokens.SomeService); const config = container.get<ApiConfig>(Tokens.ApiConfig); if (!logger || !service || !config) { // Using console.error as logger might be undefined console.error("Required dependencies not found in container!"); return; } logger.info("Doing something with the service...", { source: "someModule" }); service.performAction(config.getApiKey()); }
-
Checking Existence (
has
): Verify if a token is registered before attempting retrieval.src/check-existence.tsdeclare const logger: ILogger; if (container.has(Tokens.SomeService)) { const service = container.get<SomeService>(Tokens.SomeService); // ... use service safely } else { logger.warn("SomeService is not available in the container."); }
-
Unregistration (
unregister
/unregisterMany
): Remove dependencies, useful in testing or dynamic scenarios (less common in typical application flow).src/unregister-example.tsdeclare const logger: ILogger; container.unregister(Tokens.SomeService); logger.debug(`Has SomeService after unregister: ${container.has(Tokens.SomeService)}`); // Logs: false
-
Clearing (
clear
): Remove all registered dependencies, primarily for test environment teardown to ensure test isolation.src/clear-example.tsdeclare const logger: ILogger; container.clear(); logger.debug(`Has Logger after clear: ${container.has(EServiceToken.LOGGER)}`); // Logs: false
Benefits
- Loose Coupling: Components depend only on abstract tokens (symbols) and interfaces, not on the concrete classes or how they are created. This makes your system more flexible and easier to refactor.
- Enhanced Testability: In unit tests, you can register mock implementations of services under the same tokens, completely isolating the component under test from its real dependencies.
- Improved Reusability & Maintainability: Components become more modular and self-contained. Managing dependencies is centralized in the container setup, making the overall application structure clearer.
Core Implementation
IContainer
(Interface): Located insrc/domain/interface/container.interface.ts
. Defines the essential methods (register
,get
,has
, etc.) that any container must implement.BaseContainer
(Class): Located insrc/infrastructure/class/base/container.class.ts
. The default, concrete implementation provided by the library. It includes built-in logging for container operations (if a logger is provided) to aid debugging.createContainer
(Utility): Exported from the library root (import { createContainer } from '@elsikora/cladi';
). A convenient factory function to instantiateBaseContainer
. It acceptsIBaseContainerOptions
, primarily used for injecting a logger into the container itself.
Base Implementation Options (IBaseContainerOptions
)
Name | Type | Default |
---|---|---|
logger | any The logger to use for the container. | new ConsoleLoggerService() |