Abstract Factory Pattern

 


In Factory pattern, we create object without exposing the creation logic to the client and refer to newly created object using a common interface.

Simple Factory

In App:

ShapeFactory factory = new ShapeFactory()
Shape shape = factory.getObject("triangle");

In Factory:

public Shape getShape(String name) {
    if (name.equals("circle") return new Circle( );
    else if (name.equals("triangle") return new Triangle( );
    throw new IllegalArgumentException("Invalid shape name");
}

Problem: If we add a new shape, for example, Square, we need to alter the getShape method, which violates the Open Closed Principle. To avoid this, we can use Reflection

Java Reflection

Reflection is a feature in Java that allows an executing program to examine or "introspect" upon itself, and manipulate internal properties of the program. For example, it's possible for a Java class to obtain the names of all its methods and display them.
Java Reflection is useful because it supports dynamic retrieval of information about classes and data structures by name.

We can use this to our advantage in the Factory class, to reduce conditional code:

In App:

ShapeFactory factory = new ShapeFactory()
Shape shape = factory.getObject("domain.shapes.Triangle");

In Factory:

public Shape getShape(String className) {
    try {
        Class class = Class.forName(className);
        Shape shape = (Shape) class.getConstructor().newInstance();
    } catch (Exception e) {
        throw new IllegalArgumentException("Invalid shape class");
    }
}

This Factory class won't need to be changed if a new shape is added.
Problem: client needs to know the exact class name and package structure. We can move the package structure part ("domain.shapes.") to the getShape method, but the client still needs the correct class name. We would like to still be able to use "triangle", "circle" or other simple strings as parameters in the method. We can use an Enum.

Factory with Enum

ShapeEnum:

public enum ShapeEnum {
    CIRCLE ("Circle"),
    TRIANGLE ("Triangle");

    private String name;

    public ShapeEnum(String name) {
        this.name= name;
    }

    public String getClassName() {
        return "domain.shapes." + name;
    }
}

In Factory:

public Shape getShape(String shape) {
    try {
        ShapeEnum shapeEnum = ShapeEnum.valueOf(shape.toUpperCase());
        String className = shapeEnum.getClassName();
        Class<?> class = Class.forName(className);
        return (Shape) class.newInstance();
    } catch (Exception e) {
        throw new IllegalArgumentException("Invalid shape");
    }
}

In this case, when adding the Square shape, we just have to make a new entry in the enum (SQUARE("Square")). This violates the OCP principle again, but is way easier to maintain, very readable, and supports OCP for the rest of the application (client and factory don't need to change anymore) and the client doesn't need to know the package structure or exact class name.

Design principles (link):

  • (OK) Single Responsibility Principle - the factory has the responsability of creating an object
  • (OK) Open/Closed Principle - Adding a new subclass does not require the client code to change. How much the rest of the code needs to change depends on the method that we use. Java Reflexion removes the need for conditional code.
  • (OK) Dependency Inversion Principle - the cliend and the concrete classes both depend on the abstraction.

Relations with Other Patterns

  • A Factory class can be implemented as a Singleton.

Comments

Popular Posts