Loading content...
In the previous page, we explored a fundamental problem: direct object instantiation creates rigid, tightly coupled systems. The new keyword, despite its simplicity, binds your code to specific concrete classes at compile time, making testing difficult, configuration inflexible, and extension impossible without modification.
Now we introduce the solution that has guided object-oriented design for over three decades: the Factory Method pattern.
The insight is deceptively simple: instead of calling new directly, call a method that calls new—and make that method overridable. This transforms a compile-time decision into a runtime decision, enabling polymorphism to work for object creation just as it works for method calls.
By the end of this page, you'll understand the Factory Method pattern's core solution: defining an interface for creating objects while letting subclasses decide which class to instantiate. You'll see how this simple inversion of control unlocks flexibility, testability, and extensibility that direct instantiation can never provide.
The Factory Method pattern is built on a powerful observation: polymorphism works for methods—why not for creation?
When you call object.doSomething(), the actual implementation that runs depends on the runtime type of object. Different subclasses can provide different behaviors. This is the foundation of object-oriented design.
But when you call new SomeClass(), there's no polymorphism. The class is specified literally in the source code. Factory Method changes this by wrapping the new call in a method that can be overridden:
12345678910111213141516171819202122232425262728293031323334353637
// The Core Pattern: Replace 'new' with an overridable method // ❌ BEFORE: Direct instantiation - no flexibilityclass DocumentProcessor { processDocument(path: string): void { const document = new PdfDocument(path); // Hard-coded class document.parse(); document.validate(); document.render(); }} // ✅ AFTER: Factory Method - polymorphic creationabstract class DocumentProcessor { processDocument(path: string): void { const document = this.createDocument(path); // Calls factory method document.parse(); document.validate(); document.render(); } // The Factory Method - subclasses decide what to create protected abstract createDocument(path: string): Document;} // Subclasses provide specific implementationsclass PdfDocumentProcessor extends DocumentProcessor { protected createDocument(path: string): Document { return new PdfDocument(path); }} class WordDocumentProcessor extends DocumentProcessor { protected createDocument(path: string): Document { return new WordDocument(path); }}The transformation is subtle but profound:
| Aspect | Direct Instantiation | Factory Method |
|---|---|---|
| Code location | new PdfDocument(path) | this.createDocument(path) |
| Decision point | Compile-time | Runtime |
| Flexibility | None | Subclass can override |
The actual new call | In the using class | In the subclass |
Factory Method doesn't eliminate the new keyword—it relocates it. The new call moves from the class that uses the object to a subclass that specializes in creating it. This relocation is what enables polymorphic creation.
Let's formalize the Factory Method pattern with its official definition from the Gang of Four:
Factory Method: Define an interface for creating an object, but let subclasses decide which class to instantiate. Factory Method lets a class defer instantiation to subclasses.
Breaking down this definition:
The Factory Method signature:
A factory method typically has these characteristics:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849
abstract class Creator { // The Factory Method // - Return type is an interface or abstract class (Product) // - Can be abstract (subclasses must implement) or virtual (default) // - Usually protected or public // - Named "create", "make", "build", or domain-specific protected abstract createProduct(): Product; // Operation that uses the factory method someOperation(): void { // Call factory method to get a product const product = this.createProduct(); // Use the product through its interface // (doesn't know or care about concrete type) product.doWork(); }} // The Product interfaceinterface Product { doWork(): void;} // Concrete Productsclass ConcreteProductA implements Product { doWork(): void { console.log("Product A working"); }} class ConcreteProductB implements Product { doWork(): void { console.log("Product B working"); }} // Concrete Creators - each provides a specific productclass ConcreteCreatorA extends Creator { protected createProduct(): Product { return new ConcreteProductA(); }} class ConcreteCreatorB extends Creator { protected createProduct(): Product { return new ConcreteProductB(); }}The factory method's return type should be as abstract as possible. If all products share behavior, use an abstract class. If they only share contract, use an interface. The more abstract the return type, the more flexibility you have in providing different implementations.
Recall the problems we identified with direct instantiation. Let's see how Factory Method addresses each one:
| Problem | Direct Instantiation | Factory Method Solution |
|---|---|---|
| Compile-time binding | Class is hard-coded with new | Class is determined by which subclass is instantiated |
| Open/Closed violation | Adding types requires modifying existing code | Add new Creator subclass—existing code unchanged |
| Testing difficulty | Cannot substitute test doubles | Create TestCreator subclass returning mocks |
| Configuration rigidity | Conditional logic scattered everywhere | Select appropriate Creator at startup |
| Extensibility barriers | Must modify framework source | Extend framework with new Creator subclass |
Let's see each solution in action:
Adding new document types without modifying existing code:
12345678910111213141516171819202122232425262728293031323334353637383940
// Original code - completely unchanged when adding new typesabstract class DocumentProcessor { processDocument(path: string): void { const document = this.createDocument(path); document.parse(); document.validate(); document.render(); } protected abstract createDocument(path: string): Document;} class PdfDocumentProcessor extends DocumentProcessor { protected createDocument(path: string): Document { return new PdfDocument(path); }} // NEW: Add Excel support - no existing code modified!class ExcelDocumentProcessor extends DocumentProcessor { protected createDocument(path: string): Document { return new ExcelDocument(path); }} // NEW: Add Markdown support - no existing code modified!class MarkdownDocumentProcessor extends DocumentProcessor { protected createDocument(path: string): Document { return new MarkdownDocument(path); }} // Usage: Select processor at configuration timefunction getProcessor(type: string): DocumentProcessor { switch (type) { case 'pdf': return new PdfDocumentProcessor(); case 'excel': return new ExcelDocumentProcessor(); case 'markdown': return new MarkdownDocumentProcessor(); default: throw new Error(`Unknown type: ${type}`); }}The key point: The base DocumentProcessor class is closed for modification—it never changes. But it's open for extension—we can add unlimited new document types by creating new subclasses.
Factory Method is an example of Inversion of Control (IoC)—one of the most important principles in software design. Let's understand exactly what's being inverted.
Traditional Control Flow:
In procedural code, the main program controls everything:
123456789101112131415161718
// Traditional: The framework/library calls YOUR code // Your main program controls the flowfunction main() { const config = loadConfig(); // YOU decide what to create const database = new PostgresDatabase(config.dbUrl); const cache = new RedisCache(config.cacheUrl); const logger = new FileLogger(config.logPath); // YOU control execution const app = new Application(database, cache, logger); app.run();} // The "main" code knows about ALL concrete classes// It's a central point that must change for any configuration changeInverted Control with Factory Method:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455
// Factory Method: The framework calls YOUR factory methods // Framework defines the template with factory methodsabstract class Application { run(): void { const database = this.createDatabase(); const cache = this.createCache(); const logger = this.createLogger(); this.initialize(database, cache, logger); this.startServer(); } // Factory Methods - YOU provide implementations protected abstract createDatabase(): Database; protected abstract createCache(): Cache; protected abstract createLogger(): Logger; protected abstract initialize(db: Database, cache: Cache, log: Logger): void; protected abstract startServer(): void;} // Your code only knows about the specifics it createsclass ProductionApplication extends Application { protected createDatabase(): Database { return new PostgresDatabase(process.env.DB_URL!); } protected createCache(): Cache { return new RedisCache(process.env.CACHE_URL!); } protected createLogger(): Logger { return new CloudWatchLogger(process.env.LOG_GROUP!); } // ... other implementations} // Different configuration = different subclassclass DevelopmentApplication extends Application { protected createDatabase(): Database { return new SqliteDatabase(':memory:'); } protected createCache(): Cache { return new InMemoryCache(); } protected createLogger(): Logger { return new ConsoleLogger(); } // ... other implementations}Factory Method follows the "Hollywood Principle": Don't call us, we'll call you. The framework (base class) calls your code (factory method), not the other way around. This inversion gives the framework control over the process while letting you control the specifics.
The Factory Method pattern admits several variations depending on your needs. Understanding these variations helps you apply the pattern appropriately.
The classic form: Factory method is abstract, forcing subclasses to provide an implementation.
123456789101112131415161718192021222324252627282930
abstract class ReportGenerator { generate(): Report { const formatter = this.createFormatter(); // Must be implemented const data = this.collectData(); return formatter.format(data); } // Abstract - subclasses MUST implement protected abstract createFormatter(): ReportFormatter; protected abstract collectData(): ReportData;} // Every subclass provides its own implementationclass PdfReportGenerator extends ReportGenerator { protected createFormatter(): ReportFormatter { return new PdfFormatter(); } protected collectData(): ReportData { // ... }} class CsvReportGenerator extends ReportGenerator { protected createFormatter(): ReportFormatter { return new CsvFormatter(); } protected collectData(): ReportData { // ... }}Use when: The base class cannot provide a sensible default. All subclasses must consciously choose a product type.
Factory Method is not always the right choice. It shines in specific situations:
Don't use Factory Method when you have a single, fixed implementation that will never change. Don't use it for simple objects with trivial construction. The pattern adds a layer of abstraction—ensure that abstraction provides value. If you're creating a factory method that will only ever have one implementation, you're adding complexity without benefit.
A rule of thumb:
Use Factory Method when you answer "yes" to at least one of these questions:
Let's look at a comprehensive before/after comparison to solidify understanding. We'll build a simple e-commerce notification system.
123456789101112131415161718192021222324
class OrderService { placeOrder(order: Order): void { // Process the order this.processPayment(order); this.updateInventory(order); // Hard-coded notification const emailSender = new SmtpEmailSender({ host: 'smtp.example.com', port: 587 }); emailSender.send( order.customerEmail, `Order ${order.id} confirmed!` ); }} // Problems:// - Can't test without real SMTP// - Can't add SMS without modifying// - Can't disable in development// - Credentials hard-coded123456789101112131415161718192021222324252627
abstract class OrderService { placeOrder(order: Order): void { this.processPayment(order); this.updateInventory(order); // Factory method const notifier = this.createNotifier(); notifier.notify( order.customerEmail, `Order ${order.id} confirmed!` ); } protected abstract createNotifier(): Notifier;} class ProductionOrderService extends OrderService { protected createNotifier(): Notifier { return new SmtpNotifier(config.smtp); }} class TestOrderService extends OrderService { protected createNotifier(): Notifier { return new MockNotifier(); }}The benefits are immediate:
| Aspect | Before | After |
|---|---|---|
| Testing | Requires SMTP server | MockNotifier, no network |
| Adding SMS | Modify OrderService | Create SmsOrderService |
| Different environments | Environment checks everywhere | Different subclass per environment |
| Credentials | Hard-coded or scattered | Encapsulated in creator |
We've now thoroughly explored the Factory Method solution. Let's consolidate the key insights:
new with an overridable method — This transforms compile-time binding into runtime polymorphism.The Core Pattern:
Base Class: Defines template method that calls factory method
Factory Method: Abstract or virtual method returning Product interface
Subclasses: Override factory method to return concrete products
In the next page, we'll examine the participants and structure of Factory Method in detail—the Creator, Product, ConcreteCreator, and ConcreteProduct roles, and how they collaborate.
You now understand how the Factory Method pattern solves the problems of direct instantiation by defining an interface for creation and letting subclasses decide which class to instantiate. This simple but powerful inversion transforms rigid code into flexible, testable, extensible architectures.