A Factory hides the details of instantiating objects within a class hierarchy. The client provides some information regarding the object it needs. Based on that information the factory determines which class of objects to instantiate and how to go about it. Factories are typically Singletons but don’t necessarily need to be, and it depends on the implementation needs.
Factories is very common. Anytime you are using a library that manages objects but delegates their construction to you – you are creating a Factory. One such library is generic-pool
.
Another example is constructing different objects within a family based on configuration. For example, in a development environment you may want to use one type of database and in production another. In that case, you may choose to specify a configuration parameter that indicates what database driver to instantiate.
Factories in TypeScript
Source: Factory.ts
export interface Product {
id: string;
description: string;
checkInventory(): number ;
updateInventory(delta:number): void;
}
class ProductImpl implements Product {
id: string;
description: string;
inventory:number;
constructor(id:string, description:string) {
this.id=id;
this.description=description;
this.inventory=10;
}
checkInventory(): number {
return this.inventory;
}
updateInventory(delta:number) {
this.inventory=this.inventory+delta;
}
}
export function createProduct(id: string, description: string): Product {
return new ProductImpl(id, description);
}
Source: client.ts
import * as factory from './Factory';
var myProduct=factory.createProduct("abc123","Design Patterns");
console.log(myProduct.checkInventory());
myProduct.updateInventory(-3);
console.log(myProduct.checkInventory());
Case study: storage connection factory
Hashicorp Vault is a tool for managing secrets. It supports different storage backends. The exact storage backend is not known until the configuration is specified and the tool is started. Hashicorp Vault is written in Go, but as an exercise, we can think about how it manages configurable storage backends.
Source: StorageFactory.ts
Observe that in our example we only export the interface, but not the implementation. We also export an enumeration of storage kinds. There are no implementation details for each storage type in this example because we are focusing on the pattern. The magic happens in the createStorage
function.
export interface Storage {
put(key:string, value:string);
get(key:string):string;
}
export enum StorageKind {
Redis,
InMemory
}
class RedisStorage implements Storage {
put(key:string, value:string) {
//... do something
}
get(key:string):string {
return "foo";
}
}
class InMemoryStorage implements Storage {
put(key:string, value:string) {
//... do something
}
get(key:string):string {
return "foo";
}
}
export function createStorage(kind:StorageKind):Storage {
switch(kind) {
case StorageKind.Redis:
return new RedisStorage();
case StorageKind.InMemory:
return new InMemoryStorage();
}
}
Variations of the pattern
The original “Gang of Four” design patterns book described two more variations of the factory pattern: Factory Method and Abstract Factory. Both patterns can sometimes be found in libraries, but at the end of the day, they are still factories. In my experience, there is no sense in dwelling on the small details, and I am skipping these patterns for this article.