Overview
The Abstract Factory pattern addresses the problem of creating groups of related objects that must work together consistently. Rather than instantiating concrete classes directly throughout an application, code depends on abstract interfaces that define what objects should be created. Concrete factory implementations then provide specific versions of these objects that maintain compatibility within their family.
This pattern emerged from the need to build systems that support multiple product variants—different look-and-feels for user interfaces, cross-platform applications, or systems that must work with different data formats or external services. By encapsulating object creation behind factory interfaces, the pattern isolates client code from the details of which concrete classes get instantiated.
The Abstract Factory extends the Factory Method pattern from creating single products to creating entire families of products. Where Factory Method uses inheritance to delegate object creation to subclasses, Abstract Factory uses object composition. A client receives a factory object and calls methods on it to create a family of related products.
Consider a cross-platform GUI framework. The framework needs to create buttons, windows, and scrollbars that match the operating system's native look. Windows requires one set of GUI components, macOS another, and Linux yet another. Each family of components must work together—mixing a Windows button with a macOS window creates visual inconsistencies. The Abstract Factory pattern solves this by defining a factory interface with methods to create each component type, then providing concrete factories for each platform that create coordinated sets of components.
# Abstract factory interface
class GUIFactory
def create_button
raise NotImplementedError
end
def create_window
raise NotImplementedError
end
end
# Client code uses the factory without knowing concrete types
def render_interface(factory)
button = factory.create_button
window = factory.create_window
window.add_component(button)
window.render
end
The pattern separates object creation from object use, making systems more modular and easier to configure. Applications can switch between different product families by swapping factory implementations without modifying client code.
Key Principles
The Abstract Factory pattern rests on several interconnected principles that govern how object creation gets structured and isolated from client code.
Abstract Factory Interface: The core of the pattern is an interface declaring creation methods for each type of product in a family. This interface serves as a contract that all concrete factories must fulfill. Each method returns an abstract product type rather than a concrete class, ensuring clients program to interfaces rather than implementations.
Concrete Factory Implementation: Concrete factories implement the abstract factory interface, providing specific instances that belong to a single product family. Each concrete factory knows which concrete product classes to instantiate and ensures all products it creates work together coherently. The factory encapsulates the knowledge of which classes to instantiate and how to configure them.
Abstract Product Interfaces: Each product type has its own abstract interface defining operations that all variants must support. These interfaces establish contracts that concrete products fulfill, allowing client code to work with any product variant through a common interface. Product interfaces remain independent of factory interfaces—they define what products do, while factories define how products get created.
Concrete Product Implementation: Concrete products implement their respective abstract product interfaces. Each concrete product belongs to a specific product family and maintains consistency with other products from the same family. Concrete products contain the actual implementation details that abstract interfaces hide.
Client Independence: Client code depends only on abstract factory and product interfaces. Clients request products from the factory but never instantiate concrete classes directly. This dependency inversion makes clients independent of which concrete classes the system uses, allowing product families to change without affecting client code.
Family Consistency: All products created by a single concrete factory belong to the same family and work together correctly. This constraint ensures that mixing incompatible products becomes impossible when using the factory properly. The factory serves as the single point of control for maintaining family consistency.
The relationships between these components follow a specific structure. Client code holds a reference to an abstract factory and calls creation methods to obtain abstract products. The concrete factory selected at configuration time determines which concrete product implementations get created. Products interact with each other but never reference the factory that created them.
# Abstract product interfaces
class Button
def render
raise NotImplementedError
end
end
class Window
def render
raise NotImplementedError
end
def add_component(component)
raise NotImplementedError
end
end
# Concrete products for Windows
class WindowsButton < Button
def render
puts "Rendering Windows-style button"
end
end
class WindowsWindow < Window
def initialize
@components = []
end
def render
puts "Rendering Windows-style window"
@components.each(&:render)
end
def add_component(component)
@components << component
end
end
# Concrete factory for Windows
class WindowsFactory < GUIFactory
def create_button
WindowsButton.new
end
def create_window
WindowsWindow.new
end
end
This structure enables the pattern to fulfill its primary purpose: allowing an application to work with multiple families of products while maintaining consistency within each family and independence from concrete implementations. The factory acts as a gatekeeper, ensuring clients receive compatible products without needing to understand compatibility requirements.
Design Considerations
The Abstract Factory pattern introduces complexity through additional layers of abstraction. This complexity provides benefits in specific scenarios but creates unnecessary overhead in others.
When to Apply Abstract Factory: The pattern proves valuable when a system must work with multiple families of related products and ensure products from the same family get used together. Applications requiring cross-platform support, multiple theme variants, or different protocol implementations benefit from this structure. The pattern also helps when the system needs to be configured with one of several product families and that choice should be made at runtime or through configuration rather than being hard-coded.
Product families must have clear relationships and compatibility requirements for the pattern to provide value. If products lack meaningful relationships or can mix freely, the factory adds abstraction without solving a real problem. Similarly, if the application only ever uses one product family, simpler patterns like Factory Method or direct instantiation suffice.
Trade-offs Against Factory Method: Factory Method uses inheritance to delegate object creation to subclasses, while Abstract Factory uses composition and delegation to factory objects. Factory Method works well when creating single products or when product creation integrates tightly with application logic. Abstract Factory handles multiple related products better but requires more upfront design.
Factory Method requires subclassing the creator class for each product variant. This can lead to class explosion when multiple dimensions of variation exist. Abstract Factory avoids this by separating the factory hierarchy from the product hierarchy, allowing independent variation of factories and products.
# Factory Method approach - requires subclassing
class Application
def create_document
raise NotImplementedError
end
def open_document
doc = create_document
doc.open
end
end
class TextApplication < Application
def create_document
TextDocument.new
end
end
# Abstract Factory approach - uses composition
class Application
def initialize(factory)
@factory = factory
end
def open_document
doc = @factory.create_document
doc.open
end
end
factory = TextFactory.new
app = Application.new(factory)
Alternatives to Consider: The Builder pattern creates complex objects step by step and works well when products require elaborate construction. Use Builder when object creation involves many configuration options or multi-step assembly. Abstract Factory focuses on creating families of simple objects rather than configuring single complex objects.
Prototype pattern creates objects by cloning existing instances. This approach works when the system has a small, fixed set of object variations that can serve as templates. Prototype avoids the factory hierarchy but loses the ability to add new product families easily.
Dependency Injection frameworks can replace Abstract Factory in many scenarios. These frameworks manage object creation and wiring based on configuration, eliminating hand-written factory code. DI containers provide more features but add external dependencies and configuration complexity.
Impact on System Architecture: Abstract Factory centralizes product creation in factory classes, making it easy to swap entire product families. This centralization means changing product families requires changing only the factory instance passed to client code. However, adding new product types to the family requires modifying the factory interface and all concrete factories, violating the open-closed principle for this dimension.
The pattern increases the number of classes in a system. Each product family requires a concrete factory class and concrete product classes for each product type. This proliferation of classes can make the codebase harder to navigate but improves modularity and testability.
# Adding a new product type requires changing all factories
class GUIFactory
def create_button; raise NotImplementedError; end
def create_window; raise NotImplementedError; end
def create_checkbox; raise NotImplementedError; end # New product type
end
class WindowsFactory < GUIFactory
# Must implement all three creation methods
def create_checkbox
WindowsCheckbox.new
end
end
class MacFactory < GUIFactory
# Must also implement the new method
def create_checkbox
MacCheckbox.new
end
end
The pattern makes testing easier by allowing test doubles to replace production factories. Tests can inject factories that create mock products, isolating the code under test from real product implementations. This particularly benefits integration testing where controlling product behavior proves difficult.
Ruby Implementation
Ruby's dynamic nature and metaprogramming features enable both conventional implementations of Abstract Factory and uniquely Ruby-flavored approaches. The language's support for modules, blocks, and runtime class creation provides alternatives to traditional class hierarchies.
Class-Based Implementation: The straightforward approach uses Ruby classes and inheritance to implement abstract factories and products. Abstract base classes define interfaces using methods that raise NotImplementedError, while concrete classes provide implementations.
# Abstract product interfaces using inheritance
class Database
def connect
raise NotImplementedError, "#{self.class} must implement #connect"
end
def query(sql)
raise NotImplementedError, "#{self.class} must implement #query"
end
end
class Cache
def get(key)
raise NotImplementedError, "#{self.class} must implement #get"
end
def set(key, value)
raise NotImplementedError, "#{self.class} must implement #set"
end
end
# Concrete implementations for PostgreSQL ecosystem
class PostgresDatabase < Database
def connect
@connection = PG.connect(dbname: 'mydb')
end
def query(sql)
@connection.exec(sql)
end
end
class RedisCache < Cache
def initialize
@client = Redis.new
end
def get(key)
@client.get(key)
end
def set(key, value)
@client.set(key, value)
end
end
# Abstract factory
class DataFactory
def create_database
raise NotImplementedError
end
def create_cache
raise NotImplementedError
end
end
# Concrete factory
class PostgresFactory < DataFactory
def create_database
PostgresDatabase.new
end
def create_cache
RedisCache.new
end
end
Module-Based Interfaces: Ruby modules provide an alternative to class inheritance for defining product interfaces. Modules can specify required methods without forcing inheritance hierarchies, making product classes more flexible.
# Product interfaces as modules
module Database
def connect; raise NotImplementedError; end
def query(sql); raise NotImplementedError; end
end
module Cache
def get(key); raise NotImplementedError; end
def set(key, value); raise NotImplementedError; end
end
# Concrete products include modules
class MongoDatabase
include Database
def connect
@client = Mongo::Client.new(['localhost:27017'])
end
def query(conditions)
@client[:collection].find(conditions)
end
end
class MemcachedCache
include Cache
def initialize
@client = Dalli::Client.new
end
def get(key)
@client.get(key)
end
def set(key, value)
@client.set(key, value)
end
end
Block-Based Configuration: Ruby's blocks enable factories to configure products during creation. This approach eliminates separate initialization steps and ensures products get configured correctly.
class DataFactory
def create_database(&config)
db = PostgresDatabase.new
config.call(db) if block_given?
db
end
def create_cache(&config)
cache = RedisCache.new
config.call(cache) if block_given?
cache
end
end
factory = DataFactory.new
db = factory.create_database do |d|
d.host = 'localhost'
d.port = 5432
d.pool_size = 10
end
Registry-Based Factories: Ruby's dynamic nature allows factories to register product classes at runtime rather than defining them through inheritance. This approach provides more flexibility for plugin systems or applications that discover product types dynamically.
class PluggableFactory
def initialize
@products = {}
end
def register(product_name, product_class)
@products[product_name] = product_class
end
def create(product_name, *args)
product_class = @products[product_name]
raise ArgumentError, "Unknown product: #{product_name}" unless product_class
product_class.new(*args)
end
end
factory = PluggableFactory.new
factory.register(:database, PostgresDatabase)
factory.register(:cache, RedisCache)
db = factory.create(:database)
cache = factory.create(:cache)
Const-Based Product Selection: Using constants and string interpolation allows factories to determine product classes based on naming conventions. This reduces boilerplate when product families follow predictable patterns.
class ConventionFactory
def initialize(family_name)
@family = family_name
end
def create_database
Object.const_get("#{@family}Database").new
end
def create_cache
Object.const_get("#{@family}Cache").new
end
end
# Assumes classes named PostgresDatabase, PostgresCache exist
factory = ConventionFactory.new('Postgres')
db = factory.create_database # Creates PostgresDatabase instance
Singleton Factories: Ruby's singleton pattern integrates naturally with Abstract Factory when only one factory instance should exist. This prevents multiple factories from being created accidentally and provides a global access point.
require 'singleton'
class ProductionFactory
include Singleton
def create_database
PostgresDatabase.new
end
def create_cache
RedisCache.new
end
end
# Access the single instance
factory = ProductionFactory.instance
db = factory.create_database
Ruby's approach to the Abstract Factory pattern balances traditional object-oriented design with dynamic language features. The choice between class hierarchies, modules, blocks, or metaprogramming depends on the specific needs for flexibility, discoverability, and maintainability in each application.
Practical Examples
Real-world applications of Abstract Factory demonstrate how the pattern solves concrete problems in different domains. These examples show complete implementations with realistic complexity.
Cross-Platform Notification System: An application sends notifications through multiple channels—email, SMS, and push notifications—with different implementations for development and production environments. The development environment uses logging and mock services, while production uses real service providers.
# Abstract product interfaces
class EmailSender
def send_email(to, subject, body)
raise NotImplementedError
end
end
class SmsSender
def send_sms(phone, message)
raise NotImplementedError
end
end
class PushNotifier
def send_push(device_token, payload)
raise NotImplementedError
end
end
# Development implementations
class LogEmailSender < EmailSender
def send_email(to, subject, body)
puts "[DEV EMAIL] To: #{to}, Subject: #{subject}"
puts body
end
end
class LogSmsSender < SmsSender
def send_sms(phone, message)
puts "[DEV SMS] To: #{phone}, Message: #{message}"
end
end
class LogPushNotifier < PushNotifier
def send_push(device_token, payload)
puts "[DEV PUSH] Device: #{device_token}, Payload: #{payload.inspect}"
end
end
# Production implementations
class SendgridEmailSender < EmailSender
def initialize
@client = SendGrid::API.new(api_key: ENV['SENDGRID_KEY'])
end
def send_email(to, subject, body)
mail = SendGrid::Mail.new
mail.from = Email.new(email: 'noreply@example.com')
mail.subject = subject
mail.add_content(Content.new(type: 'text/plain', value: body))
personalization = Personalization.new
personalization.add_to(Email.new(email: to))
mail.add_personalization(personalization)
@client.client.mail._('send').post(request_body: mail.to_json)
end
end
class TwilioSmsSender < SmsSender
def initialize
@client = Twilio::REST::Client.new(
ENV['TWILIO_SID'],
ENV['TWILIO_TOKEN']
)
end
def send_sms(phone, message)
@client.messages.create(
from: ENV['TWILIO_PHONE'],
to: phone,
body: message
)
end
end
class FirebasePushNotifier < PushNotifier
def initialize
@client = FCM.new(ENV['FIREBASE_KEY'])
end
def send_push(device_token, payload)
@client.send([device_token], payload)
end
end
# Abstract factory
class NotificationFactory
def create_email_sender
raise NotImplementedError
end
def create_sms_sender
raise NotImplementedError
end
def create_push_notifier
raise NotImplementedError
end
end
# Concrete factories
class DevelopmentNotificationFactory < NotificationFactory
def create_email_sender
LogEmailSender.new
end
def create_sms_sender
LogSmsSender.new
end
def create_push_notifier
LogPushNotifier.new
end
end
class ProductionNotificationFactory < NotificationFactory
def create_email_sender
SendgridEmailSender.new
end
def create_sms_sender
TwilioSmsSender.new
end
def create_push_notifier
FirebasePushNotifier.new
end
end
# Client code
class NotificationService
def initialize(factory)
@email = factory.create_email_sender
@sms = factory.create_sms_sender
@push = factory.create_push_notifier
end
def notify_user(user, message)
@email.send_email(user.email, "Notification", message)
@sms.send_sms(user.phone, message) if user.phone
@push.send_push(user.device_token, {message: message}) if user.device_token
end
end
# Configuration based on environment
factory = ENV['RAILS_ENV'] == 'production' ?
ProductionNotificationFactory.new :
DevelopmentNotificationFactory.new
service = NotificationService.new(factory)
service.notify_user(current_user, "Your order has shipped")
Document Export System: An application generates reports in multiple formats (PDF, Excel, HTML) using different rendering engines. Each format requires coordinated components for layout, styling, and content rendering.
# Abstract products
class DocumentRenderer
def render_header(text); raise NotImplementedError; end
def render_paragraph(text); raise NotImplementedError; end
def render_table(data); raise NotImplementedError; end
def save(filename); raise NotImplementedError; end
end
class StyleSheet
def header_style; raise NotImplementedError; end
def paragraph_style; raise NotImplementedError; end
def table_style; raise NotImplementedError; end
end
# PDF implementation
class PdfRenderer < DocumentRenderer
def initialize(stylesheet)
@pdf = Prawn::Document.new
@stylesheet = stylesheet
end
def render_header(text)
@pdf.text text, @stylesheet.header_style
end
def render_paragraph(text)
@pdf.text text, @stylesheet.paragraph_style
end
def render_table(data)
@pdf.table data, @stylesheet.table_style
end
def save(filename)
@pdf.render_file(filename)
end
end
class PdfStyleSheet < StyleSheet
def header_style
{ size: 24, style: :bold }
end
def paragraph_style
{ size: 12 }
end
def table_style
{ cell_style: { borders: [:bottom], border_width: 1 } }
end
end
# Excel implementation
class ExcelRenderer < DocumentRenderer
def initialize(stylesheet)
@workbook = WriteXLSX.new
@worksheet = @workbook.add_worksheet
@stylesheet = stylesheet
@row = 0
end
def render_header(text)
format = @workbook.add_format(@stylesheet.header_style)
@worksheet.write(@row, 0, text, format)
@row += 1
end
def render_paragraph(text)
format = @workbook.add_format(@stylesheet.paragraph_style)
@worksheet.write(@row, 0, text, format)
@row += 1
end
def render_table(data)
format = @workbook.add_format(@stylesheet.table_style)
data.each do |row|
row.each_with_index do |cell, col|
@worksheet.write(@row, col, cell, format)
end
@row += 1
end
end
def save(filename)
@workbook.close
end
end
class ExcelStyleSheet < StyleSheet
def header_style
{ bold: 1, size: 16 }
end
def paragraph_style
{ size: 11 }
end
def table_style
{ border: 1 }
end
end
# Factory
class DocumentFactory
def create_renderer
raise NotImplementedError
end
def create_stylesheet
raise NotImplementedError
end
end
class PdfDocumentFactory < DocumentFactory
def create_renderer
PdfRenderer.new(create_stylesheet)
end
def create_stylesheet
PdfStyleSheet.new
end
end
class ExcelDocumentFactory < DocumentFactory
def create_renderer
ExcelRenderer.new(create_stylesheet)
end
def create_stylesheet
ExcelStyleSheet.new
end
end
# Client code
class ReportGenerator
def initialize(factory)
@factory = factory
end
def generate(data, filename)
renderer = @factory.create_renderer
renderer.render_header("Sales Report")
renderer.render_paragraph("Generated on #{Time.now}")
renderer.render_table(data)
renderer.save(filename)
end
end
# Usage
pdf_factory = PdfDocumentFactory.new
generator = ReportGenerator.new(pdf_factory)
generator.generate(sales_data, "report.pdf")
excel_factory = ExcelDocumentFactory.new
generator = ReportGenerator.new(excel_factory)
generator.generate(sales_data, "report.xlsx")
Multi-Region Cloud Infrastructure: A deployment system provisions resources across different cloud providers (AWS, Azure, GCP) with each provider requiring specific APIs for compute, storage, and networking resources.
# Abstract products
class ComputeInstance
def launch(config); raise NotImplementedError; end
def terminate(id); raise NotImplementedError; end
def get_status(id); raise NotImplementedError; end
end
class StorageBucket
def create(name); raise NotImplementedError; end
def upload(name, file); raise NotImplementedError; end
def delete(name); raise NotImplementedError; end
end
class NetworkBalancer
def create(name, targets); raise NotImplementedError; end
def add_target(name, target); raise NotImplementedError; end
def remove_target(name, target); raise NotImplementedError; end
end
# AWS implementations
class EC2Instance < ComputeInstance
def initialize
@client = Aws::EC2::Client.new
end
def launch(config)
response = @client.run_instances(
image_id: config[:image_id],
instance_type: config[:instance_type],
min_count: 1,
max_count: 1
)
response.instances.first.instance_id
end
def terminate(id)
@client.terminate_instances(instance_ids: [id])
end
def get_status(id)
response = @client.describe_instances(instance_ids: [id])
response.reservations.first.instances.first.state.name
end
end
class S3Bucket < StorageBucket
def initialize
@client = Aws::S3::Client.new
end
def create(name)
@client.create_bucket(bucket: name)
end
def upload(name, file)
@client.put_object(bucket: name, key: File.basename(file), body: File.read(file))
end
def delete(name)
@client.delete_bucket(bucket: name)
end
end
class ELBBalancer < NetworkBalancer
def initialize
@client = Aws::ElasticLoadBalancingV2::Client.new
end
def create(name, targets)
response = @client.create_load_balancer(name: name)
lb_arn = response.load_balancers.first.load_balancer_arn
targets.each { |t| add_target(lb_arn, t) }
lb_arn
end
def add_target(name, target)
@client.register_targets(
target_group_arn: name,
targets: [{ id: target }]
)
end
def remove_target(name, target)
@client.deregister_targets(
target_group_arn: name,
targets: [{ id: target }]
)
end
end
# Factory
class CloudFactory
def create_compute
raise NotImplementedError
end
def create_storage
raise NotImplementedError
end
def create_balancer
raise NotImplementedError
end
end
class AwsFactory < CloudFactory
def create_compute
EC2Instance.new
end
def create_storage
S3Bucket.new
end
def create_balancer
ELBBalancer.new
end
end
# Deployment orchestrator
class InfrastructureDeployer
def initialize(factory)
@compute = factory.create_compute
@storage = factory.create_storage
@balancer = factory.create_balancer
end
def deploy_application(config)
# Create storage for application assets
@storage.create(config[:bucket_name])
@storage.upload(config[:bucket_name], config[:app_package])
# Launch compute instances
instances = config[:instance_count].times.map do
@compute.launch(
image_id: config[:image_id],
instance_type: config[:instance_type]
)
end
# Wait for instances to be running
instances.each do |id|
sleep 5 until @compute.get_status(id) == 'running'
end
# Create load balancer
@balancer.create(config[:app_name], instances)
instances
end
end
# Usage
aws_factory = AwsFactory.new
deployer = InfrastructureDeployer.new(aws_factory)
instance_ids = deployer.deploy_application(
bucket_name: 'myapp-assets',
app_package: 'dist/app.zip',
image_id: 'ami-12345',
instance_type: 't3.medium',
instance_count: 3,
app_name: 'myapp-lb'
)
These examples demonstrate how Abstract Factory maintains consistency across related components while allowing the entire family to change through factory selection. Each example shows realistic complexity with multiple coordinated products and practical configuration requirements.
Common Patterns
Several variations and related patterns complement the basic Abstract Factory structure, addressing different design needs and constraints.
Factory Registration Pattern: Instead of creating new factory subclasses for each product family, factories can register product classes dynamically. This approach reduces the number of factory classes when product families share similar creation logic.
class ConfigurableFactory
def initialize
@product_classes = {}
end
def register_product(product_type, product_class)
@product_classes[product_type] = product_class
end
def create_product(product_type, *args)
product_class = @product_classes[product_type]
raise ArgumentError, "No product registered for #{product_type}" unless product_class
product_class.new(*args)
end
end
# Configure for development
dev_factory = ConfigurableFactory.new
dev_factory.register_product(:database, MockDatabase)
dev_factory.register_product(:cache, MemoryCache)
# Configure for production
prod_factory = ConfigurableFactory.new
prod_factory.register_product(:database, PostgresDatabase)
prod_factory.register_product(:cache, RedisCache)
Parameterized Factory Method: Factory methods accept parameters that influence which concrete class gets instantiated. This reduces the number of factory methods when products have simple variations.
class DocumentFactory
def create_document(format, options = {})
case format
when :pdf
PdfDocument.new(options[:page_size] || 'A4')
when :docx
DocxDocument.new(options[:template] || 'default')
when :html
HtmlDocument.new(options[:css_framework] || 'bootstrap')
else
raise ArgumentError, "Unknown format: #{format}"
end
end
end
factory = DocumentFactory.new
pdf = factory.create_document(:pdf, page_size: 'Letter')
html = factory.create_document(:html, css_framework: 'tailwind')
Prototype-Based Factory: Factories clone prototype objects instead of calling constructors. This works well when products have complex initialization and only minor variations between instances.
class PrototypeFactory
def initialize
@prototypes = {}
end
def register_prototype(key, prototype)
@prototypes[key] = prototype
end
def create(key)
prototype = @prototypes[key]
raise ArgumentError, "No prototype for #{key}" unless prototype
prototype.clone
end
end
# Set up prototypes
factory = PrototypeFactory.new
factory.register_prototype(:standard_report, Report.new(
font: 'Arial',
page_size: 'A4',
margins: [1, 1, 1, 1]
))
factory.register_prototype(:compact_report, Report.new(
font: 'Helvetica',
page_size: 'A5',
margins: [0.5, 0.5, 0.5, 0.5]
))
# Create instances from prototypes
report1 = factory.create(:standard_report)
report2 = factory.create(:compact_report)
Factory Composition: Complex factory hierarchies can compose simpler factories to build product families incrementally. This reduces duplication when product families share some but not all components.
class BaseDataFactory
def create_database
PostgresDatabase.new
end
end
class CachedDataFactory < BaseDataFactory
def create_cache
RedisCache.new
end
end
class FullStackDataFactory < CachedDataFactory
def create_message_queue
RabbitMQQueue.new
end
def create_search_engine
ElasticsearchEngine.new
end
end
# Use the appropriate level of factory
basic_factory = BaseDataFactory.new
db = basic_factory.create_database
full_factory = FullStackDataFactory.new
db = full_factory.create_database
cache = full_factory.create_cache
queue = full_factory.create_message_queue
Dependency Injection with Factories: Rather than clients holding factory references, dependency injection containers can use factories internally to create and wire objects. This combines the benefits of both patterns.
class Container
def initialize
@factories = {}
@singletons = {}
end
def register_factory(key, factory_class)
@factories[key] = factory_class
end
def resolve(key)
return @singletons[key] if @singletons.key?(key)
factory_class = @factories[key]
raise ArgumentError, "Nothing registered for #{key}" unless factory_class
instance = factory_class.create
@singletons[key] = instance if factory_class.singleton?
instance
end
end
# Register factories
container = Container.new
container.register_factory(:database, DatabaseFactory)
container.register_factory(:cache, CacheFactory)
# Resolve dependencies
class UserRepository
def initialize(database, cache)
@database = database
@cache = cache
end
end
db = container.resolve(:database)
cache = container.resolve(:cache)
repo = UserRepository.new(db, cache)
Abstract Factory with Builder: Combine Abstract Factory with Builder pattern when products require multi-step construction. The factory creates builders configured for the appropriate product family.
class DocumentBuilder
def set_title(title); raise NotImplementedError; end
def add_section(heading, content); raise NotImplementedError; end
def add_image(path); raise NotImplementedError; end
def build; raise NotImplementedError; end
end
class PdfBuilder < DocumentBuilder
def initialize
@document = PdfDocument.new
end
def set_title(title)
@document.title = title
self
end
def add_section(heading, content)
@document.add_heading(heading)
@document.add_paragraph(content)
self
end
def add_image(path)
@document.add_image(path)
self
end
def build
@document
end
end
class DocumentFactory
def create_builder
raise NotImplementedError
end
end
class PdfDocumentFactory < DocumentFactory
def create_builder
PdfBuilder.new
end
end
# Use factory to get builder
factory = PdfDocumentFactory.new
builder = factory.create_builder
document = builder
.set_title("Annual Report")
.add_section("Summary", "Executive summary text...")
.add_image("charts/revenue.png")
.build
These patterns extend Abstract Factory's applicability to different scenarios while maintaining the core principle of creating families of related objects through abstract interfaces. The choice between patterns depends on whether the emphasis falls on runtime flexibility, configuration complexity, or integration with other design patterns.
Common Pitfalls
Developers frequently encounter specific problems when implementing or using the Abstract Factory pattern. Understanding these pitfalls helps avoid design mistakes and implementation bugs.
Incomplete Product Families: Adding new product types to the abstract factory interface forces changes to all concrete factories. If product families grow frequently, the pattern creates significant maintenance burden. Each new product type requires implementing a creation method in every concrete factory, even if some factories provide stub implementations.
# Original factory interface
class GUIFactory
def create_button; raise NotImplementedError; end
def create_window; raise NotImplementedError; end
end
# Adding new product type breaks all existing factories
class GUIFactory
def create_button; raise NotImplementedError; end
def create_window; raise NotImplementedError; end
def create_menu; raise NotImplementedError; end # New requirement
def create_toolbar; raise NotImplementedError; end # Another new requirement
end
# All concrete factories must now implement these methods
class WindowsFactory < GUIFactory
def create_button; WindowsButton.new; end
def create_window; WindowsWindow.new; end
def create_menu; WindowsMenu.new; end # Must implement
def create_toolbar; WindowsToolbar.new; end # Must implement
end
This violates the open-closed principle for the product dimension. Consider using more flexible patterns like Plugin Registry or separating concerns into multiple smaller factory interfaces when product types change frequently.
Inconsistent Product Families: Concrete factories must ensure all products they create work together correctly. Nothing in the type system enforces this constraint. A factory could accidentally mix incompatible products, and the error only appears at runtime when products interact.
class MixedFactory < GUIFactory
def create_button
WindowsButton.new # Windows product
end
def create_window
MacWindow.new # Mac product - incompatible!
end
end
# Runtime error when products interact
factory = MixedFactory.new
button = factory.create_button
window = factory.create_window
window.add_component(button) # May crash or behave incorrectly
Test factories thoroughly to verify product consistency. Consider builder methods that create and validate entire product families atomically rather than creating products individually.
Unnecessary Abstraction: Applying Abstract Factory when the application only ever uses one product family adds complexity without benefit. The pattern requires abstract interfaces for all products and at least two concrete factories to provide value. With a single concrete factory, direct instantiation proves simpler.
# Unnecessary abstraction for single implementation
class DatabaseFactory
def create_connection
raise NotImplementedError
end
end
class PostgresFactory < DatabaseFactory
def create_connection
PG::Connection.new
end
end
# Simpler without pattern
connection = PG::Connection.new
Only introduce Abstract Factory when multiple product families exist or when adding families becomes likely. Premature abstraction makes code harder to understand and modify.
Factory Method Explosion: Factories with many product types lead to interfaces with numerous creation methods. This makes factories difficult to implement and understand. Each factory must implement all methods, even for products it never creates.
class ComputeFactory
def create_instance; raise NotImplementedError; end
def create_volume; raise NotImplementedError; end
def create_snapshot; raise NotImplementedError; end
def create_network; raise NotImplementedError; end
def create_subnet; raise NotImplementedError; end
def create_security_group; raise NotImplementedError; end
def create_load_balancer; raise NotImplementedError; end
def create_auto_scaling_group; raise NotImplementedError; end
# Many more methods...
end
Split large factories into smaller, focused interfaces. Group related products into separate factory interfaces that clients can compose as needed.
Wrong Abstraction Level: Product interfaces that expose implementation details defeat the purpose of abstraction. If all products share an interface but each requires type-checking before use, the abstraction fails.
class Database
def execute(query)
raise NotImplementedError
end
# Wrong: Exposing SQL-specific details in abstract interface
def get_connection_string
raise NotImplementedError
end
def set_transaction_isolation_level(level)
raise NotImplementedError
end
end
# Client forced to know concrete types
def optimize_query(database, query)
if database.is_a?(PostgresDatabase)
database.set_transaction_isolation_level('SERIALIZABLE')
elsif database.is_a?(MongoDatabase)
# Different approach for MongoDB
end
database.execute(query)
end
Design product interfaces around what clients need, not what products can do. Keep interfaces minimal and avoid leaking implementation details.
Circular Dependencies: Factories that depend on other factories or products that reference factories create circular dependencies. This makes initialization complex and testing difficult.
class ComponentFactory
def initialize(service_factory)
@service_factory = service_factory
end
def create_component
service = @service_factory.create_service
Component.new(service)
end
end
class ServiceFactory
def initialize(component_factory)
@component_factory = component_factory
end
def create_service
component = @component_factory.create_component
Service.new(component)
end
end
# Cannot initialize either factory!
comp_factory = ComponentFactory.new(serv_factory) # serv_factory not defined yet
serv_factory = ServiceFactory.new(comp_factory)
Break circular dependencies by injecting products instead of factories, using lazy initialization, or restructuring dependencies to flow in one direction.
Testing Difficulties with Shared State: When products maintain shared state or depend on global configuration, testing becomes problematic. Mock factories cannot create independent product instances.
class RedisCache
def initialize
@client = Redis.new # Uses global Redis configuration
end
def get(key)
@client.get(key)
end
end
# Hard to test - all instances share same Redis server
def test_cache_operations
factory = ProductionFactory.new
cache1 = factory.create_cache
cache2 = factory.create_cache
cache1.set('key', 'value1')
cache2.get('key') # Returns 'value1' - unexpected interaction!
end
Design products to accept configuration through constructors. Factories should create isolated instances that do not share state unless explicitly intended.
Forgotten Product Interface Updates: When product interfaces change, all concrete implementations must update. Missing updates cause runtime errors that type systems do not catch.
class Database
def query(sql); raise NotImplementedError; end
def close; raise NotImplementedError; end
def begin_transaction; raise NotImplementedError; end # New method added
end
class PostgresDatabase < Database
def query(sql)
@conn.exec(sql)
end
def close
@conn.close
end
# Forgot to implement begin_transaction!
end
# Runtime error
db = factory.create_database
db.begin_transaction # Raises NotImplementedError
Use automated testing to verify all concrete products implement required interface methods. Consider static analysis tools or type checking to catch missing implementations at development time.
Reference
Pattern Components
| Component | Purpose | Implementation |
|---|---|---|
| Abstract Factory | Declares creation methods for each product type | Interface or abstract class with creation methods returning abstract products |
| Concrete Factory | Implements creation methods to produce specific product variants | Class that implements abstract factory and returns concrete product instances |
| Abstract Product | Defines interface for a product type | Interface or abstract class declaring operations all variants must support |
| Concrete Product | Implements a specific product variant | Class implementing abstract product interface for one product family |
| Client | Uses factories and products through abstract interfaces | Code that depends only on abstract factory and product interfaces |
Factory Method Structure
| Element | Description | Example |
|---|---|---|
| Return Type | Abstract product interface | Database, Cache, MessageQueue |
| Method Name | Describes product being created | create_database, create_cache |
| Parameters | Optional configuration for product creation | Options hash, configuration block |
| Responsibility | Instantiate and return concrete product | Returns PostgresDatabase.new, RedisCache.new |
Product Interface Design
| Characteristic | Guideline | Rationale |
|---|---|---|
| Minimal | Include only essential operations | Reduces coupling, easier to implement |
| Stable | Changes require updating all implementations | High stability reduces maintenance |
| Abstract | No implementation details in interface | Enables true substitution |
| Cohesive | Related operations only | Clear purpose, focused responsibility |
When to Use Abstract Factory
| Scenario | Use Abstract Factory | Alternative |
|---|---|---|
| Multiple product families that must work together | Yes | Direct instantiation |
| Product families determined at runtime | Yes | Configuration + Factory Method |
| Need to enforce family consistency | Yes | Builder pattern |
| Single product family only | No | Direct instantiation |
| Products mix freely without constraints | No | Simple Factory |
| Simple object creation | No | Constructor |
Factory Selection Patterns
| Approach | Implementation | Trade-offs |
|---|---|---|
| Configuration File | Read factory class name from config, instantiate via reflection | Flexible but requires configuration management |
| Environment Variable | Select factory based on environment variable | Simple but limited options |
| Conditional Logic | Use case/if statements to select factory | Explicit but couples selection logic |
| Dependency Injection | Container provides factory instance | Flexible but adds container dependency |
| Service Locator | Registry maps keys to factory instances | Global access but hidden dependencies |
Common Ruby Implementation Approaches
| Approach | Advantages | Disadvantages |
|---|---|---|
| Class Inheritance | Clear structure, IDE support | Rigid hierarchy |
| Module Inclusion | Flexible, multiple inheritance | Less explicit |
| Duck Typing | Minimal boilerplate | No interface enforcement |
| Registry | Runtime flexibility | Dynamic, less discoverable |
| Metaprogramming | Reduces code duplication | Can be harder to debug |
Design Trade-offs
| Aspect | Abstract Factory | Factory Method | Direct Instantiation |
|---|---|---|---|
| Complexity | High - multiple abstractions | Medium - subclass per variant | Low - straightforward |
| Flexibility | Very flexible - swap families | Flexible - override method | Inflexible - hard-coded |
| Family Consistency | Enforced by factory | Not enforced | Not enforced |
| Adding Product Types | Requires interface changes | Requires new methods | Requires code changes |
| Adding Families | Add new factory class | Add new creator subclass | Add new conditional branch |
Anti-patterns to Avoid
| Anti-pattern | Problem | Solution |
|---|---|---|
| God Factory | Factory creates unrelated products | Split into focused factories |
| Leaky Abstraction | Product interfaces expose implementation | Design interface for client needs |
| Premature Abstraction | Pattern applied with single family | Use direct instantiation until needed |
| Shallow Hierarchy | Concrete products barely differ | Question need for abstraction |
| Mixed Responsibilities | Factory handles creation and business logic | Separate concerns |
Testing Strategies
| Strategy | Purpose | Implementation |
|---|---|---|
| Mock Factory | Isolate client code from real products | Create factory returning test doubles |
| Stub Products | Control product behavior in tests | Implement minimal product interface |
| Factory Verification | Ensure factory creates correct types | Assert instance types |
| Integration Tests | Verify product compatibility | Test products created by same factory work together |
| Factory Registration | Test dynamic factory selection | Verify correct factory selected for scenario |