DEV Community

Cover image for Understanding the Factory Method Design Pattern
bilel salem
bilel salem

Posted on

Understanding the Factory Method Design Pattern

Hello everyone, السلام عليكم و رحمة الله و بركاته

The Factory Method 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. It helps in dealing with the problem of creating objects without having to specify the exact class of the object that will be created. This is particularly useful in scenarios where the creation process is complex or involves multiple steps.

What Problem Does the Factory Method Solve?

  1. Class Instantiation Issues: Directly instantiating objects using new can lead to code that is tightly coupled with specific classes. This makes the code difficult to maintain and extend.

  2. Complex Object Creation: When object creation involves several steps or configurations, using a constructor can be cumbersome and hard to read.

  3. Subclasses Control: It provides a way for subclasses to decide which class to instantiate, allowing for more flexible and reusable code.

Key Concepts of the Factory Method

  • Product: The interface or abstract class defining the objects that the factory method will create.
  • Concrete Product: The implementation of the product interface.
  • Creator: The abstract class or interface that declares the factory method.
  • Concrete Creator: The class that implements the factory method to create an object of the Concrete Product class.

Real-World Example: Document Creation

Consider a scenario where we have an application that can create different types of documents such as Word Documents, PDF Documents, and Excel Sheets. The application should be able to generate these documents without knowing their specific implementation details.

Step-by-Step Implementation in TypeScript

  1. Define the Product Interface
interface IDocument {
    open(): void;
    save(): void;
    close(): void;
}
Enter fullscreen mode Exit fullscreen mode
  1. Concrete Products
class WordDocument implements IDocument {
    open(): void {
        console.log("Opening Word Document");
    }
    save(): void {
        console.log("Saving Word Document");
    }
    close(): void {
        console.log("Closing Word Document");
    }
}

class PDFDocument implements IDocument {
    open(): void {
        console.log("Opening PDF Document");
    }
    save(): void {
        console.log("Saving PDF Document");
    }
    close(): void {
        console.log("Closing PDF Document");
    }
}

class ExcelDocument implements IDocument {
    open(): void {
        console.log("Opening Excel Document");
    }
    save(): void {
        console.log("Saving Excel Document");
    }
    close(): void {
        console.log("Closing Excel Document");
    }
}
Enter fullscreen mode Exit fullscreen mode
  1. Creator Abstract Class
abstract class DocumentCreator {
    public abstract createDocument(): IDocument;

    public newDocument(): void {
        const doc = this.createDocument();
        doc.open();
        doc.save();
        doc.close();
    }
}
Enter fullscreen mode Exit fullscreen mode
  1. Concrete Creators
class WordDocumentCreator extends DocumentCreator {
    public createDocument(): IDocument {
        return new WordDocument();
    }
}

class PDFDocumentCreator extends DocumentCreator {
    public createDocument(): IDocument {
        return new PDFDocument();
    }
}

class ExcelDocumentCreator extends DocumentCreator {
    public createDocument(): IDocument {
        return new ExcelDocument();
    }
}
Enter fullscreen mode Exit fullscreen mode
  1. Client Code
class Application {
    public static main(): void {
        let creator: DocumentCreator;

        creator = new WordDocumentCreator();
        creator.newDocument();

        creator = new PDFDocumentCreator();
        creator.newDocument();

        creator = new ExcelDocumentCreator();
        creator.newDocument();
    }
}

Application.main();
Enter fullscreen mode Exit fullscreen mode

Explanation

  1. IDocument Interface: This interface defines the methods open(), save(), and close() that all document types must implement.

  2. Concrete Products: WordDocument, PDFDocument, and ExcelDocument are classes that implement the IDocument interface. Each class provides its own implementation for the methods defined in the interface.

  3. DocumentCreator Abstract Class: This abstract class declares the factory method createDocument(). It also provides a newDocument() method that calls the factory method to create a document and then performs a series of operations on it.

  4. Concrete Creators: WordDocumentCreator, PDFDocumentCreator, and ExcelDocumentCreator are subclasses of DocumentCreator. Each subclass implements the createDocument() method to instantiate a specific type of document.

  5. Client Code: The Application class demonstrates how to use the factory method pattern. It creates instances of different document creators and calls the newDocument() method to generate and operate on documents.

Benefits of Using the Factory Method Pattern in TypeScript

  • Decoupling: The client code (Application) does not need to know the exact class of the document it works with. It only interacts with the IDocument interface and the DocumentCreator abstract class.

  • Single Responsibility: Each creator class is responsible for instantiating a specific type of document. The document classes handle their own specific behaviors.

  • Flexibility and Extensibility: Adding a new type of document is straightforward. You can create a new class that implements the IDocument interface and a new creator class that extends DocumentCreator.

  • Maintainability: Changes in the creation process of specific documents do not affect the client code or other document types.

By using the Factory Method pattern, you can create complex applications with different features without complicating the code, adhering to principles of clean code and design patterns.

Top comments (4)

Collapse
 
efpage profile image
Eckehard

Maybe I did not get the point, but isn´t this the reason why people use abstract classes? If you use an abstract base class "document" in your client code, you do not need to know the details of implementation. But your client code will be able to deal with any document that is a decendent of "document". So, you simply inherit your Word or PDF-document from "document" to make them part of the family.

I´m not aware if there are any reasons this does not work in Typescript, but this is the way things are solved in other OO languages.

Collapse
 
bilelsalemdev profile image
bilel salem • Edited

Here is a detailed explanation why Abstract Classes are not same as Factory Method Pattern

Feature Abstract Classes Factory Method Pattern
Purpose Define common behavior and enforce method implementation in subclasses. Encapsulate object creation and allow subclasses to determine the concrete class to instantiate.
Usage When you want to define a common interface and shared behavior among a group of related classes. When the creation process is complex, varies between subclasses, or should be decoupled from client code.
Polymorphism Yes, allows different subclasses to be treated uniformly. Yes, but focuses on the creation process being polymorphic.
Code Reuse Allows code reuse by defining common methods in the abstract class. Separates creation logic from the business logic, allowing reuse of the creation process.
Encapsulation Encapsulates common behavior and properties of subclasses. Encapsulates object creation logic, making it easier to manage and change.
Client Code Needs to know the specific subclass to instantiate. Decoupled from concrete classes, only interacts with abstract creator and product interfaces.
Flexibility Limited to defining behavior and properties; does not handle instantiation flexibility. High, as it allows subclasses to change the class of objects that will be created without modifying the client code.
Extensibility Adding new behavior requires modifying the abstract class or existing subclasses. Adding new products requires creating new creator subclasses, without modifying existing client code.
Instantiation Direct instantiation of subclasses, leading to tighter coupling between client code and concrete classes. Uses a factory method to create objects, promoting loose coupling and adherence to the Open/Closed Principle.

Example Abstract Class in TypeScript

abstract class Document {
    abstract open(): void;
    abstract save(): void;
    abstract close(): void;

    print(): void {
        console.log("Printing document");
    }
}

class WordDocument extends Document {
    open(): void {
        console.log("Opening Word Document");
    }
    save(): void {
        console.log("Saving Word Document");
    }
    close(): void {
        console.log("Closing Word Document");
    }
}

class PDFDocument extends Document {
    open(): void {
        console.log("Opening PDF Document");
    }
    save(): void {
        console.log("Saving PDF Document");
    }
    close(): void {
        console.log("Closing PDF Document");
    }
}

// Client code
function processDocument(doc: Document) {
    doc.open();
    doc.save();
    doc.close();
    doc.print();
}

const wordDoc: Document = new WordDocument();
processDocument(wordDoc);

const pdfDoc: Document = new PDFDocument();
processDocument(pdfDoc);
Enter fullscreen mode Exit fullscreen mode

Example Factory Method Pattern in TypeScript

interface Document {
    open(): void;
    save(): void;
    close(): void;
}

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");
    }
}

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");
    }
}

abstract class DocumentCreator {
    public abstract createDocument(): Document;

    public newDocument(): void {
        const doc = this.createDocument();
        doc.open();
        doc.save();
        doc.close();
    }
}

class WordDocumentCreator extends DocumentCreator {
    public createDocument(): Document {
        return new WordDocument();
    }
}

class PDFDocumentCreator extends DocumentCreator {
    public createDocument(): Document {
        return new PDFDocument();
    }
}

// Client code
class Application {
    public static main(): void {
        let creator: DocumentCreator;

        creator = new WordDocumentCreator();
        creator.newDocument();

        creator = new PDFDocumentCreator();
        creator.newDocument();
    }
}

Application.main();
Enter fullscreen mode Exit fullscreen mode

By using this table and examples, you can see how abstract classes and the Factory Method pattern serve different purposes and how they can complement each other .

Collapse
 
efpage profile image
Eckehard • Edited

Thank you much for your detailed explanation. Maybe this goes beyond the tasks I commonly used classes for. I just have still some questions:

Why did you not add newDocument to the abstract class like we would commonly do (this is JS syntax)? Isn´t this the task of an abstract class?

class Document {
    constructor(){ ... }
    open(){ ... };
    save(){ ... };
    close(){ ... };
    newDocument(): void {
        this.open();
        this.save();
        this.close();
    }
}
class WordDocument extends Document { ... }

creator = new WordDocument();
creator.newDocument();
Enter fullscreen mode Exit fullscreen mode

Did you use ChatGPT for the examples, as the code seems to have some issuses?

    public newDocument(): void {
        const doc = this.createDocument();
        doc.open();
        doc.save();
        doc.close();
        return doc; // <== missing
    }
Enter fullscreen mode Exit fullscreen mode

newDocument does not work without actually returning the document

        let creator: DocumentCreator;

        creator = new WordDocumentCreator();
        creator.newDocument();

        creator = new PDFDocumentCreator();  // prevent access access to the WordDocument without removing the instance? 
        creator.newDocument();

Enter fullscreen mode Exit fullscreen mode

In my understanding assigning a new object to creator will override the accessor without removing the instance, so you will have no access to the object anymore and get a memory leak.

Or did I get something wrong?

Thread Thread
 
bilelsalemdev profile image
bilel salem • Edited

First, I updated the code of the article because in Typescript the Document is a built-in interface representing the HTML Document Object Model (DOM) interface .

Second, yes you can add newDocument to the abstract class and this will facilitates our code, Thank you for the information .

Third, Regarding your concern about memory leaks and object access:

Object Lifetime: Assigning a new object to creator does not remove the previous instance. It only changes the reference in the creator variable. The previous instance is still accessible through any other references or will be garbage collected if no references exist.

Returning the Document: In your example, newDocument returns the created document. This ensures that the client code can maintain a reference to the created document, preventing memory leaks and allowing continued access.