CrackedRuby CrackedRuby

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