Factory Method Pattern

MaverickMaverick
6 min read

The Factory Design Pattern is a creational pattern that provides a way to encapsulate the instantiation logic of a set of related classes. It defines an interface for creating an object but allows subclasses to alter the type of objects that will be created. This pattern is useful in situations where a class can't anticipate the class of objects it must create and when the class doesn't want to withhold the responsibility of creation within itself.

In simple words, the Factory Design Pattern is like a factory that makes stuff. Imagine you have a factory that makes different types of toys—like cars, dolls, and robots. Instead of making the toys directly, you ask the factory to make them for you. This way, you don’t have to know how these toys are made; you just tell the factory what type of toy you want.

Let’s go look into

  • When to Use Factory Design Pattern

  • Why Use Factory Design Pattern

  • Components of Factory Design Pattern

  • Learning Example

  • Real world implementation

  • Pros and Cons of Factory Design Pattern

When to Use Factory Design Pattern

The Factory Design Pattern is especially useful in situations where you need to create instances of many different classes that share a common interface or base class, but the exact class that should be instantiated isn't known until runtime. By using the Factory Pattern, you can delegate the responsibility of instantiation to a separate component.

  • Complex Object Creation Logic: When object creation involves complex logic, such as validating input or setting various properties, you can encapsulate this logic within the factory.

  • Centralized Object Creation: When you want to centralize the creation process to maintain consistency and control over how objects are created.

  • Configuration Based Object Creation: When you want to instantiate objects based on configuration, runtime parameters, or conditions.

  • Avoiding Dependency on Concrete Classes: When you want to avoid a dependency on specific concrete classes and prefer working with interfaces or abstract classes.

  • Object Variety: When you need to manage and instantiate various types or families of objects that share a common interface or superclass.

Why Use Factory Design Pattern

  • Encapsulation: Encapsulates the creation logic, preventing tight coupling between the code that needs the objects and the classes that instantiate them.

  • Flexibility: Makes it easy to add new product types without changing the client code that uses them.

  • Code Maintenance: Improves code maintenance by centralizing the instantiation logic, making it easier to update or change the creation process.

  • Testability: Facilitates easier testing since the creation logic is separate from the business logic.

Components of Factory Design Pattern

  1. Product Interface: Declares the interfaces for the various types of products.

  2. Concrete Products: Implements the product interface.

  3. Factory Interface (Creator): Declares the creation method which returns an object of the product type.

  4. Concrete Factory (ConcreteCreator): Implements the creation method to instantiate and return an object of the product type.

Example

Lets go with a simple example for the learning purpose

  1. Define the Shape(Product) Interface:
public interface Shape {
    void draw();
}
  1. Implement Concrete Shape Classes:
public class Circle implements Shape {
    @Override
    public void draw() {
        System.out.println("Drawing a Circle");
    }
}

public class Square implements Shape {
    @Override
    public void draw() {
        System.out.println("Drawing a Square");
    }
}

public class Rectangle implements Shape {
    @Override
    public void draw() {
        System.out.println("Drawing a Rectangle");
    }
}
  1. Create the Factory Class:
public class ShapeFactory {
    // Use getShape method to get object of type Shape
    public Shape getShape(String shapeType) {
        if (shapeType == null) {
            return null;
        }
        if (shapeType.equalsIgnoreCase("CIRCLE")) {
            return new Circle();
        } else if (shapeType.equalsIgnoreCase("SQUARE")) {
            return new Square();
        } else if (shapeType.equalsIgnoreCase("RECTANGLE")) {
            return new Rectangle();
        }
        return null;
    }
}
  1. Use the Factory to Get Objects:
public class FactoryPatternDemo {
    public static void main(String[] args) {
        ShapeFactory shapeFactory = new ShapeFactory();

        // Get an object of Circle and call its draw method.
        Shape shape1 = shapeFactory.getShape("CIRCLE");
        shape1.draw();

        // Get an object of Square and call its draw method.
        Shape shape2 = shapeFactory.getShape("SQUARE");
        shape2.draw();

        // Get an object of Rectangle and call its draw method.
        Shape shape3 = shapeFactory.getShape("RECTANGLE");
        shape3.draw();
    }
}

Real-Life Example

Imagine you're working at a big company like Amazon. Amazon has different databases that store data, like MySQL, Oracle, and PostgreSQL. Instead of having different parts of the application figure out how to connect to these databases, you create a Database Factory.

  • You use the factory to get database connections without worrying about the details of establishing connections.

  • The Factory knows how to create connections to MySQL, Oracle, and PostgreSQL databases.

Whenever you need a database connection, you ask the factory, and it gives you a connection based on your needs.

By using the Factory Design Pattern, you make your code easier to maintain and extend, since the details of creating different objects (or connections) are centralized in the factory, and you don’t need to change your entire codebase when adding new types.

Step 1: Define the Product Interface

// DatabaseConnection.java
public interface DatabaseConnection {
    void connect();
    void disconnect();
}

Step 2: Create Concrete Products

// MySQLConnection.java
public class MySQLConnection implements DatabaseConnection {
    @Override
    public void connect() {
        System.out.println("Connecting to MySQL database...");
    }

    @Override
    public void disconnect() {
        System.out.println("Disconnecting from MySQL database...");
    }
}

// OracleConnection.java
public class OracleConnection implements DatabaseConnection {
    @Override
    public void connect() {
        System.out.println("Connecting to Oracle database...");
    }

    @Override
    public void disconnect() {
        System.out.println("Disconnecting from Oracle database...");
    }
}

// PostgreSQLConnection.java
public class PostgreSQLConnection implements DatabaseConnection {
    @Override
    public void connect() {
        System.out.println("Connecting to PostgreSQL database...");
    }

    @Override
    public void disconnect() {
        System.out.println("Disconnecting from PostgreSQL database...");
    }
}

Step 3: Create Factory Interface

// DatabaseConnectionFactory.java
public interface DatabaseConnectionFactory {
    DatabaseConnection createConnection();
}

Step 4: Create Concrete Factories

// MySQLConnectionFactory.java
public class MySQLConnectionFactory implements DatabaseConnectionFactory {
    @Override
    public DatabaseConnection createConnection() {
        return new MySQLConnection();
    }
}

// OracleConnectionFactory.java
public class OracleConnectionFactory implements DatabaseConnectionFactory {
    @Override
    public DatabaseConnection createConnection() {
        return new OracleConnection();
    }
}

// PostgreSQLConnectionFactory.java
public class PostgreSQLConnectionFactory implements DatabaseConnectionFactory {
    @Override
    public DatabaseConnection createConnection() {
        return new PostgreSQLConnection();
    }
}

Step 5: Create Client Code

public class Main {
    public static void main(String[] args) {
        DatabaseConnectionFactory mysqlFactory = new MySQLConnectionFactory();
        DatabaseConnection mysqlConnection = mysqlFactory.createConnection();
        mysqlConnection.connect();
        mysqlConnection.disconnect();

        DatabaseConnectionFactory oracleFactory = new OracleConnectionFactory();
        DatabaseConnection oracleConnection = oracleFactory.createConnection();
        oracleConnection.connect();
        oracleConnection.disconnect();

        DatabaseConnectionFactory postgresFactory = new PostgreSQLConnectionFactory();
        DatabaseConnection postgresConnection = postgresFactory.createConnection();
        postgresConnection.connect();
        postgresConnection.disconnect();
    }
}

Pros

  • Encapsulation of Object Creation: It abstracts the creation process, making it easier to manage and modify without affecting the client code.

  • Promotes Code Reusability: By centralizing object creation, it can reduce code duplication and improve maintainability.

  • Flexibility and Scalability: New types of objects can be introduced and instantiated through the factory without changing the existing code that uses the factory interface.

  • Enhanced Code Readability: Simplifies client code by hiding complex creation logic.

  • Loose Coupling: The client code relies on the factory interface rather than concrete classes, leading to a lower dependency on specific implementations.

Cons

  • Increased Complexity in Design: Introduces an extra layer of abstraction, which can complicate the design and understanding of the system.

  • Maintenance Overhead: Changes in object creation may require updates to the factory classes, increasing maintenance effort.

  • Performance Overhead: Introducing factories can add a slight performance overhead due to the additional method calls and possible indirection.

  • Harder Debugging and Tracing: Indirection via factory methods can sometimes make debugging and tracing through code paths more difficult.

0
Subscribe to my newsletter

Read articles from Maverick directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Maverick
Maverick