DIOD (Dependency Injection On Demand)

repository·main·Indexed 41 days ago

https://github.com/artberri/diod

A lightweight, opinionated inversion of control (IoC) container and dependency injector for TypeScript, Node.js, and browser applications. DIOD v3.0.0 leverages TypeScript decorators and metadata for seamless autowiring, utilizing a ContainerBuilder to construct immutable containers and abstract classes for dependency inversion.

Tokens
14.1K
Snippets
56
Records
68
Agent score
97%

What's inside DIOD

  1. Overview of DIOD API surface

    main

    DIOD v3.0.0 provides a dependency injection system centered around a Container and a ContainerBuilder. The API is composed of several key building blocks:

    Core Classes

    • ContainerBuilder: Used to construct and configure a dependency injection container.

    Core Interfaces

    • Container: The primary interface for interacting with the dependency injection container.
    • Registration: Represents a registered dependency within the container.
    • Newable: Represents a class or constructor that can be instantiated.
    • Abstract: Base interface for DIOD abstractions.
    • ConfigurableRegistration: Interface for registrations that allow configuration.
    • WithDependencies: Interface for managing dependencies of a registration.
    • WithScopeChange: Interface for managing the scope of a registration.

    Type Aliases

    • Identifier: The key used to look up a dependency.
    • Factory: A function used to create an instance of a dependency.
    • Instance: The resolved value of a dependency.
    • BuildOptions: Configuration options used during the container building process.

    Functions

    • Service: A utility function for defining services.
  2. Get started with DIOD

    main

    DIOD (Dependency Injection On Demand) is a dependency injection library. To begin using it, you can follow the documentation guides for installation and various injection patterns such as injecting classes, using abstractions, or managing dependency scopes. The documentation provides specific guides for:

    • Installation
    • Injecting classes
    • Using abstractions to define dependencies
    • Custom decorators
    • Inheritance patterns
    • Injecting externally created instances
    • Registering factories for instance creation
    • Dependency scoping
    • Vanilla JavaScript usage
    • Disabling autowire (manual dependency declaration)
    • Private services visibility
    • Tagging services
    • Automatic registration
  3. Use abstract classes for dependency inversion and autowiring

    main

    DIOD follows the Dependency Inversion Principle (the 'D' in SOLID). While many languages use interfaces for abstractions, TypeScript interfaces are not available at runtime. Therefore, to use abstractions for autowiring dependencies in DIOD, you must define your abstractions using abstract classes.

    By injecting an abstract class into a consumer (like a Use Case), you decouple the consumer from the concrete implementation. This allows you to swap implementations (e.g., switching from SqliteUserRepository to a different database) by changing only the registration in your ContainerBuilder.

    // 1. Define the abstraction as an abstract class
    export abstract class Mailer {
      abstract sendConfirmationEmail(userData: user): void
      abstract sendResetPasswordEmail(userData: user): void
    }
    
    // 2. Inject the abstraction into your service
    @Service()
    export class SignUpUseCase {
      constructor(
        private readonly mailer: Mailer // DIOD uses the abstract class for autowiring
      ) {}
    }
    
    // 3. Implement the concrete class
    @Service()
    export class AcmeMailer implements Mailer {
      sendConfirmationEmail(userData: user): void { /* ... */ }
      sendResetPasswordEmail(userData: user): void { /* ... */ }
    }
  4. How DIOD autowiring works with TypeScript

    main

    DIOD uses constructor type-hints to automatically resolve and inject dependencies.

    Key Requirements:

    1. Decorators: All registered services must be decorated (e.g., using @Service()) so that TypeScript emits the necessary metadata at runtime.
    2. Abstractions: Because TypeScript interfaces are removed at runtime, you must use abstract classes instead of interfaces if you want to use them as dependency tokens for autowiring.
    3. Container Building: After registering services, you must use a ContainerBuilder to validate the dependency graph (checking for missing dependencies or circularities) and create an immutable container.
    // Example of a service using autowiring via constructor type-hints
    import { Service } from 'diod'
    
    @Service()
    export class SignUpUseCase {
    	constructor(
    		private readonly userRepository: UserRepository,
    		private readonly mailer: Mailer,
    	) {}
    }
  5. Use the Container interface to manage services

    main

    The Container is the central object in DIOD that creates, wires dependencies, and manages the lifetime of a set of services. You do not instantiate a Container directly; instead, you create instances using a ContainerBuilder.

    Use the Container to retrieve registered services and to look up identifiers associated with specific tags.

  6. Configure dependency scopes in DIOD

    main

    DIOD uses instance scopes to determine how services are shared. You can configure the scope during registration using a ContainerBuilder.

    There are three available scopes:

    • Transient (Default): Returns a new instance every time the service is requested.
    • Request (asInstancePerRequest): Returns a single instance within the same request context.
    • Singleton (asSingleton): Returns the same instance for every request across the entire application.

    Note that manually created instances registered via useInstance are always treated as singletons and cannot be changed to other scopes.

    const builder = new ContainerBuilder()
    
    // Returns a new instance every time (Default)
    builder.registerAndUse(Whatever).asTransient()
    
    // Returns one instance per request
    builder.registerAndUse(Whatever).asInstancePerRequest()
    
    // Returns the same instance everywhere
    builder.registerAndUse(Whatever).asSingleton()
    
    const container = builder.build()
  7. Inject externally created instances using ContainerBuilder

    main

    If you have already instantiated objects (e.g., services with complex manual setup or third-party instances), you can register them in the DI container using the ContainerBuilder.register(Token).useInstance(instance) pattern. This allows the container to provide your specific instance whenever the corresponding token (usually an abstract class or interface) is requested as a dependency.

    import { ContainerBuilder } from 'diod'
    
    // 1. Your existing instances
    const logger = new ConsoleLogger()
    const mailer = new Mailer()
    
    // 2. Register them with the builder
    const builder = new ContainerBuilder()
    builder.register(Logger).useInstance(logger)
    builder.register(Mailer).useInstance(mailer)
    
    // 3. Build the container
    const container = builder.build()
    
    // Now, any class requesting 'Logger' in its constructor 
    // will receive the 'logger' instance.
  8. Implement automatic class registration with a custom decorator

    main

    To avoid manually calling registerAndUse for every class, you can implement a custom decorator pattern that collects classes into an array and registers them all at once using a helper function.

    1. Create an autoregister.ts file containing a decorator (e.g., RegisterService) that pushes classes into a local registry array.
    2. Use the decorator on your service classes.
    3. Use an autoregister helper function to iterate through the registry and call builder.registerAndUse(service) on your ContainerBuilder instance.
    // autoregister.ts
    import { ContainerBuilder, Newable } from 'diod'
    
    const autoregisteredClasses: Newable<unknown>[] = []
    
    const isNewable = (target: unknown): target is Newable<unknown> => {
      if (typeof target !== 'function') {
        return false
      }
    
      const prototype = target.prototype
      return !!prototype && !!prototype.constructor
    }
    
    export const RegisterService = (): ClassDecorator => {
      return <TFunction extends Function>(target: TFunction): TFunction => {
        if (isNewable(target)) {
          autoregisteredClasses.push(target)
        } else {
          throw new Error('Abstract classes cannot be auto registered')
        }
    
        return target
      }
    }
    
    export const autoregister = (builder: ContainerBuilder): ContainerBuilder => {
      for (const service of autoregisteredClasses) {
        builder.registerAndUse(service)
      }
    
      return builder
    }
  9. Use DIOD with vanilla JavaScript

    main

    While DIOD is optimized for TypeScript, it can be used in vanilla JavaScript. Because JavaScript lacks type information, DIOD cannot automatically discover dependencies from constructors. You must manually register each service and explicitly provide its dependencies using the .withDependencies() method. When building the container, you must set the autowire option to false.

    import { ContainerBuilder } from 'diod'
    
    const builder = new ContainerBuilder()
    
    // Manually register services and their dependencies
    builder.registerAndUse(ServiceOne).withDependencies([ServiceTwo, ServiceThree])
    builder.registerAndUse(ServiceTwo).withDependencies([ServiceFour])
    builder.registerAndUse(ServiceThree)
    builder.registerAndUse(ServiceFour)
    
    // Build the container with autowire disabled
    export const container = builder.build({ autowire: false })
    
    // Retrieve services
    const serviceOne = container.get(ServiceOne)
  10. Register private services in DIOD

    main

    By default, all services registered in a DIOD container are public and can be queried directly from the IoC container. To restrict a service so that it can only be used as a dependency for other services and cannot be queried manually from the container, mark it as private using the .private() method during the registration process in the ContainerBuilder.

    const builder = new ContainerBuilder()
    // ...
    builder.registerAndUse(Whatever).private()
    // or
    builder
      .register(Whatever)
      .useFactory((c) => {
        return new Whatever(/**/)
      })
      .private()
    // or
    builder.register(Whatever).useInstance().private()
    // ...
    const container = builder.build()