CrackedRuby CrackedRuby

Interfaces and Abstract Classes

Overview

Interfaces and abstract classes define contracts and shared implementations that enforce consistent behavior across different object types. An interface specifies a set of methods that implementing classes must provide, without dictating how those methods work internally. An abstract class provides partial implementation alongside abstract methods that subclasses must complete.

These constructs solve the problem of ensuring multiple unrelated classes share a common set of operations while allowing each to implement those operations differently. A payment processing system might define an interface requiring process_payment, refund, and verify_transaction methods. Credit card processors, bank transfers, and digital wallets all implement these methods according to their specific requirements while presenting a uniform interface to the rest of the system.

The distinction between interfaces and abstract classes centers on implementation. Interfaces contain no implementation code—only method signatures. Abstract classes contain both concrete methods with full implementations and abstract method declarations that subclasses must implement. This difference affects their use: interfaces define what an object can do, while abstract classes provide shared code and define what subclasses must complete.

Programming languages approach these concepts differently. Java and C# include dedicated interface and abstract class keywords. Ruby lacks a formal interface keyword but achieves similar goals through modules and duck typing. Python uses abstract base classes through the abc module. The underlying goal remains consistent: establish contracts that multiple classes fulfill while maintaining flexibility in implementation.

# Interface-like behavior through module
module Drawable
  def draw
    raise NotImplementedError, "#{self.class} must implement draw method"
  end
  
  def erase
    raise NotImplementedError, "#{self.class} must implement erase method"
  end
end

class Circle
  include Drawable
  
  def draw
    "Drawing a circle"
  end
  
  def erase
    "Erasing the circle"
  end
end

class Square
  include Drawable
  
  def draw
    "Drawing a square"
  end
  
  def erase
    "Erasing the square"
  end
end

shapes = [Circle.new, Square.new]
shapes.each { |shape| puts shape.draw }
# Drawing a circle
# Drawing a square

Key Principles

Contract Definition: Interfaces establish method contracts that implementing classes must fulfill. The contract specifies method names, parameters, and return types without implementation details. A Serializable interface might require serialize and deserialize methods, but each class determines how to convert its data to and from serialized form. The contract guarantees that any object implementing the interface responds to these methods, enabling polymorphic treatment.

Polymorphic Substitution: Objects implementing the same interface or extending the same abstract class substitute for each other in code that depends on the contract. A function accepting a Comparable parameter works with any object implementing comparison methods, regardless of the object's actual class. This substitutability enables flexible code that operates on abstractions rather than concrete types.

Multiple Inheritance of Behavior: Interfaces allow classes to inherit multiple sets of behavior contracts without the diamond problem that plagues multiple implementation inheritance. A class can implement Serializable, Comparable, and Cloneable interfaces simultaneously, gaining all method contracts without conflicting implementations. This compositional approach combines capabilities without inheritance hierarchy complexity.

Template Method Pattern: Abstract classes implement the template method pattern by defining algorithm structure while delegating specific steps to subclasses. The abstract class contains the overall algorithm with calls to abstract methods. Subclasses fill in the abstract methods, customizing the algorithm without changing its structure. This pattern appears in frameworks where the framework controls the flow but applications provide specific behavior.

Partial Implementation: Abstract classes provide concrete methods containing shared logic alongside abstract methods requiring implementation. A DatabaseConnection abstract class might implement connection pooling and retry logic as concrete methods while leaving query execution as an abstract method. This approach reduces code duplication by centralizing common behavior in the base class.

Type Hierarchy and Relationships: Abstract classes establish "is-a" relationships in the type hierarchy. A Bird abstract class with Sparrow and Eagle subclasses expresses that sparrows and eagles are birds. Interfaces establish "can-do" relationships through capability. A Flyable interface implemented by Bird, Airplane, and Insect expresses flying capability without implying these classes share ancestry.

Invariant Enforcement: Abstract classes enforce invariants by implementing methods that depend on abstract method results. The abstract class guarantees certain properties hold across all subclasses by controlling how abstract methods combine. A Vehicle abstract class might implement validate_safe_speed that uses the abstract max_speed method, ensuring safety checks apply uniformly across all vehicle types.

# Abstract class demonstrating key principles
class DataStore
  def initialize
    @connection = establish_connection
  end
  
  # Template method - defines algorithm structure
  def save(record)
    validate_record(record)
    data = serialize(record)
    result = write_data(data)
    log_save(record, result)
    result
  end
  
  # Concrete method - shared implementation
  def validate_record(record)
    raise ArgumentError, "Record cannot be nil" if record.nil?
    raise ArgumentError, "Record must respond to :id" unless record.respond_to?(:id)
  end
  
  # Concrete method - common logging
  def log_save(record, result)
    puts "Saved record #{record.id} with result: #{result}"
  end
  
  # Abstract methods - subclasses must implement
  def establish_connection
    raise NotImplementedError, "#{self.class} must implement establish_connection"
  end
  
  def serialize(record)
    raise NotImplementedError, "#{self.class} must implement serialize"
  end
  
  def write_data(data)
    raise NotImplementedError, "#{self.class} must implement write_data"
  end
end

class FileStore < DataStore
  def establish_connection
    File.open('data.txt', 'a')
  end
  
  def serialize(record)
    "#{record.id},#{record.name}\n"
  end
  
  def write_data(data)
    @connection.write(data)
    @connection.flush
    true
  end
end

Liskov Substitution Principle: Interfaces and abstract classes support the Liskov Substitution Principle by ensuring subclasses extend base class behavior without breaking existing code. If code works with the base type, it must work with any derived type. This principle prevents subclasses from weakening preconditions, strengthening postconditions, or throwing exceptions the base class doesn't specify.

Open/Closed Principle: These constructs enable the Open/Closed Principle—open for extension, closed for modification. New classes implement existing interfaces or extend abstract classes to add functionality without modifying existing code. A payment system adds new payment methods by implementing the PaymentProcessor interface without changing the core payment processing logic.

Ruby Implementation

Ruby lacks dedicated interface syntax but implements interface-like behavior through modules, duck typing, and convention. The language philosophy emphasizes "duck typing"—if an object responds to the required methods, its class doesn't matter. Ruby's approach differs from static languages but achieves similar goals through different mechanisms.

Modules as Interfaces: Ruby modules serve as interface definitions by declaring methods that including classes must implement. The module defines method signatures, often raising NotImplementedError as method bodies. Classes include the module and provide implementations. This pattern documents the interface contract while allowing Ruby's dynamic nature.

module PaymentProcessor
  def process_payment(amount, details)
    raise NotImplementedError, "#{self.class} must implement process_payment"
  end
  
  def refund_payment(transaction_id, amount)
    raise NotImplementedError, "#{self.class} must implement refund_payment"
  end
  
  def verify_transaction(transaction_id)
    raise NotImplementedError, "#{self.class} must implement verify_transaction"
  end
end

class CreditCardProcessor
  include PaymentProcessor
  
  def process_payment(amount, details)
    card_number = details[:card_number]
    # Process credit card payment
    { transaction_id: generate_id, status: 'completed', amount: amount }
  end
  
  def refund_payment(transaction_id, amount)
    # Process refund
    { refund_id: generate_id, original_transaction: transaction_id, amount: amount }
  end
  
  def verify_transaction(transaction_id)
    # Verify transaction exists and is valid
    { valid: true, transaction_id: transaction_id }
  end
  
  private
  
  def generate_id
    SecureRandom.uuid
  end
end

class BankTransferProcessor
  include PaymentProcessor
  
  def process_payment(amount, details)
    account_number = details[:account_number]
    routing_number = details[:routing_number]
    # Process bank transfer
    { transaction_id: generate_id, status: 'pending', amount: amount }
  end
  
  def refund_payment(transaction_id, amount)
    # Initiate refund transfer
    { refund_id: generate_id, original_transaction: transaction_id, amount: amount }
  end
  
  def verify_transaction(transaction_id)
    # Check transaction status
    { valid: true, transaction_id: transaction_id, status: 'pending' }
  end
  
  private
  
  def generate_id
    "BANK-#{Time.now.to_i}-#{rand(1000)}"
  end
end

# Polymorphic usage
def process_order_payment(processor, amount, details)
  result = processor.process_payment(amount, details)
  verification = processor.verify_transaction(result[:transaction_id])
  
  if verification[:valid]
    "Payment processed: #{result[:transaction_id]}"
  else
    processor.refund_payment(result[:transaction_id], amount)
    "Payment failed and refunded"
  end
end

credit_processor = CreditCardProcessor.new
bank_processor = BankTransferProcessor.new

puts process_order_payment(credit_processor, 100.00, { card_number: '4111111111111111' })
puts process_order_payment(bank_processor, 100.00, { account_number: '123456', routing_number: '987654' })

Abstract Base Classes: Ruby creates abstract classes through regular class syntax combined with methods that raise NotImplementedError. The base class provides concrete implementations for shared behavior and raises exceptions for methods subclasses must override. This pattern requires discipline—Ruby's runtime doesn't prevent instantiation of abstract classes or enforce method implementation.

class Report
  def initialize(data)
    @data = data
    @generated_at = Time.now
  end
  
  # Template method
  def generate
    validate_data
    formatted_data = format_data(@data)
    header = generate_header
    body = generate_body(formatted_data)
    footer = generate_footer
    
    assemble_report(header, body, footer)
  end
  
  # Concrete methods - shared behavior
  def validate_data
    raise ArgumentError, "Data cannot be nil" if @data.nil?
    raise ArgumentError, "Data must be enumerable" unless @data.respond_to?(:each)
  end
  
  def generate_header
    "Report Generated: #{@generated_at.strftime('%Y-%m-%d %H:%M:%S')}\n#{'=' * 50}\n"
  end
  
  def generate_footer
    "\n#{'=' * 50}\nEnd of Report"
  end
  
  def assemble_report(header, body, footer)
    header + body + footer
  end
  
  # Abstract methods - must be implemented
  def format_data(data)
    raise NotImplementedError, "#{self.class} must implement format_data"
  end
  
  def generate_body(formatted_data)
    raise NotImplementedError, "#{self.class} must implement generate_body"
  end
end

class CSVReport < Report
  def format_data(data)
    data.map { |row| row.is_a?(Hash) ? row : { value: row } }
  end
  
  def generate_body(formatted_data)
    return "No data available\n" if formatted_data.empty?
    
    keys = formatted_data.first.keys
    header_row = keys.join(',')
    data_rows = formatted_data.map { |row| keys.map { |k| row[k] }.join(',') }
    
    "#{header_row}\n#{data_rows.join("\n")}\n"
  end
end

class TableReport < Report
  def format_data(data)
    data.map { |row| row.is_a?(Hash) ? row : { value: row } }
  end
  
  def generate_body(formatted_data)
    return "No data available\n" if formatted_data.empty?
    
    keys = formatted_data.first.keys
    max_widths = keys.map { |k| [k.to_s.length, formatted_data.map { |r| r[k].to_s.length }.max].max }
    
    header = keys.map.with_index { |k, i| k.to_s.ljust(max_widths[i]) }.join(' | ')
    separator = max_widths.map { |w| '-' * w }.join('-+-')
    rows = formatted_data.map do |row|
      keys.map.with_index { |k, i| row[k].to_s.ljust(max_widths[i]) }.join(' | ')
    end
    
    "#{header}\n#{separator}\n#{rows.join("\n")}\n"
  end
end

data = [
  { name: 'Alice', age: 30, city: 'NYC' },
  { name: 'Bob', age: 25, city: 'LA' },
  { name: 'Charlie', age: 35, city: 'Chicago' }
]

csv_report = CSVReport.new(data)
puts csv_report.generate

table_report = TableReport.new(data)
puts table_report.generate

Duck Typing and Implicit Interfaces: Ruby's duck typing philosophy means interfaces exist implicitly through method requirements. Code expects objects to respond to certain methods without formal interface declarations. The respond_to? method checks method availability at runtime. This approach provides flexibility but sacrifices compile-time safety.

# Implicit interface through duck typing
class DocumentProcessor
  def process(document)
    # Expects document to respond to: read, parse, transform
    content = document.read
    parsed = document.parse(content)
    transformed = document.transform(parsed)
    document.save(transformed)
  end
end

class PDFDocument
  def initialize(filepath)
    @filepath = filepath
  end
  
  def read
    "Reading PDF from #{@filepath}"
  end
  
  def parse(content)
    "Parsing PDF content"
  end
  
  def transform(parsed)
    "Transforming PDF: #{parsed.upcase}"
  end
  
  def save(transformed)
    "Saving transformed PDF: #{transformed}"
  end
end

class XMLDocument
  def initialize(filepath)
    @filepath = filepath
  end
  
  def read
    "Reading XML from #{@filepath}"
  end
  
  def parse(content)
    "Parsing XML content"
  end
  
  def transform(parsed)
    "Transforming XML: #{parsed.downcase}"
  end
  
  def save(transformed)
    "Saving transformed XML: #{transformed}"
  end
end

processor = DocumentProcessor.new
pdf = PDFDocument.new('document.pdf')
xml = XMLDocument.new('data.xml')

processor.process(pdf)
processor.process(xml)

Module Composition: Ruby modules compose multiple interface-like contracts through multiple inclusion. A class includes several modules, gaining all their method contracts. This approach provides interface-like multiple inheritance without implementation conflicts.

module Searchable
  def search(query)
    raise NotImplementedError
  end
  
  def filter(criteria)
    raise NotImplementedError
  end
end

module Sortable
  def sort_by_field(field)
    raise NotImplementedError
  end
  
  def reverse_sort
    raise NotImplementedError
  end
end

module Exportable
  def export_to_csv
    raise NotImplementedError
  end
  
  def export_to_json
    raise NotImplementedError
  end
end

class ProductCatalog
  include Searchable
  include Sortable
  include Exportable
  
  def initialize
    @products = []
  end
  
  def search(query)
    @products.select { |p| p[:name].include?(query) }
  end
  
  def filter(criteria)
    @products.select { |p| criteria.all? { |k, v| p[k] == v } }
  end
  
  def sort_by_field(field)
    @products.sort_by { |p| p[field] }
  end
  
  def reverse_sort
    @products.reverse
  end
  
  def export_to_csv
    @products.map { |p| p.values.join(',') }.join("\n")
  end
  
  def export_to_json
    require 'json'
    @products.to_json
  end
end

Refinements for Scoped Interfaces: Ruby refinements apply interface-like behavior to existing classes within limited scope. Refinements add methods to core classes without global modification, providing localized interface extensions.

module StringSerializable
  refine String do
    def serialize
      { type: 'string', value: self, length: length }
    end
  end
end

module ArraySerializable
  refine Array do
    def serialize
      { type: 'array', value: self, size: size }
    end
  end
end

class Serializer
  using StringSerializable
  using ArraySerializable
  
  def serialize_object(obj)
    obj.serialize
  end
end

serializer = Serializer.new
puts serializer.serialize_object("hello")
# {:type=>"string", :value=>"hello", :length=>5}
puts serializer.serialize_object([1, 2, 3])
# {:type=>"array", :value=>[1, 2, 3], :size=>3}

Practical Examples

Plugin System Architecture: A plugin system defines interfaces that plugins must implement, allowing third-party code to extend application functionality. The host application depends on plugin interfaces without knowing specific implementations. Plugins register themselves and the host application invokes their methods through the interface contract.

module Plugin
  def initialize_plugin(config)
    raise NotImplementedError
  end
  
  def execute(context)
    raise NotImplementedError
  end
  
  def cleanup
    raise NotImplementedError
  end
  
  def plugin_name
    raise NotImplementedError
  end
  
  def plugin_version
    raise NotImplementedError
  end
end

class EmailNotificationPlugin
  include Plugin
  
  def initialize_plugin(config)
    @smtp_server = config[:smtp_server]
    @from_address = config[:from_address]
    @enabled = true
  end
  
  def execute(context)
    return unless @enabled
    
    recipient = context[:recipient]
    message = context[:message]
    subject = context[:subject] || "Notification"
    
    send_email(recipient, subject, message)
  end
  
  def cleanup
    @enabled = false
    puts "#{plugin_name} cleaned up"
  end
  
  def plugin_name
    "Email Notification Plugin"
  end
  
  def plugin_version
    "1.0.0"
  end
  
  private
  
  def send_email(recipient, subject, message)
    "Sending email to #{recipient}: #{subject} - #{message}"
  end
end

class WebhookPlugin
  include Plugin
  
  def initialize_plugin(config)
    @endpoint_url = config[:endpoint_url]
    @api_key = config[:api_key]
    @retry_count = config[:retry_count] || 3
  end
  
  def execute(context)
    payload = {
      event: context[:event],
      data: context[:data],
      timestamp: Time.now.to_i
    }
    
    post_to_webhook(payload)
  end
  
  def cleanup
    puts "#{plugin_name} cleaned up"
  end
  
  def plugin_name
    "Webhook Plugin"
  end
  
  def plugin_version
    "2.1.0"
  end
  
  private
  
  def post_to_webhook(payload)
    "Posting to #{@endpoint_url}: #{payload}"
  end
end

class PluginManager
  def initialize
    @plugins = []
  end
  
  def register_plugin(plugin_class, config)
    plugin = plugin_class.new
    plugin.initialize_plugin(config)
    @plugins << plugin
    puts "Registered: #{plugin.plugin_name} v#{plugin.plugin_version}"
  end
  
  def execute_all(context)
    @plugins.each do |plugin|
      begin
        plugin.execute(context)
      rescue => e
        puts "Error in #{plugin.plugin_name}: #{e.message}"
      end
    end
  end
  
  def shutdown
    @plugins.each(&:cleanup)
  end
end

manager = PluginManager.new
manager.register_plugin(EmailNotificationPlugin, {
  smtp_server: 'smtp.example.com',
  from_address: 'noreply@example.com'
})
manager.register_plugin(WebhookPlugin, {
  endpoint_url: 'https://api.example.com/webhook',
  api_key: 'secret_key'
})

manager.execute_all({
  event: 'user_signup',
  data: { user_id: 123, email: 'user@example.com' },
  recipient: 'user@example.com',
  message: 'Welcome to our service!'
})

manager.shutdown

State Machine with Abstract States: State machines implement each state as a class sharing a common interface or extending an abstract state class. The context delegates operations to the current state object, which handles the operation according to state-specific logic and transitions to new states.

class State
  def initialize(context)
    @context = context
  end
  
  def enter
    raise NotImplementedError
  end
  
  def exit
    raise NotImplementedError
  end
  
  def handle_event(event)
    raise NotImplementedError
  end
  
  def state_name
    raise NotImplementedError
  end
end

class IdleState < State
  def enter
    puts "Entering Idle state"
  end
  
  def exit
    puts "Exiting Idle state"
  end
  
  def handle_event(event)
    case event
    when :start
      @context.transition_to(RunningState.new(@context))
    when :stop
      puts "Already idle, ignoring stop event"
    else
      puts "Unknown event: #{event}"
    end
  end
  
  def state_name
    "Idle"
  end
end

class RunningState < State
  def enter
    puts "Entering Running state"
    @start_time = Time.now
  end
  
  def exit
    duration = Time.now - @start_time
    puts "Exiting Running state (ran for #{duration.round(2)}s)"
  end
  
  def handle_event(event)
    case event
    when :pause
      @context.transition_to(PausedState.new(@context))
    when :stop
      @context.transition_to(StoppedState.new(@context))
    when :start
      puts "Already running, ignoring start event"
    else
      puts "Unknown event: #{event}"
    end
  end
  
  def state_name
    "Running"
  end
end

class PausedState < State
  def enter
    puts "Entering Paused state"
  end
  
  def exit
    puts "Exiting Paused state"
  end
  
  def handle_event(event)
    case event
    when :resume
      @context.transition_to(RunningState.new(@context))
    when :stop
      @context.transition_to(StoppedState.new(@context))
    else
      puts "Unknown event: #{event}"
    end
  end
  
  def state_name
    "Paused"
  end
end

class StoppedState < State
  def enter
    puts "Entering Stopped state"
  end
  
  def exit
    puts "Exiting Stopped state"
  end
  
  def handle_event(event)
    case event
    when :reset
      @context.transition_to(IdleState.new(@context))
    else
      puts "Cannot handle event #{event} in Stopped state"
    end
  end
  
  def state_name
    "Stopped"
  end
end

class StateMachine
  def initialize
    @current_state = IdleState.new(self)
    @current_state.enter
  end
  
  def transition_to(new_state)
    @current_state.exit
    @current_state = new_state
    @current_state.enter
  end
  
  def handle_event(event)
    puts "\nHandling event: #{event} in state: #{@current_state.state_name}"
    @current_state.handle_event(event)
  end
  
  def current_state_name
    @current_state.state_name
  end
end

machine = StateMachine.new
machine.handle_event(:start)
sleep(0.1)
machine.handle_event(:pause)
machine.handle_event(:resume)
sleep(0.1)
machine.handle_event(:stop)
machine.handle_event(:reset)

Data Access Layer with Repository Pattern: Abstract repository classes define data access interfaces while concrete implementations handle specific data sources. Applications depend on repository interfaces, remaining agnostic to whether data comes from databases, APIs, files, or memory.

class Repository
  def find_by_id(id)
    raise NotImplementedError
  end
  
  def find_all
    raise NotImplementedError
  end
  
  def find_where(conditions)
    raise NotImplementedError
  end
  
  def save(entity)
    raise NotImplementedError
  end
  
  def delete(id)
    raise NotImplementedError
  end
  
  def count
    raise NotImplementedError
  end
end

class InMemoryRepository < Repository
  def initialize
    @storage = {}
    @next_id = 1
  end
  
  def find_by_id(id)
    @storage[id]
  end
  
  def find_all
    @storage.values
  end
  
  def find_where(conditions)
    @storage.values.select do |entity|
      conditions.all? { |key, value| entity[key] == value }
    end
  end
  
  def save(entity)
    if entity[:id]
      @storage[entity[:id]] = entity
    else
      entity[:id] = @next_id
      @storage[@next_id] = entity
      @next_id += 1
    end
    entity
  end
  
  def delete(id)
    @storage.delete(id)
  end
  
  def count
    @storage.size
  end
end

class FileRepository < Repository
  def initialize(filepath)
    @filepath = filepath
    @storage = load_from_file
  end
  
  def find_by_id(id)
    @storage[id.to_s]
  end
  
  def find_all
    @storage.values
  end
  
  def find_where(conditions)
    @storage.values.select do |entity|
      conditions.all? { |key, value| entity[key.to_s] == value }
    end
  end
  
  def save(entity)
    entity['id'] ||= generate_id
    @storage[entity['id']] = entity
    persist_to_file
    entity
  end
  
  def delete(id)
    result = @storage.delete(id.to_s)
    persist_to_file
    result
  end
  
  def count
    @storage.size
  end
  
  private
  
  def load_from_file
    return {} unless File.exist?(@filepath)
    require 'json'
    JSON.parse(File.read(@filepath))
  rescue
    {}
  end
  
  def persist_to_file
    require 'json'
    File.write(@filepath, JSON.pretty_generate(@storage))
  end
  
  def generate_id
    "#{Time.now.to_i}-#{rand(10000)}"
  end
end

class UserService
  def initialize(repository)
    @repository = repository
  end
  
  def create_user(name, email)
    user = { name: name, email: email, created_at: Time.now.to_s }
    @repository.save(user)
  end
  
  def get_user(id)
    @repository.find_by_id(id)
  end
  
  def find_by_email(email)
    @repository.find_where(email: email).first
  end
  
  def all_users
    @repository.find_all
  end
  
  def delete_user(id)
    @repository.delete(id)
  end
end

# Using in-memory repository
memory_repo = InMemoryRepository.new
service = UserService.new(memory_repo)

user1 = service.create_user("Alice", "alice@example.com")
user2 = service.create_user("Bob", "bob@example.com")

puts "All users: #{service.all_users.size}"
puts "Find by email: #{service.find_by_email('alice@example.com')}"

# Switching to file repository
file_repo = FileRepository.new('users.json')
service = UserService.new(file_repo)

service.create_user("Charlie", "charlie@example.com")
puts "Users in file: #{service.all_users.size}"

Command Pattern with Abstract Command: Command objects encapsulate operations as first-class objects. An abstract command class defines the interface for executing and undoing operations. Concrete commands implement specific operations while a command manager handles execution, undo, and redo functionality.

class Command
  def execute
    raise NotImplementedError
  end
  
  def undo
    raise NotImplementedError
  end
  
  def description
    raise NotImplementedError
  end
end

class CreateFileCommand < Command
  def initialize(filepath, content)
    @filepath = filepath
    @content = content
    @existed = false
  end
  
  def execute
    @existed = File.exist?(@filepath)
    File.write(@filepath, @content)
    "Created file: #{@filepath}"
  end
  
  def undo
    if @existed
      File.write(@filepath, "")
    else
      File.delete(@filepath) if File.exist?(@filepath)
    end
    "Undone file creation: #{@filepath}"
  end
  
  def description
    "Create file #{@filepath}"
  end
end

class DeleteFileCommand < Command
  def initialize(filepath)
    @filepath = filepath
    @previous_content = nil
  end
  
  def execute
    if File.exist?(@filepath)
      @previous_content = File.read(@filepath)
      File.delete(@filepath)
      "Deleted file: #{@filepath}"
    else
      "File does not exist: #{@filepath}"
    end
  end
  
  def undo
    if @previous_content
      File.write(@filepath, @previous_content)
      "Restored file: #{@filepath}"
    else
      "Cannot undo: file was not deleted"
    end
  end
  
  def description
    "Delete file #{@filepath}"
  end
end

class AppendToFileCommand < Command
  def initialize(filepath, content)
    @filepath = filepath
    @content = content
    @append_length = content.length
  end
  
  def execute
    File.open(@filepath, 'a') { |f| f.write(@content) }
    "Appended to file: #{@filepath}"
  end
  
  def undo
    current_content = File.read(@filepath)
    new_content = current_content[0...-@append_length]
    File.write(@filepath, new_content)
    "Undone append to: #{@filepath}"
  end
  
  def description
    "Append to file #{@filepath}"
  end
end

class CommandManager
  def initialize
    @history = []
    @undo_stack = []
  end
  
  def execute_command(command)
    result = command.execute
    @history << command
    @undo_stack.clear
    puts result
    result
  end
  
  def undo
    return "Nothing to undo" if @history.empty?
    
    command = @history.pop
    result = command.undo
    @undo_stack << command
    puts result
    result
  end
  
  def redo
    return "Nothing to redo" if @undo_stack.empty?
    
    command = @undo_stack.pop
    result = command.execute
    @history << command
    puts result
    result
  end
  
  def show_history
    puts "Command History:"
    @history.each_with_index do |cmd, i|
      puts "#{i + 1}. #{cmd.description}"
    end
  end
end

manager = CommandManager.new

manager.execute_command(CreateFileCommand.new('test.txt', 'Hello'))
manager.execute_command(AppendToFileCommand.new('test.txt', ' World'))
manager.show_history

manager.undo
manager.undo
manager.redo

Design Considerations

Interface vs Abstract Class Selection: Choose interfaces for pure behavior contracts without shared implementation. Use abstract classes when subclasses share common code alongside required method implementations. Interfaces suit unrelated classes that need common operations. Abstract classes suit related classes with shared functionality and partial implementations. A Flyable interface works for birds, planes, and insects. A Bird abstract class works for sparrows, eagles, and penguins sharing common bird characteristics.

Multiple interface implementation contrasts with single abstract class inheritance. Classes implement multiple interfaces to gain various capability contracts. Classes extend one abstract class to inherit shared implementation. This distinction affects design flexibility—interfaces provide compositional capability, abstract classes provide hierarchical relationships.

Granularity and Interface Segregation: Design focused, single-purpose interfaces rather than monolithic ones. The Interface Segregation Principle states that clients should not depend on methods they don't use. Split large interfaces into smaller, cohesive ones. Instead of a Printer interface with print, scan, fax, and staple methods, create separate Printable, Scannable, Faxable, and Stapleable interfaces. Classes implement only relevant interfaces.

Fine-grained interfaces reduce coupling and increase flexibility. A simple printer implements only Printable. A multifunction device implements all interfaces. Code requiring printing depends only on Printable, working with simple and complex devices. This granularity enables targeted mocking in tests and reduces implementation burden on classes needing subset functionality.

Explicit vs Implicit Contracts: Static languages enforce contracts at compile time through type checking. Dynamic languages rely on runtime checks and documentation. Ruby's implicit contracts through duck typing provide flexibility but sacrifice safety. Explicit contract verification through modules with NotImplementedError provides middle ground—runtime enforcement with clear documentation.

Design decisions balance flexibility and safety. Explicit contracts catch missing implementations at method call time. Implicit contracts fail only when code paths execute. Production systems often prefer explicit contracts despite Ruby's dynamic nature. The added verbosity documents expectations and catches errors earlier.

Abstraction Level: Position interfaces and abstract classes at appropriate abstraction levels. High-level abstractions capture general concepts applicable across domains. Low-level abstractions address specific technical concerns. A Comparable interface represents high-level ordering concept. A SQLQueryBuilder abstract class addresses specific database query construction.

Abstraction level affects reusability and flexibility. High-level abstractions apply broadly but may lack specific functionality. Low-level abstractions provide targeted capabilities but limit applicability. Design interfaces at the level where multiple concrete implementations naturally exist. If only one implementation exists, abstraction may be premature.

Stability and Evolution: Interfaces and abstract classes form stable contracts that multiple clients depend on. Changes to these contracts affect all implementing classes and dependent code. Design interfaces carefully to minimize future changes. Consider extension points for adding functionality without breaking existing contracts.

Abstract classes handle evolution better than interfaces through concrete method additions. Adding a concrete method to an abstract class doesn't break subclasses. Adding a method to an interface requires all implementations to update. Ruby's dynamic nature allows adding methods without breaking compilation, but runtime errors occur when code calls unimplemented methods.

Versioning strategies handle necessary contract changes. Create new interface versions rather than modifying existing ones. Mark deprecated methods while maintaining backward compatibility. Use adapter patterns to bridge old and new interfaces during transitions.

Composition Over Inheritance: Favor interface composition over abstract class inheritance when possible. Composition provides flexibility to combine capabilities without deep inheritance hierarchies. A class implementing multiple interfaces gains varied capabilities. Deep inheritance hierarchies create tight coupling and fragility.

Abstract classes work well for template method patterns and shared implementation among closely related types. Interfaces work better for cross-cutting concerns that span unrelated classes. A logging capability applies across unrelated classes through a Loggable interface. A Vehicle abstract class captures shared behavior among cars, trucks, and motorcycles.

Testing and Mockability: Interfaces and abstract classes improve testability by enabling dependency injection and mocking. Tests inject mock implementations satisfying interface contracts without requiring real implementations. A test for code depending on PaymentProcessor injects a mock processor that records calls and returns predetermined results.

Abstract classes may complicate testing if they contain complex concrete methods. Tests must exercise or stub these methods when testing subclasses. Prefer minimal concrete functionality in abstract classes, delegating complexity to subclasses or separate classes. This approach simplifies testing by reducing abstract class surface area.

Common Patterns

Mix-in Pattern: Ruby modules provide mix-in functionality, adding interface contracts and concrete implementations to classes through inclusion. Mix-ins implement horizontal capabilities that span unrelated classes. Unlike Java interfaces, Ruby mix-ins can include both abstract method declarations and concrete implementations.

module Timestampable
  def created_at
    @created_at ||= Time.now
  end
  
  def updated_at
    @updated_at
  end
  
  def touch
    @updated_at = Time.now
  end
end

module Validatable
  def valid?
    validate.empty?
  end
  
  def errors
    @errors ||= []
  end
  
  def validate
    raise NotImplementedError, "#{self.class} must implement validate"
  end
end

class User
  include Timestampable
  include Validatable
  
  attr_accessor :email, :name
  
  def initialize(email, name)
    @email = email
    @name = name
  end
  
  def validate
    errors = []
    errors << "Email required" if email.nil? || email.empty?
    errors << "Name required" if name.nil? || name.empty?
    errors << "Invalid email format" unless email =~ /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
    errors
  end
end

user = User.new("test@example.com", "Test User")
puts "Created at: #{user.created_at}"
puts "Valid: #{user.valid?}"
user.touch
puts "Updated at: #{user.updated_at}"

Strategy Pattern: Strategy pattern encapsulates algorithms as objects implementing a common interface. The context delegates algorithm execution to a strategy object. Clients select strategies at runtime, changing behavior without modifying the context. This pattern separates algorithm variations from code using the algorithms.

module SortStrategy
  def sort(array)
    raise NotImplementedError
  end
  
  def strategy_name
    raise NotImplementedError
  end
end

class BubbleSortStrategy
  include SortStrategy
  
  def sort(array)
    arr = array.dup
    n = arr.length
    
    (n - 1).times do
      (0...n - 1).each do |i|
        if arr[i] > arr[i + 1]
          arr[i], arr[i + 1] = arr[i + 1], arr[i]
        end
      end
    end
    
    arr
  end
  
  def strategy_name
    "Bubble Sort"
  end
end

class QuickSortStrategy
  include SortStrategy
  
  def sort(array)
    return array if array.length <= 1
    
    pivot = array[array.length / 2]
    left = array.select { |x| x < pivot }
    middle = array.select { |x| x == pivot }
    right = array.select { |x| x > pivot }
    
    sort(left) + middle + sort(right)
  end
  
  def strategy_name
    "Quick Sort"
  end
end

class MergeSortStrategy
  include SortStrategy
  
  def sort(array)
    return array if array.length <= 1
    
    mid = array.length / 2
    left = sort(array[0...mid])
    right = sort(array[mid..-1])
    
    merge(left, right)
  end
  
  def strategy_name
    "Merge Sort"
  end
  
  private
  
  def merge(left, right)
    result = []
    
    until left.empty? || right.empty?
      result << (left.first <= right.first ? left.shift : right.shift)
    end
    
    result + left + right
  end
end

class Sorter
  def initialize(strategy)
    @strategy = strategy
  end
  
  def change_strategy(strategy)
    @strategy = strategy
  end
  
  def sort(array)
    puts "Using #{@strategy.strategy_name}"
    @strategy.sort(array)
  end
end

data = [64, 34, 25, 12, 22, 11, 90]

sorter = Sorter.new(BubbleSortStrategy.new)
puts sorter.sort(data).inspect

sorter.change_strategy(QuickSortStrategy.new)
puts sorter.sort(data).inspect

sorter.change_strategy(MergeSortStrategy.new)
puts sorter.sort(data).inspect

Null Object Pattern: Null object pattern implements an interface or extends an abstract class to provide default behavior for absent objects. Instead of checking for nil, code operates on null objects that respond to the same methods as real objects but perform no-op or default operations. This pattern eliminates nil checks and simplifies calling code.

class Logger
  def log(message)
    raise NotImplementedError
  end
  
  def error(message)
    raise NotImplementedError
  end
  
  def warn(message)
    raise NotImplementedError
  end
end

class ConsoleLogger < Logger
  def log(message)
    puts "[INFO] #{Time.now}: #{message}"
  end
  
  def error(message)
    puts "[ERROR] #{Time.now}: #{message}"
  end
  
  def warn(message)
    puts "[WARN] #{Time.now}: #{message}"
  end
end

class NullLogger < Logger
  def log(message)
    # Do nothing
  end
  
  def error(message)
    # Do nothing
  end
  
  def warn(message)
    # Do nothing
  end
end

class Application
  def initialize(logger = NullLogger.new)
    @logger = logger
  end
  
  def process_data(data)
    @logger.log("Processing data: #{data}")
    
    if data.empty?
      @logger.warn("Empty data received")
      return
    end
    
    begin
      # Process data
      result = data.upcase
      @logger.log("Processing completed: #{result}")
      result
    rescue => e
      @logger.error("Processing failed: #{e.message}")
      nil
    end
  end
end

# With real logger
app_with_logging = Application.new(ConsoleLogger.new)
app_with_logging.process_data("test data")

# Without logger (null object)
app_without_logging = Application.new
app_without_logging.process_data("test data")

Adapter Pattern: Adapter pattern converts an interface into another interface clients expect. An adapter implements the target interface while wrapping an object with incompatible interface. This pattern enables integration of classes with incompatible interfaces without modifying their code. Adapters translate method calls and data formats between interfaces.

module MediaPlayer
  def play(filename)
    raise NotImplementedError
  end
  
  def stop
    raise NotImplementedError
  end
end

class MP3Player
  def play_mp3(filename)
    "Playing MP3 file: #{filename}"
  end
  
  def stop_mp3
    "Stopping MP3 playback"
  end
end

class MP4Player
  def play_mp4(filename)
    "Playing MP4 file: #{filename}"
  end
  
  def stop_mp4
    "Stopping MP4 playback"
  end
end

class MP3Adapter
  include MediaPlayer
  
  def initialize
    @player = MP3Player.new
  end
  
  def play(filename)
    @player.play_mp3(filename)
  end
  
  def stop
    @player.stop_mp3
  end
end

class MP4Adapter
  include MediaPlayer
  
  def initialize
    @player = MP4Player.new
  end
  
  def play(filename)
    @player.play_mp4(filename)
  end
  
  def stop
    @player.stop_mp4
  end
end

class AudioSystem
  def initialize(player)
    @player = player
  end
  
  def play_audio(filename)
    result = @player.play(filename)
    puts result
  end
  
  def stop_audio
    result = @player.stop
    puts result
  end
end

mp3_system = AudioSystem.new(MP3Adapter.new)
mp3_system.play_audio("song.mp3")
mp3_system.stop_audio

mp4_system = AudioSystem.new(MP4Adapter.new)
mp4_system.play_audio("video.mp4")
mp4_system.stop_audio

Factory Method Pattern: Factory method pattern defines an interface for creating objects but delegates instantiation to subclasses. An abstract creator class declares a factory method returning objects of an abstract product type. Concrete creators override the factory method to return specific product instances. This pattern encapsulates object creation and promotes loose coupling.

class Document
  def open
    raise NotImplementedError
  end
  
  def save(content)
    raise NotImplementedError
  end
  
  def close
    raise NotImplementedError
  end
end

class TextDocument < Document
  def open
    "Opening text document"
  end
  
  def save(content)
    "Saving text: #{content}"
  end
  
  def close
    "Closing text document"
  end
end

class SpreadsheetDocument < Document
  def open
    "Opening spreadsheet document"
  end
  
  def save(content)
    "Saving spreadsheet data: #{content}"
  end
  
  def close
    "Closing spreadsheet document"
  end
end

class Application
  def create_document
    raise NotImplementedError, "#{self.class} must implement create_document"
  end
  
  def new_document
    doc = create_document
    puts doc.open
    doc
  end
  
  def save_document(doc, content)
    puts doc.save(content)
  end
end

class TextEditorApplication < Application
  def create_document
    TextDocument.new
  end
end

class SpreadsheetApplication < Application
  def create_document
    SpreadsheetDocument.new
  end
end

text_app = TextEditorApplication.new
doc1 = text_app.new_document
text_app.save_document(doc1, "Hello World")

sheet_app = SpreadsheetApplication.new
doc2 = sheet_app.new_document
sheet_app.save_document(doc2, [[1, 2], [3, 4]])

Reference

Interface Design Guidelines

Guideline Description Example
Single Responsibility Each interface defines one cohesive set of operations Separate Readable and Writable instead of combined ReadWriteable
Method Naming Use clear, descriptive method names that convey intent process_payment instead of proc or handle
Parameter Design Accept general types, return specific types Accept hash or object, return specific result type
Documentation Document expected behavior, preconditions, postconditions Specify valid parameter ranges and return value meanings
Consistency Use consistent naming and parameter patterns across methods All query methods return collections, all save methods return boolean

Abstract Class Implementation

Component Purpose Ruby Implementation
Abstract Methods Methods requiring subclass implementation Define method raising NotImplementedError
Concrete Methods Shared implementation across subclasses Regular method definitions with complete logic
Template Methods Algorithm structure delegating to abstract methods Concrete method calling abstract methods
Initialization Shared setup logic for subclasses Initialize method with common setup, call super in subclasses
Class Methods Factory methods or utility methods Define class methods for object creation or shared utilities

Module vs Inheritance Decision Matrix

Scenario Use Module Use Inheritance Rationale
Unrelated classes need same capability Yes No Composition over inheritance for cross-cutting concerns
Classes share is-a relationship No Yes Inheritance expresses taxonomic relationship
Multiple capability sets needed Yes No Ruby supports multiple module inclusion
Shared implementation required Maybe Yes Abstract classes better for substantial shared code
Interface contract only Yes No Modules define behavior contracts
Template method pattern No Yes Abstract classes control algorithm structure

Common Interface Patterns

Pattern Interface Structure Use Case
Comparable compare_to or spaceship operator Objects requiring ordering and comparison
Enumerable each method returning elements Collections and sequences
Serializable serialize and deserialize methods Object persistence and transmission
Observable notify and subscribe methods Event-driven architectures
Closeable close or cleanup method Resource management and cleanup
Cloneable clone or deep_copy method Object duplication
Validator valid? and errors methods Data validation

Module Inclusion Strategies

Strategy Syntax Behavior Common Use
Include include ModuleName Adds module methods as instance methods Adding capabilities to instances
Extend extend ModuleName Adds module methods as class methods Adding class-level functionality
Prepend prepend ModuleName Inserts module before class in method lookup Method wrapping and decoration
Refinements using ModuleName Scoped modifications to existing classes Localized class extensions

Error Handling in Abstract Methods

Approach Implementation Advantage Disadvantage
NotImplementedError raise NotImplementedError Clear error message, explicit contract Only fails at runtime when called
Method Check respond_to? before calling Prevents calling unimplemented methods Requires defensive coding
Module Verification Module included check Ensures interface compliance Checks presence not implementation
Contract Testing Automated test checking implementation Catches missing methods in tests Requires comprehensive test coverage

Interface Evolution Strategies

Strategy Approach Impact When to Use
New Methods Add methods to interface Breaks existing implementations When functionality truly required
Default Implementation Add concrete method to abstract class No breakage, may not suit all subclasses When reasonable default exists
New Interface Version Create InterfaceV2 Maintains backward compatibility Major changes required
Optional Methods Document as optional, provide default Flexible but unclear contract Gradual feature adoption
Extension Modules Separate optional capabilities Clean separation of concerns Optional feature sets

Testing Interface Implementations

Test Type Focus Example
Contract Tests Verify all required methods present Check class responds to interface methods
Behavior Tests Verify method behavior meets specification Test return values and side effects
Polymorphism Tests Verify substitutability Test interface reference with different implementations
Edge Case Tests Verify handling of boundary conditions Test nil, empty, extreme values
Integration Tests Verify implementations work in context Test with real dependencies

Duck Typing Verification

Technique Code Example Purpose
respond_to? object.respond_to?(:method_name) Check method availability
method_defined? Class.method_defined?(:method_name) Check if class defines method
Rescue NoMethodError begin; object.method; rescue NoMethodError Handle missing methods gracefully
Method Check if object.methods.include?(:method_name) Verify method exists

Ruby Module Method Types

Method Type Definition Access Purpose
Instance Methods def method_name Via instances after include Add instance capabilities
Class Methods def self.method_name Via class after extend Add class-level utilities
Module Functions module_function :method_name Both module and instance Utility functions
Private Methods private def method_name Only within class/module Internal implementation
Protected Methods protected def method_name Within class hierarchy Controlled access