Boost Modularity with the Factory Method Pattern

The Factory Method Pattern is a creational design pattern that provides an interface for creating objects, but delegates the actual instantiation logic to subclasses or derived classes. It allows the client code to create objects without being tied to the specific classes of those objects. This decoupling of object creation from client code promotes flexibility, extensibility, and code reusability.

When to use Factory Pattern

  1. Object creation complexity: When object creation involves complex logic, such as conditional branching or multiple steps, using the Factory Pattern centralizes this logic in one place, making it easier to manage and modify.
  2. Decoupling object creation: When you want to decouple the client code from the specific classes of objects being created, the Factory Pattern provides a common interface for object creation. This promotes loose coupling and allows for easier substitution of different implementations.
  3. Creating objects based on runtime conditions: If the type of object to be created depends on runtime conditions or configuration parameters, the Factory Pattern allows you to encapsulate the decision-making process within the factory, simplifying object creation.
  4. Code extensibility: The Factory Pattern supports the open-closed principle, enabling easy extension of the codebase with new types of objects. You can introduce new subclasses or derived classes in the factory without modifying the existing client code.
  5. Testability and dependency injection: By using the Factory Pattern, you can create factory interfaces or abstract factories, which facilitates unit testing and allows for dependency injection. This enables easier mocking of object creation during testing.

Implementation

Github

// Product: Ticket interface
public interface Ticket {
    void purchase();
}

// Concrete Products
public class MovieTicket implements Ticket {
    @Override
    public void purchase() {
        System.out.println("Purchasing a movie ticket...");
        // Additional movie ticket specific logic
    }
}

public class ConcertTicket implements Ticket {
    @Override
    public void purchase() {
        System.out.println("Purchasing a concert ticket...");
        // Additional concert ticket specific logic
    }
}

public class MatchTicket implements Ticket {
    @Override
    public void purchase() {
        System.out.println("Purchasing a match ticket...");
        // Additional match ticket specific logic
    }
}

public enum TicketType {
    MOVIE,
    CONCERT,
    MATCH;
}

// Factory: TicketFactory class
public class TicketFactory {
    public static Ticket createTicket(TicketType type) {
        switch(type) {
            case MOVIE:
                return new MovieTicket();
            case CONCERT:
                return new ConcertTicket();
            case MATCH:
                return new MatchTicket();
        }
        throw new IllegalArgumentException("Invalid ticket type.");
    }
}


// Usage
public class Application {
    public static void main(String[] args) {
        Ticket movieTicket = TicketFactory.createTicket(TicketType.MOVIE);
        movieTicket.purchase(); // Output: Purchasing a movie ticket...

        Ticket concertTicket = TicketFactory.createTicket(TicketType.CONCERT);
        concertTicket.purchase(); // Output: Purchasing a concert ticket...

        Ticket matchTicket = TicketFactory.createTicket(TicketType.MATCH);
        matchTicket.purchase(); // Output: Purchasing a match ticket...
    }
}
Java
Factory Pattern Class Diagram
Factory Pattern Class Diagram

Unit Tests

import org.junit.Test;
import static org.junit.Assert.*;

public class TicketBookingSystemTest {

    @Test
    public void testMovieTicketCreation() {
        Ticket movieTicket = TicketFactory.createTicket("movie");
        assertNotNull(movieTicket);
        assertTrue(movieTicket instanceof MovieTicket);
    }

    @Test
    public void testConcertTicketCreation() {
        Ticket concertTicket = TicketFactory.createTicket("concert");
        assertNotNull(concertTicket);
        assertTrue(concertTicket instanceof ConcertTicket);
    }

    @Test(expected = IllegalArgumentException.class)
    public void testInvalidTicketType() {
        TicketFactory.createTicket("invalid"); // Should throw IllegalArgumentException
    }
}
Java

Factory Method pattern in JVM

  1. Java Collection Framework: The java.util.Collections class contains various factory methods, such as emptyList(), singletonList(), and unmodifiableList(), which create specialized instances of collection objects.
  2. JDBC (Java Database Connectivity): JDBC utilizes the Factory Pattern extensively. The DriverManager class provides a factory method called getConnection() to create instances of Connection objects for database interactions.
  3. java.util.Calendar#getInstance()
  4. java.util.ResourceBundle#getBundle()
  5. java.text.NumberFormat#getInstance()
  6. java.nio.charset.Charset#forName()
  7. java.net.URLStreamHandlerFactory#createURLStreamHandler(String) (returns different singleton objects, depending on a protocol)
  8. java.util.EnumSet#of()

Common Incorrect Implementations

  1. Violation of Single Responsibility Principle (SRP): If the factory class is responsible for both object creation and performing additional unrelated tasks, it violates the SRP. The factory class should focus solely on creating objects, while other responsibilities should be delegated to separate classes.
  2. Lack of abstraction: If the factory class directly instantiates concrete product objects instead of using interfaces or abstract classes, it limits flexibility and makes it difficult to introduce new product variants. The Factory Pattern should utilize abstraction to allow for easy extensibility and substitution of product implementations.
  3. Complex conditional logic: If the factory class has complex conditional logic to determine the type of object to create, it may indicate a violation of the Open-Closed Principle (OCP). The factory class should be open for extension but closed for modification, allowing the addition of new product types without modifying the existing factory code.
  4. Tight coupling between the factory and client code: If the client code is tightly coupled with the specific factory implementation, it restricts the ability to switch or use different factory implementations. The factory class should be decoupled from the client code through the use of interfaces or dependency injection.
  5. Lack of error handling: If the factory class does not handle or provide appropriate error handling for invalid or unsupported object types, it can lead to runtime exceptions or unexpected behavior. The factory should handle such cases gracefully, either by throwing exceptions or providing default/fallback behavior.