20
DecUnderstanding Abstract Factory Design Pattern
18 Sep 2024
Intermediate
216K Views
46 min read
Abstract Factory Design Patterns
Abstract Factory Design Patterns is an important concept in software design. It enables you to define families of linked or dependent objects without defining the concrete classes. This provides a straightforward and adaptable interface for constructing objects, making the system easier to comprehend and operate.In this Design Patterns Tutorial, we will discuss the Abstract Factory Design Pattern, including "What is the Abstract Factory Design Pattern?" and "When to Use the Abstract Factory Design Pattern?" We'll also see examples of the Abstract Factory Design Pattern in action. So, let us begin with "What is the Abstract Factory Design Pattern?".
What is an Abstract Factory Pattern?
- Abstract Factory Pattern is a creational design pattern that allows you to create families of related or dependent objects without specifying their specific classes.
- It enables you to create many types of connected objects while assuring compatibility with one another.
- This pattern is beneficial when a system must function independently of how its objects are formed, constructed, and displayed.
When to use it?
- Multiple Families of Related Products: Use the Abstract Factory pattern to create families of linked or dependent objects without specifying their specific classes.
- Enforce Consistency: It guarantees that items from the same family are utilized together, which helps to preserve system consistency.
- Configuration Changes: This is ideal for changing the system configuration dynamically at runtime, allowing the establishment of different product families.
- Decoupling Code: Assists in decoupling code from concrete classes, which promotes loose coupling and increases maintainability.
Abstract Factory Pattern - UML Diagram & Implementation
The UML class diagram for the implementation of the abstract factory design pattern is given below:
The classes, interfaces, and objects in the above UML class diagram are as follows:
Abstract Factory (Interface)
- Describes a set of strategies for producing abstract products.
- Example: public interface. Abstract Factory { AbstractProduct createproduct(); }
ConcreteFactory (Class)
- Uses the AbstractFactory interface to create tangible products.
- Example: public class ConcreteFactory implements AbstractFactory { public AbstractProduct createProduct() { return new ConcreteProduct(); } }
AbstractProduct (Interface)
- Declares the interface for a type of product.
- Example: public interface AbstractProduct { void someMethod(); }
ConcreteProduct (Class)
- Implements the AbstractProduct interface to create a specific product.
- Example: public class ConcreteProduct implements AbstractProduct { public void someMethod() { // Implementation } }
Client (Class)
- Uses the AbstractFactory and AbstractProduct interfaces to create a family of related objects.
- Example: public class Client { private AbstractFactory factory; public Client(AbstractFactory factory) { this.factory = factory; } public void doSomething() { AbstractProduct product = factory.createProduct(); product.someMethod(); } }
These components work together as follows:
- The Client interacts with the AbstractFactory to generate AbstractProduct instances.
- ConcreteFactory offers the implementation details for creating specific ConcreteProduct objects.
- ConcreteProduct implements the AbstractProduct interface and provides the real behavior.
- This framework promotes flexibility and scalability by allowing you to introduce new product families without modifying the client code.
Real-Life Example
This diagram shows the Abstract Factory design pattern in the framework of automobile manufacturing. Here is a breakdown of the elements.
1. VehicleClient class
- Represents a client who works with the vehicle factory to obtain vehicles.
- Has fields for storing references to bikes and scooters.
- Includes techniques for obtaining the names of the bike and scooter.
2. Vehicle Factory Interface
- Defines a single interface for automotive producers.
- Specifies how to create Bike and Scooter objects.
3. Concrete Vehicle Factories (Honda Factory and Hero Factory)
- Implement the VehicleFactory interface.
- Provide real implementations for developing specific sorts of bikes and scooters (for example, Honda bikes and Hero scooters).
4. Bike and Scooter Interfaces
- Define a single interface for motorcycles and scooters.
- Determine a way to obtain the vehicle's name.
5. Concrete Bike and Scooter Classes
- Implement the bike and scooter interfaces.
- Provide detailed implementations for different types of bicycles and scooters.
6. Key points
- The Abstract Factory pattern enables free connectivity between clients and concrete vehicle factories.
- The client interacts with the abstract factory interface, which allows for the simple substitution of several physical factories.
- The pattern ensures that related items (bike and scooter) are produced together, resulting in uniformity.
- This approach is handy for creating families of related items without providing their exact classes.
Example
using System;
// Abstract Factory Interface
interface VehicleFactory
{
Bike GetBike(string bikeType);
Scooter GetScooter(string scooterType);
}
// Concrete Factory 1: Honda Factory
class HondaFactory : VehicleFactory
{
public Bike GetBike(string bikeType)
{
switch (bikeType)
{
case "Sports":
return new SportsBike();
case "Regular":
return new RegularBike();
default:
throw new ApplicationException($"Bike '{bikeType}' cannot be created");
}
}
public Scooter GetScooter(string scooterType)
{
switch (scooterType)
{
case "Sports":
return new Scooty();
case "Regular":
return new RegularScooter();
default:
throw new ApplicationException($"Scooter '{scooterType}' cannot be created");
}
}
}
// Concrete Factory 2: Hero Factory
class HeroFactory : VehicleFactory
{
public Bike GetBike(string bikeType)
{
switch (bikeType)
{
case "Sports":
return new SportsBike();
case "Regular":
return new RegularBike();
default:
throw new ApplicationException($"Bike '{bikeType}' cannot be created");
}
}
public Scooter GetScooter(string scooterType)
{
switch (scooterType)
{
case "Sports":
return new Scooty();
case "Regular":
return new RegularScooter();
default:
throw new ApplicationException($"Scooter '{scooterType}' cannot be created");
}
}
}
// Abstract Product A: Bike
interface Bike
{
string Name();
}
// Abstract Product B: Scooter
interface Scooter
{
string Name();
}
// Concrete Product A1: Regular Bike
class RegularBike : Bike
{
public string Name() => "Regular Bike - Name";
}
// Concrete Product A2: Sports Bike
class SportsBike : Bike
{
public string Name() => "Sports Bike - Name";
}
// Concrete Product B1: Regular Scooter
class RegularScooter : Scooter
{
public string Name() => "Regular Scooter - Name";
}
// Concrete Product B2: Scooty
class Scooty : Scooter
{
public string Name() => "Scooty - Name";
}
// Client Class
class VehicleClient
{
private Bike _bike;
private Scooter _scooter;
public VehicleClient(VehicleFactory factory, string type)
{
_bike = factory.GetBike(type);
_scooter = factory.GetScooter(type);
}
public string GetBikeName() => _bike.Name();
public string GetScooterName() => _scooter.Name();
}
// Main Program
class Program
{
static void Main()
{
VehicleFactory honda = new HondaFactory();
VehicleClient hondaClient = new VehicleClient(honda, "Regular");
Console.WriteLine("******* Honda **********");
Console.WriteLine(hondaClient.GetBikeName());
Console.WriteLine(hondaClient.GetScooterName());
hondaClient = new VehicleClient(honda, "Sports");
Console.WriteLine(hondaClient.GetBikeName());
Console.WriteLine(hondaClient.GetScooterName());
VehicleFactory hero = new HeroFactory();
VehicleClient heroClient = new VehicleClient(hero, "Regular");
Console.WriteLine("******* Hero **********");
Console.WriteLine(heroClient.GetBikeName());
Console.WriteLine(heroClient.GetScooterName());
heroClient = new VehicleClient(hero, "Sports");
Console.WriteLine(heroClient.GetBikeName());
Console.WriteLine(heroClient.GetScooterName());
}
}
import java.util.*;
// Abstract Factory
interface VehicleFactory {
Bike getBike(String bikeType);
Scooter getScooter(String scooterType);
}
// Concrete Factory 1: Honda Factory
class HondaFactory implements VehicleFactory {
public Bike getBike(String bikeType) {
switch (bikeType) {
case "Sports": return new SportsBike();
case "Regular": return new RegularBike();
default: throw new IllegalArgumentException("Bike '" + bikeType + "' cannot be created");
}
}
public Scooter getScooter(String scooterType) {
switch (scooterType) {
case "Sports": return new Scooty();
case "Regular": return new RegularScooter();
default: throw new IllegalArgumentException("Scooter '" + scooterType + "' cannot be created");
}
}
}
// Concrete Factory 2: Hero Factory
class HeroFactory implements VehicleFactory {
public Bike getBike(String bikeType) {
switch (bikeType) {
case "Sports": return new SportsBike();
case "Regular": return new RegularBike();
default: throw new IllegalArgumentException("Bike '" + bikeType + "' cannot be created");
}
}
public Scooter getScooter(String scooterType) {
switch (scooterType) {
case "Sports": return new Scooty();
case "Regular": return new RegularScooter();
default: throw new IllegalArgumentException("Scooter '" + scooterType + "' cannot be created");
}
}
}
// Abstract Product A: Bike
interface Bike {
String getName();
}
// Abstract Product B: Scooter
interface Scooter {
String getName();
}
// Concrete Product A1: Regular Bike
class RegularBike implements Bike {
public String getName() {
return "Regular Bike - Name";
}
}
// Concrete Product A2: Sports Bike
class SportsBike implements Bike {
public String getName() {
return "Sports Bike - Name";
}
}
// Concrete Product B1: Regular Scooter
class RegularScooter implements Scooter {
public String getName() {
return "Regular Scooter - Name";
}
}
// Concrete Product B2: Scooty
class Scooty implements Scooter {
public String getName() {
return "Scooty - Name";
}
}
// Client Class
class VehicleClient {
private Bike bike;
private Scooter scooter;
public VehicleClient(VehicleFactory factory, String type) {
this.bike = factory.getBike(type);
this.scooter = factory.getScooter(type);
}
public String getBikeName() {
return bike.getName();
}
public String getScooterName() {
return scooter.getName();
}
}
// Main Program
public class Main {
public static void main(String[] args) {
VehicleFactory honda = new HondaFactory();
VehicleClient hondaClient = new VehicleClient(honda, "Regular");
System.out.println("******* Honda **********");
System.out.println(hondaClient.getBikeName());
System.out.println(hondaClient.getScooterName());
hondaClient = new VehicleClient(honda, "Sports");
System.out.println(hondaClient.getBikeName());
System.out.println(hondaClient.getScooterName());
VehicleFactory hero = new HeroFactory();
VehicleClient heroClient = new VehicleClient(hero, "Regular");
System.out.println("******* Hero **********");
System.out.println(heroClient.getBikeName());
System.out.println(heroClient.getScooterName());
heroClient = new VehicleClient(hero, "Sports");
System.out.println(heroClient.getBikeName());
System.out.println(heroClient.getScooterName());
}
}
from abc import ABC, abstractmethod
# Abstract Factory
class VehicleFactory(ABC):
@abstractmethod
def get_bike(self, bike_type: str):
pass
@abstractmethod
def get_scooter(self, scooter_type: str):
pass
# Concrete Factory 1: Honda Factory
class HondaFactory(VehicleFactory):
def get_bike(self, bike_type: str):
if bike_type == "Sports":
return SportsBike()
elif bike_type == "Regular":
return RegularBike()
else:
raise ValueError(f"Bike '{bike_type}' cannot be created")
def get_scooter(self, scooter_type: str):
if scooter_type == "Sports":
return Scooty()
elif scooter_type == "Regular":
return RegularScooter()
else:
raise ValueError(f"Scooter '{scooter_type}' cannot be created")
# Concrete Factory 2: Hero Factory
class HeroFactory(VehicleFactory):
def get_bike(self, bike_type: str):
if bike_type == "Sports":
return SportsBike()
elif bike_type == "Regular":
return RegularBike()
else:
raise ValueError(f"Bike '{bike_type}' cannot be created")
def get_scooter(self, scooter_type: str):
if scooter_type == "Sports":
return Scooty()
elif scooter_type == "Regular":
return RegularScooter()
else:
raise ValueError(f"Scooter '{scooter_type}' cannot be created")
# Abstract Product A: Bike
class Bike(ABC):
@abstractmethod
def get_name(self):
pass
# Abstract Product B: Scooter
class Scooter(ABC):
@abstractmethod
def get_name(self):
pass
# Concrete Product A1: Regular Bike
class RegularBike(Bike):
def get_name(self):
return "Regular Bike - Name"
# Concrete Product A2: Sports Bike
class SportsBike(Bike):
def get_name(self):
return "Sports Bike - Name"
# Concrete Product B1: Regular Scooter
class RegularScooter(Scooter):
def get_name(self):
return "Regular Scooter - Name"
# Concrete Product B2: Scooty
class Scooty(Scooter):
def get_name(self):
return "Scooty - Name"
# Client Class
class VehicleClient:
def __init__(self, factory: VehicleFactory, vehicle_type: str):
self.bike = factory.get_bike(vehicle_type)
self.scooter = factory.get_scooter(vehicle_type)
def get_bike_name(self):
return self.bike.get_name()
def get_scooter_name(self):
return self.scooter.get_name()
# Main Program
if __name__ == "__main__":
honda = HondaFactory()
honda_client = VehicleClient(honda, "Regular")
print("******* Honda **********")
print(honda_client.get_bike_name())
print(honda_client.get_scooter_name())
honda_client = VehicleClient(honda, "Sports")
print(honda_client.get_bike_name())
print(honda_client.get_scooter_name())
hero = HeroFactory()
hero_client = VehicleClient(hero, "Regular")
print("******* Hero **********")
print(hero_client.get_bike_name())
print(hero_client.get_scooter_name())
hero_client = VehicleClient(hero, "Sports")
print(hero_client.get_bike_name())
print(hero_client.get_scooter_name())
interface Bike {
getName(): string;
}
interface Scooter {
getName(): string;
}
interface VehicleFactory {
getBike(bikeType: string): Bike;
getScooter(scooterType: string): Scooter;
}
class HondaFactory implements VehicleFactory {
getBike(bikeType: string): Bike {
if (bikeType === "Sports") {
return new SportsBike();
} else if (bikeType === "Regular") {
return new RegularBike();
} else {
throw new Error(`Bike '${bikeType}' cannot be created`);
}
}
getScooter(scooterType: string): Scooter {
if (scooterType === "Sports") {
return new Scooty();
} else if (scooterType === "Regular") {
return new RegularScooter();
} else {
throw new Error(`Scooter '${scooterType}' cannot be created`);
}
}
}
class HeroFactory implements VehicleFactory {
getBike(bikeType: string): Bike {
if (bikeType === "Sports") {
return new SportsBike();
} else if (bikeType === "Regular") {
return new RegularBike();
} else {
throw new Error(`Bike '${bikeType}' cannot be created`);
}
}
getScooter(scooterType: string): Scooter {
if (scooterType === "Sports") {
return new Scooty();
} else if (scooterType === "Regular") {
return new RegularScooter();
} else {
throw new Error(`Scooter '${scooterType}' cannot be created`);
}
}
}
class RegularBike implements Bike {
getName(): string {
return "Regular Bike - Name";
}
}
class SportsBike implements Bike {
getName(): string {
return "Sports Bike - Name";
}
}
class RegularScooter implements Scooter {
getName(): string {
return "Regular Scooter - Name";
}
}
class Scooty implements Scooter {
getName(): string {
return "Scooty - Name";
}
}
class VehicleClient {
private bike: Bike;
private scooter: Scooter;
constructor(factory: VehicleFactory, type: string) {
this.bike = factory.getBike(type);
this.scooter = factory.getScooter(type);
}
getBikeName(): string {
return this.bike.getName();
}
getScooterName(): string {
return this.scooter.getName();
}
}
// Main Program
const hondaFactory = new HondaFactory();
const hondaClient = new VehicleClient(hondaFactory, "Regular");
console.log("******* Honda **********");
console.log(hondaClient.getBikeName());
console.log(hondaClient.getScooterName());
const hondaSportsClient = new VehicleClient(hondaFactory, "Sports");
console.log(hondaSportsClient.getBikeName());
console.log(hondaSportsClient.getScooterName());
const heroFactory = new HeroFactory();
const heroClient = new VehicleClient(heroFactory, "Regular");
console.log("******* Hero **********");
console.log(heroClient.getBikeName());
console.log(heroClient.getScooterName());
const heroSportsClient = new VehicleClient(heroFactory, "Sports");
console.log(heroSportsClient.getBikeName());
console.log(heroSportsClient.getScooterName());
class RegularBike {
getName() {
return "Regular Bike - Name";
}
}
class SportsBike {
getName() {
return "Sports Bike - Name";
}
}
class RegularScooter {
getName() {
return "Regular Scooter - Name";
}
}
class Scooty {
getName() {
return "Scooty - Name";
}
}
class HondaFactory {
getBike(bikeType) {
if (bikeType === "Sports") {
return new SportsBike();
} else if (bikeType === "Regular") {
return new RegularBike();
} else {
throw new Error(`Bike '${bikeType}' cannot be created`);
}
}
getScooter(scooterType) {
if (scooterType === "Sports") {
return new Scooty();
} else if (scooterType === "Regular") {
return new RegularScooter();
} else {
throw new Error(`Scooter '${scooterType}' cannot be created`);
}
}
}
class HeroFactory {
getBike(bikeType) {
if (bikeType === "Sports") {
return new SportsBike();
} else if (bikeType === "Regular") {
return new RegularBike();
} else {
throw new Error(`Bike '${bikeType}' cannot be created`);
}
}
getScooter(scooterType) {
if (scooterType === "Sports") {
return new Scooty();
} else if (scooterType === "Regular") {
return new RegularScooter();
} else {
throw new Error(`Scooter '${scooterType}' cannot be created`);
}
}
}
class VehicleClient {
constructor(factory, type) {
this.bike = factory.getBike(type);
this.scooter = factory.getScooter(type);
}
getBikeName() {
return this.bike.getName();
}
getScooterName() {
return this.scooter.getName();
}
}
// Main Program
const hondaFactory = new HondaFactory();
let hondaClient = new VehicleClient(hondaFactory, "Regular");
console.log("******* Honda **********");
console.log(hondaClient.getBikeName());
console.log(hondaClient.getScooterName());
hondaClient = new VehicleClient(hondaFactory, "Sports");
console.log(hondaClient.getBikeName());
console.log(hondaClient.getScooterName());
const heroFactory = new HeroFactory();
let heroClient = new VehicleClient(heroFactory, "Regular");
console.log("******* Hero **********");
console.log(heroClient.getBikeName());
console.log(heroClient.getScooterName());
heroClient = new VehicleClient(heroFactory, "Sports");
console.log(heroClient.getBikeName());
console.log(heroClient.getScooterName());
Output
******* Honda **********
Regular Bike- Name
Regular Scooter- Name
Sports Bike- Name
Scooty- Name
******* Hero **********
Regular Bike- Name
Regular Scooter- Name
Sports Bike- Name
Scooty- Name
Explanation
- The code shows the Abstract Factory design pattern by generating different kinds of bikes and scooters with HondaFactory and HeroFactory.
- The VehicleClient class communicates with these factories to obtain and display the names of the manufactured vehicles (bikes and scooters).
- The main application generates client instances for both factories and vehicle kinds and then prints the vehicle names to the console.
Advantages of using Abstract Factory Design Pattern
1. Isolation of concrete classes
- The Abstract Factory design allows you to control the kinds of objects that an application creates.
- Because a factory integrates the responsibility and process of producing product objects, it separates customers from implementation classes.
- Clients manipulate instances using their abstract interfaces.
- Product class names are segregated throughout the concrete factory construction and do not appear in the client code.
2. Exchanging Product Families easily
- The concrete factory class occurs only once in an application where it is instantiated.
- This allows you to easily change the concrete manufacturer that an application uses.
- It may switch between product configurations by merely changing the concrete factory.
- Because an abstract factory produces a complete family of items, the entire product family changes at once.
3. Promoting consistency among products
- When product objects in a family are designed to work together, an application should only use objects from one family at a time.
- AbstractFactory makes this simple to enforce.
Disadvantages of using Abstract Factory Design Pattern
1. Complexity
- Abstract Factory can add complexity to the codebase.
- Multiple factories and abstract product interfaces may be unnecessary for smaller projects.
2. Rigidity with New Product Types
- Introducing new product categories (classes) into the system might be tricky.
- You may need to adjust both the concrete factories and the abstract factory interface, which may have an impact on the current code.
3. Increased Number of Classes
- As you add more abstract factories and product families, the number of classes in your system will rapidly increase.
- This can make the code more difficult to manage and understand, especially for smaller projects.
4. Dependency Inversion Principle Violation
- In some circumstances, the Abstract Factory pattern may violate the Dependency Inversion Principle, particularly if client code relies on actual factory implementations rather than abstract interfaces.
5. Limited Extensibility
- Extending the abstract factory hierarchy or introducing new product families could require adjustments to numerous areas of the code, potentially resulting in cascading changes and making the system less flexible.
6. Not Ideal for Simple Systems
- The Abstract Factory pattern may be overkill for smaller, less complicated systems when the costs of creating abstract factories and products outweigh the pattern's benefits.
Relationship with Other Patterns
1. Mediator Pattern
- It simplifies interactions between objects by centralizing communication, while the Abstract Factory Pattern focuses on object creation for related families.
- The two patterns serve distinct purposes, with the Mediator handling communication and the Abstract Factory handling creation.
2. Adapter Pattern
- It is designed to make incompatible interfaces compatible, while the Abstract Factory Pattern is focused on creating related objects without specifying their concrete classes.
- The Adapter Pattern adapts interfaces, while the Abstract Factory handles object creation.
3. Bridge Pattern
- It separates abstraction from implementation, allowing both to change independently.
- The Abstract Factory Pattern works alongside the Bridge Pattern, which deals with object creation, while the Bridge Pattern focuses on decoupling abstraction and implementation.
4. Decorator Pattern
- It is used to add responsibilities dynamically to objects.
- The Abstract Factory Pattern, on the other hand, focuses on the creation of families of related objects without altering their responsibilities, while the Decorator Pattern adds new functionalities.
Summary
The Abstract Factory Design Pattern allows you to create families of related objects without defining explicit classes, which increases flexibility and consistency. It is useful for systems that require independence from object generation, but it might increase complexity and rigidity when introducing new product types. Examples demonstrate its application in contexts such as automotive production. Also, consider our Software Architecture and Design Certification Trainingfor a better understanding of other design patterns concepts.FAQs
The Abstract Factory design pattern is used in real-world applications to generate families of related objects without specifying their actual classes. This provides product consistency and interchangeability, making it perfect for systems such as UI toolkits that require uniform application of diverse themes or styles. It fosters loose coupling and increases system flexibility.
The Abstract Factory design provides an interface for constructing families of linked or dependent objects without specifying their specific classes. It ensures that items from the same family are employed concurrently, enabling consistency and flexibility in object development. This pattern aids in managing object generation and achieving loose coupling in systems.
The Abstract Factory pattern is designed to assure consistency across related objects built simultaneously while obscuring their particular implementations. It offers a versatile and scalable approach to object development, particularly when working with several product families. This approach separates client code from specialized classes, which improves maintainability.
The Facade pattern simplifies and unifies a subsystem's interfaces, making them easier to use. In contrast, the Abstract Factory approach focuses on constructing families of linked objects without identifying their concrete classes. Facade facilitates interactions with complicated systems, whereas Abstract Factory handles object creation and assures consistency across connected items.
The primary advantage of employing the Factory pattern is that it centralizes object generation, giving you greater control over which classes are instantiated and how. It encourages loose coupling by obscuring concrete class details from client code, improving maintainability and flexibility. This style also makes object creation easier to manage and simplifies complex instantiation logic.