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 aname
, looks up the corresponding template object in its associatedRegistry
, clones it (by default usingstructuredClone
), potentially transforms it, caches it, and returns the final instance.src/user-factory.tsimport { 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 specificIRegistry
instance provided during its creation (IBaseFactoryOptions.registry
). -
Cloning Behavior: The
BaseFactory
usesstructuredClone
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 tocreate
for the same name willstructuredClone
this cached template, avoiding repeated lookups and transformations. -
Cache Clearing (
clearCache
): If the underlyingRegistry
changes, you must callfactory.clearCache(name?)
to remove the factory’s cached template(s) and force it to re-fetch (and re-transform) from the registry on the nextcreate
call. Ifname
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 simplecreate(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 insrc/domain/interface/factory.interface.ts
. Defines thecreate
method andgetRegistry
.BaseFactory<T>
(Class): Located insrc/infrastructure/class/base/factory.class.ts
. The default implementation using an associatedRegistry
, with template caching, cloning, and optional transformation.createFactory<T>
(Utility): Exported from the library root. A convenient function to instantiateBaseFactory
, requiringIBaseFactoryOptions
(which must include aregistry
).