Skip to Content

Factory Pattern

The Factory pattern, implemented via IFactory and BaseFactory, provides a standardized way to create instances of objects based on named templates stored in a Registry.

Instead of scattering new MyObject(...) calls throughout your code, you ask the factory to create('objectName'). This centralizes creation logic and makes it easy to switch out implementations or configurations.

Key Features & Examples

  • Creation (create): The core method. It takes a name, looks up the corresponding template object in its associated Registry, clones it (by default using structuredClone), potentially transforms it, caches it, and returns the final instance.

    src/user-factory.ts
    import { createFactory, createRegistry, type IFactory, type IRegistry } from '@elsikora/cladi'; // Define user profile structure interface UserProfile { name: string; // Registry key userId: string; email: string; plan: 'free' | 'pro' | 'enterprise'; features: string[]; } // 1. Set up the Registry with templates const userRegistry: IRegistry<UserProfile> = createRegistry<UserProfile>({}); userRegistry.register({ name: "free-user", userId: `user-${Math.random().toString(36).substring(2, 9)}`, email: "", plan: "free", features: ["basic-support"] }); userRegistry.register({ name: "pro-user", userId: `user-${Math.random().toString(36).substring(2, 9)}`, email: "", plan: "pro", features: ["priority-support", "advanced-reporting"] }); // 2. Create the Factory linked to the Registry const userFactory: IFactory<UserProfile> = createFactory<UserProfile>({ registry: userRegistry }); // 3. Use the Factory to create instances const newUserFree = userFactory.create("free-user"); newUserFree.email = "test1@example.com"; // Modify the instance const newUserPro = userFactory.create("pro-user"); newUserPro.email = "test2@example.com"; console.log(`Created Free User: ${newUserFree.userId}, Plan: ${newUserFree.plan}`); console.log(`Created Pro User: ${newUserPro.userId}, Plan: ${newUserPro.plan}`); // Creating another free user gets a *new instance* (due to cloning) const anotherFreeUser = userFactory.create("free-user"); console.log(`Are the two free users the same object? ${newUserFree === anotherFreeUser}`); // false
  • Registry Association: Each Factory instance is tightly coupled to a specific IRegistry instance provided during its creation (IBaseFactoryOptions.registry).

  • Cloning Behavior: The BaseFactory uses structuredClone by default. This ensures that modifying a created instance does not affect the original template in the registry or subsequently created instances.

  • Optional Transformation (transformer): You can provide a function (IBaseFactoryOptions.transformer) that takes the template from the registry and returns a modified version before it’s cached and cloned by the factory. This is useful for applying defaults, adding dynamic properties, or performing complex setup.

    src/transforming-factory.ts
    // ... registry setup as before ... // Assume userRegistry is declared and populated declare const userRegistry: IRegistry<UserProfile>; const transformingFactory = createFactory<UserProfile>({ registry: userRegistry, transformer: (template) => { // Add a timestamp to every created user profile const transformed = structuredClone(template); // Clone to avoid modifying registry template (transformed as any).createdAt = new Date().toISOString(); return transformed; } }); const userWithTimestamp = transformingFactory.create("pro-user"); console.log(userWithTimestamp); // Output includes: ..., createdAt: '2023-10-27T...'
  • Caching (Internal): The BaseFactory caches the first transformed template for each name. Subsequent calls to create for the same name will structuredClone this cached template, avoiding repeated lookups and transformations.

  • Cache Clearing (clearCache): If the underlying Registry changes, you must call factory.clearCache(name?) to remove the factory’s cached template(s) and force it to re-fetch (and re-transform) from the registry on the next create call. If name is omitted, the entire factory cache is cleared.

Purpose & Use Cases

  • Decouple Object Creation: Hides the complexity of creating objects (new ..., setting defaults, etc.) behind a simple create(name) interface.
  • Manage Instance Variations: Easily create different pre-configured instances of the same base object type (like user profiles, database connections, API clients).
  • Enforce Consistency: Ensures objects are created according to the defined templates in the registry.
  • Combine with DI: Factories themselves can be registered in the Container to provide on-demand creation of specific object types throughout the application.

Core Implementation

  • IFactory<T> (Interface): Located in src/domain/interface/factory.interface.ts. Defines the create method and getRegistry.
  • BaseFactory<T> (Class): Located in src/infrastructure/class/base/factory.class.ts. The default implementation using an associated Registry, with template caching, cloning, and optional transformation.
  • createFactory<T> (Utility): Exported from the library root. A convenient function to instantiate BaseFactory, requiring IBaseFactoryOptions (which must include a registry).

Base Implementation Options (IBaseFactoryOptions)

NameTypeDefault
loggerany

The logger to use for logging.

new ConsoleLoggerService()
registryIRegistry<T>

The registry to use for creating items.

transformer(template: T) => T

The transformer to use for creating items.

Last updated on