Exploring the Abstract Factory Pattern in PHP

The Abstract Factory pattern is a creational design pattern that provides an interface for creating families of related or dependent objects without specifying their concrete classes. It involves multiple Factory Methods, one for each type of object to be created.

Let's simplify this concept with a restaurant analogy before diving into a real-world scenario.

Restaurant Analogy

Imagine a restaurant offering Asian, Italian, and Chinese cuisines. This diverse menu setup mirrors the Abstract Factory concept.

Abstract Factory: The restaurant's kitchen represents the Abstract Factory, equipped with different sections (concrete factories) complete with necessary ingredients and chefs skilled in specific cuisines like Chicken Biryani, Dumplings, and Margherita Pizza.

Concrete Factories: These are the specialized chefs, each overseeing their respective cuisine sections.

Products: The various types of dishes served, such as curries, rice, noodles, and pizzas.

Concrete Products: The actual dishes prepared, like Chicken Curry, Chow Mein, and Margherita Pizza, are the concrete products.

When a customer orders an Italian meal, the restaurant manager directs the order to the Italian chef (Concrete Factory) in the kitchen (Abstract Factory). The chef then prepares a specific dish (Concrete Product), like Margherita Pizza, following the general method of preparing Italian dishes (Product).

Real-world Scenario: Student Accommodation Marketplace

Consider a student accommodation marketplace offering different types of accommodations, each with unique amenities.

A naive implementation might look like this:

function getDescription(string $accommodation_type): string {
    if ($accommodation_type == 'apartment') {
        return 'Example description for an apartment';
    } elseif ($accommodation_type == 'shared-room') {
        return 'Example description for a shared-room';
    } else {
        throw new Exception('Invalid accommodation type');
    }
}

function getAmenities(string $accommodation_type): array {
    if ($accommodation_type == 'apartment') {
        return ['TV', 'private bathroom', 'private kitchen', 'private entrance'];
    } elseif ($accommodation_type == 'shared-room') {
        return ['TV', 'shared bathroom', 'shared kitchen', 'shared entrance'];
    } else {
        throw new Exception('Invalid accommodation type');
    }
}

This approach, while functional, violates the Open/Closed Principle and the Single Responsibility Principle, and is prone to errors as more accommodation types are introduced.

Abstract Factory to the Rescue

Let's refactor our codebase to utilize the Abstract Factory pattern:

interface Description
{
    public function get(): string;
}

interface Amenity
{
    public function get(): array;
}

class ApartmentDescription implements Description
{
    public function get(): string
    {
        return 'Example description for an apartment';
    }
}

class SharedRoomDescription implements Description
{
    public function get(): string
    {
        return 'Example description for a shared-room';
    }
}

class ApartmentAmenities implements Amenity
{
    public function get(): array
    {
        return ['TV', 'private bathroom', 'private kitchen', 'private entrance'];
    }
}

class SharedRoomAmenities implements Amenity
{
    public function get(): array
    {
        return ['TV', 'shared bathroom', 'shared kitchen', 'shared entrance'];
    }
}

With these interfaces and classes, we can introduce as many accommodation types as needed, ensuring compatibility and adherence to design principles.

Next, define our factory interface and concrete factories:

interface AccommodationFactory
{
    public function createDescription(): Description;
    public function createAmenities(): Amenity;
}

class ApartmentFactory implements AccommodationFactory
{
    public function createDescription(): Description
    {
        return new ApartmentDescription();
    }

    public function createAmenities(): Amenity
    {
        return new ApartmentAmenities();
    }
}

class SharedRoomFactory implements AccommodationFactory
{
    public function createDescription(): Description
    {
        return new SharedRoomDescription();
    }

    public function createAmenities(): Amenity
    {
        return new SharedRoomAmenities();
    }
}

Usage

This pattern allows for flexible and scalable code, adhering to solid design principles. Accommodations can be dynamically created with their respective descriptions and amenities, simplifying additions and modifications.

$factories = [
    'apartment' => new ApartmentFactory(),
    'shared-room' => new SharedRoomFactory(),
];

$accommodation_type = 'apartment'; // Example type
$factory = $factories[$accommodation_type];

return [
    'description' => $factory->createDescription()->get(),
    'amenities' => $factory->createAmenities()->get(),
];

Conclusion

The Abstract Factory pattern is invaluable for creating objects within a family or theme without specifying their concrete classes. It promotes a modular and scalable approach to software development, ensuring objects created are compatible with each other. Understanding and implementing this pattern can significantly enhance the structure and maintainability of your PHP projects.