Explore practical examples and use cases of the Factory Method pattern in JavaScript and TypeScript, focusing on creating different types of documents and the benefits of extensibility and maintenance.
The Factory Method pattern is a creational design pattern that provides an interface for creating objects in a superclass but allows subclasses to alter the type of objects that will be created. This pattern is particularly useful when dealing with scenarios where the exact type of object to be created is determined at runtime. In this section, we’ll explore practical examples of the Factory Method pattern in action, focusing on creating different types of documents in an application. We’ll also discuss the benefits of this pattern in terms of extensibility and code maintenance.
Before diving into examples, let’s briefly revisit the Factory Method pattern. The core idea is to define an interface for creating an object, but let subclasses decide which class to instantiate. The Factory Method lets a class defer instantiation to subclasses.
Here’s a simple UML diagram to visualize the Factory Method pattern:
classDiagram class Creator { +factoryMethod() Product +someOperation() void } class ConcreteCreatorA { +factoryMethod() ConcreteProductA } class ConcreteCreatorB { +factoryMethod() ConcreteProductB } class Product { <<interface>> } class ConcreteProductA class ConcreteProductB Creator <|-- ConcreteCreatorA Creator <|-- ConcreteCreatorB Product <|-- ConcreteProductA Product <|-- ConcreteProductB Creator --> Product
In this diagram, Creator
is an abstract class that declares the factory method factoryMethod
, which returns an object of type Product
. ConcreteCreatorA
and ConcreteCreatorB
are subclasses that implement the factory method to return instances of ConcreteProductA
and ConcreteProductB
, respectively.
Consider an application that needs to create different types of documents, such as PDFs, Word documents, and Excel spreadsheets. Using the Factory Method pattern, we can define a common interface for document creation and let subclasses decide which type of document to create.
First, we define an interface for the document:
// Document.ts
interface Document {
open(): void;
save(): void;
close(): void;
}
This interface declares methods that all document types must implement.
Next, we implement concrete classes for each type of document:
// PdfDocument.ts
class PdfDocument implements Document {
open(): void {
console.log("Opening PDF document...");
}
save(): void {
console.log("Saving PDF document...");
}
close(): void {
console.log("Closing PDF document...");
}
}
// WordDocument.ts
class WordDocument implements Document {
open(): void {
console.log("Opening Word document...");
}
save(): void {
console.log("Saving Word document...");
}
close(): void {
console.log("Closing Word document...");
}
}
// ExcelDocument.ts
class ExcelDocument implements Document {
open(): void {
console.log("Opening Excel spreadsheet...");
}
save(): void {
console.log("Saving Excel spreadsheet...");
}
close(): void {
console.log("Closing Excel spreadsheet...");
}
}
Each class implements the Document
interface and provides specific functionality for opening, saving, and closing the respective document type.
Now, we define an abstract creator class with a factory method:
// DocumentCreator.ts
abstract class DocumentCreator {
abstract createDocument(): Document;
openDocument(): void {
const doc = this.createDocument();
doc.open();
}
saveDocument(): void {
const doc = this.createDocument();
doc.save();
}
closeDocument(): void {
const doc = this.createDocument();
doc.close();
}
}
The DocumentCreator
class declares the createDocument
factory method, which returns a Document
object. It also provides methods to open, save, and close documents, utilizing the factory method.
Finally, we implement concrete creator classes for each document type:
// PdfDocumentCreator.ts
class PdfDocumentCreator extends DocumentCreator {
createDocument(): Document {
return new PdfDocument();
}
}
// WordDocumentCreator.ts
class WordDocumentCreator extends DocumentCreator {
createDocument(): Document {
return new WordDocument();
}
}
// ExcelDocumentCreator.ts
class ExcelDocumentCreator extends DocumentCreator {
createDocument(): Document {
return new ExcelDocument();
}
}
Each concrete creator class overrides the createDocument
method to return an instance of the corresponding document type.
Let’s see how we can use these classes in practice:
function main() {
const pdfCreator = new PdfDocumentCreator();
pdfCreator.openDocument();
pdfCreator.saveDocument();
pdfCreator.closeDocument();
const wordCreator = new WordDocumentCreator();
wordCreator.openDocument();
wordCreator.saveDocument();
wordCreator.closeDocument();
const excelCreator = new ExcelDocumentCreator();
excelCreator.openDocument();
excelCreator.saveDocument();
excelCreator.closeDocument();
}
main();
In this example, we create instances of PdfDocumentCreator
, WordDocumentCreator
, and ExcelDocumentCreator
to manage different document types. Each creator is responsible for creating and managing its specific document type.
The Factory Method pattern offers several benefits, particularly in terms of extensibility and code maintenance:
Extensibility: Adding new document types is straightforward. We simply create a new document class and a corresponding creator class without modifying existing code. This adheres to the Open/Closed Principle, which states that software entities should be open for extension but closed for modification.
Code Maintenance: The pattern centralizes the creation logic, making it easier to manage and update. If the document creation process changes, we only need to update the factory method in the relevant creator class.
Decoupling: The pattern decouples the client code from the concrete classes it needs to instantiate. This reduces dependencies and enhances flexibility.
Single Responsibility: Each creator class has a single responsibility: to create a specific type of document. This aligns with the Single Responsibility Principle, which states that a class should have only one reason to change.
To better understand how the Factory Method pattern works, let’s visualize the process of creating different document types using a sequence diagram:
sequenceDiagram participant Client participant Creator participant Document Client->>Creator: createDocument() Creator->>Document: new ConcreteDocument() Document-->>Creator: Document instance Creator-->>Client: Document instance Client->>Document: open() Client->>Document: save() Client->>Document: close()
In this sequence diagram, the client requests a document from the creator. The creator uses the factory method to instantiate the appropriate document type and returns it to the client. The client then interacts with the document instance.
To deepen your understanding of the Factory Method pattern, try modifying the code examples:
Add a New Document Type: Implement a new document type, such as a PowerPointDocument
, and create a corresponding PowerPointDocumentCreator
. Update the main
function to use the new creator.
Enhance Document Functionality: Add additional methods to the Document
interface, such as print
or export
, and implement these methods in each document class.
Refactor the Creator Class: Experiment with different ways to refactor the DocumentCreator
class to improve code readability or performance.
Before we conclude, let’s pose a few questions to reinforce your understanding of the Factory Method pattern:
The Factory Method pattern is a powerful tool for managing object creation in a flexible and extensible manner. By defining a common interface for creating objects and allowing subclasses to determine the specific object types, this pattern promotes code maintainability and adherence to design principles. As you continue to explore design patterns, remember that the Factory Method pattern is just one of many techniques available to help you write more scalable and maintainable code.
Remember, this is just the beginning. As you progress, you’ll build more complex and interactive applications. Keep experimenting, stay curious, and enjoy the journey!