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 |