Exploring the Abstract Factory Pattern in PHP
What is Abstract Factory?
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.
I know, I know its quite a mouthful. Lets try to understand this pattern using restaurant analogy.
Restaurant Analogy
Imagine a restaurant where Asian, Italian and Chinese food is served. Nothing wrong with wishful thinking, I hope one day I will find such restaurant. Any way, lets move on and compare the restaurant's setup with Abstract factory.
Abstract Factory
The entire kitchen in the restaurant can be classified as Abstract Factory. The kitchen has different sections with required ingredients and qualified chefs who can prepare the specialised cuisines like Chicken Biryani, Dumplings and Margherita pizza.
Concrete Factories
These would be the qualified chefs. e.g. Chinese chef would be supervising Chinese dishes, while Asian chef would supervising Asian food and so on.
Products
These would be the different types of dishes e.g. curries, rice, noodles and pizzas etc.
Concrete Products
These would be the actual dishes prepared that we can order. e.g. Chicken curry, Chow Mein and Margarita are concrete products.
If a customer wants an Italian meal, the restaurant manager directs the order to the Italian chef (Concrete Factory) in the kitchen (Abstract Factory). The Italian chef then prepares a specific Italian dish (Concrete Product) like Margherita Pizza along with its sides, following the general method of preparing Italian dishes (Product).
Using a real world scenario
Imagine a student accommodation marketplace where accommodations can be of different types e.g. shared room and apartment etc. Each type of accommodation might have different amenities like bathroom, kitchen and entrance etc
A naive way of accomplishing above would be some thing like below:
Note:
I am using hardcoded data but this could very well be coming from database or API.
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');
}
}
Lets suppose, user search for apartment, we take that input and make certain decisions in the codebase before returning the results. May be some thing like below;
$accommodation_type = 'apartment'; // a query param
// We will get all accommodations of type apartment and then run it through some class
// or function to get the description and amenities for each one
return [
'description' => getDescription($accommodation_type),
'amenities' => getAmenities($accommodation_type),
];
Well, thats not bad now is it? one might argue. Lets try to break down the above code and understand some of the drawbacks. This exercise could also help us understand some of the other design principles.
Violations and Drawbacks
- If you need to add another accommodation type you will need to make changes in two different functions. Imagine if you have 10 different features tucked away in 10 different functions. You can do the maths. This is a clear violation of Open/Closed principle.
- Even though
getDescription
andgetAmenities
logic is tucked away in its relevant function, which by the way is much better than using conditionals, still its keeping track of different accommodation types which breaks the Single Responsibility Principle. - The above logic is more prone to errors. As we introduce other types of accommodation, modifying the existing logic can introduce bugs.
Abstract factory to the rescue
Lets refactor our codebase and try utilize the Abstract Factory
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 the above contracts in place, we can add as many accommodation types as we want. As long as they adhere to the contract, we are guaranteed to get description or amenities for an accommodation type. Once we have the above contracts in place, lets define our factory contract and then concrete factories
interface AccommodationFactory
{
public function createDescription(): string;
public function createAmenities(): array;
}
class ApartmentFactory implements AccommodationFactory
{
public function createDescription(): string
{
return (new ApartmentDescription())->get();
}
public function createAmenities(): array
{
return (new ApartmentAmenities())->get();
}
}
class SharedRoomFactory implements AccommodationFactory
{
public function createDescription(): string
{
return (new SharedRoomDescription())->get();
}
public function createAmenities(): array
{
return (new SharedRoomAmenities())->get();
}
}
Usage
By leveraging the Abstract Factory pattern, we can seamlessly obtain a suitable factory and subsequently fetch the description and amenities as needed, anywhere within our application.
This approach not only adheres to the Open/Closed and Single Responsibility principles but also ensures that our existing codebase remains intact, even when new accommodation types are introduced into the system.
We could leverage Table look up pattern to resolve the relevant accommodation factory as below:
$factories = [
'apartment' => ApartmentFactory::class,
'shared-room' => SharedRoomFactory::class,
];
if (!array_key_exists($accommodation_type, $factories)) {
throw new Exception('Invalid accommodation type');
}
$factoryClass = $factories[$accommodation_type];
$factory = new $factoryClass();
return [
'description' => $factory->createDescription(),
'amenities' => $factory->createAmenities(),
];
Conclusion:
The Abstract Factory pattern is a powerful design pattern that allows for the creation of objects that belong to a family or theme without specifying their concrete classes. By using this pattern, you can ensure that the objects you create are compatible with each other, as they come from the same family. Having a better understanding of this pattern can help you structure your code in a modular and scalable way.