CrackedRuby CrackedRuby

Overview

Coupling measures the degree of interdependence between software modules, while cohesion measures how closely related the responsibilities within a single module are. These two metrics form the foundation of modular design, directly impacting maintainability, testability, and system flexibility.

Larry Constantine introduced these concepts in the late 1960s as part of structured design methodology. Coupling describes the strength of connections between modules, ranging from tight coupling where changes in one module force changes in others, to loose coupling where modules operate independently. Cohesion describes the internal unity of a module, ranging from low cohesion where a module handles unrelated responsibilities, to high cohesion where all elements work toward a single, well-defined purpose.

The relationship between these metrics drives design quality: systems with low coupling and high cohesion resist change propagation, isolate failures, and support independent development and testing. Conversely, tightly coupled systems with low cohesion create cascading failures, complicate testing, and make modifications risky.

# High coupling - Order directly manipulates Customer internals
class Order
  def apply_discount(customer)
    if customer.purchase_history.total > 1000
      customer.loyalty_points += 50
      customer.discount_tier = 'gold'
    end
    @total *= 0.9
  end
end

# Low coupling - Order delegates through Customer's interface
class Order
  def apply_discount(customer)
    customer.process_loyalty_reward(50) if qualifies_for_reward?(customer)
    @total *= 0.9
  end
  
  private
  
  def qualifies_for_reward?(customer)
    customer.lifetime_value > 1000
  end
end

The distinction matters because coupling and cohesion interact: reducing coupling often requires increasing cohesion, as responsibilities consolidate into well-defined modules. This tension forces architectural decisions about where to draw module boundaries.

Key Principles

Coupling exists in multiple forms, each with different implications for system design. Content coupling occurs when one module modifies or relies on the internal workings of another. Common coupling emerges when multiple modules share global data. Control coupling appears when one module controls the flow of another by passing control information. Stamp coupling happens when modules share composite data structures but use only portions of them. Data coupling, the loosest form, involves modules sharing only primitive data through parameters.

# Content coupling - accessing another class's internals
class ReportGenerator
  def generate(user)
    # Directly accessing and modifying User's internal state
    user.instance_variable_set(:@last_report_date, Time.now)
    data = user.instance_variable_get(:@cached_data)
  end
end

# Data coupling - clean interface communication
class ReportGenerator
  def generate(user_id, report_type)
    user_data = UserRepository.fetch(user_id)
    format_report(user_data, report_type)
  end
end

Cohesion also manifests in distinct levels. Coincidental cohesion represents the weakest form, where module elements share no meaningful relationship. Logical cohesion groups elements that perform similar operations but on different data. Temporal cohesion clusters operations performed at the same time. Procedural cohesion links elements that execute in sequence. Communicational cohesion groups operations on the same data. Sequential cohesion chains elements where output from one feeds into the next. Functional cohesion, the strongest form, unifies all elements toward a single, well-defined purpose.

# Low cohesion - mixed responsibilities
class UserManager
  def create_user(params)
    # User creation
  end
  
  def send_email(to, subject)
    # Email sending
  end
  
  def log_activity(action)
    # Logging
  end
  
  def validate_credit_card(number)
    # Payment validation
  end
end

# High cohesion - focused responsibility
class UserRegistration
  def initialize(params)
    @params = params
  end
  
  def register
    validate_input
    create_user_record
    send_welcome_email
  end
  
  private
  
  def validate_input
    # Validation logic
  end
  
  def create_user_record
    # Persistence logic
  end
  
  def send_welcome_email
    # Welcome email logic
  end
end

The dependency inversion principle relates directly to coupling: high-level modules should not depend on low-level modules, both should depend on abstractions. This principle reduces coupling by introducing interfaces that isolate modules from implementation details. Ruby's duck typing supports this through implicit interfaces, where objects need only respond to expected methods rather than inherit from specific types.

Information hiding reinforces low coupling by restricting access to module internals. Ruby provides varying degrees of visibility through public, protected, and private methods, though the language's dynamic nature allows circumvention of these restrictions. The principle remains valuable: expose minimal interfaces and encapsulate implementation details.

The Law of Demeter codifies low coupling through a simple rule: an object should only talk to its immediate neighbors. Specifically, a method should only call methods on: the object itself, method parameters, objects it creates, and its direct attributes. Violations create coupling chains where changes ripple through multiple modules.

# Violates Law of Demeter
class OrderProcessor
  def calculate_shipping(order)
    order.customer.address.country.shipping_rate
  end
end

# Follows Law of Demeter
class OrderProcessor
  def calculate_shipping(order)
    order.shipping_rate
  end
end

class Order
  def shipping_rate
    customer.shipping_rate_for_location
  end
end

Ruby Implementation

Ruby's module system provides several mechanisms for managing coupling and cohesion. Modules serve as namespaces, mixins, and containers for related functionality. The include keyword adds module methods as instance methods, extend adds them as class methods, and prepend inserts the module before the class in the method lookup chain.

module Auditable
  def audit_log
    @audit_log ||= []
  end
  
  def record_change(attribute, old_value, new_value)
    audit_log << {
      attribute: attribute,
      old_value: old_value,
      new_value: new_value,
      timestamp: Time.now
    }
  end
end

class Account
  include Auditable
  
  attr_reader :balance
  
  def initialize(initial_balance)
    @balance = initial_balance
  end
  
  def deposit(amount)
    old_balance = @balance
    @balance += amount
    record_change(:balance, old_balance, @balance)
  end
end

Ruby's open classes create potential for tight coupling. Any code can modify any class at runtime, leading to action-at-a-distance problems where changes in one location affect distant code. Refinements provide scoped monkey patching, limiting modifications to specific contexts.

# Global modification - high coupling risk
class String
  def titleize
    split.map(&:capitalize).join(' ')
  end
end

# Scoped modification - controlled coupling
module StringRefinements
  refine String do
    def titleize
      split.map(&:capitalize).join(' ')
    end
  end
end

class DocumentFormatter
  using StringRefinements
  
  def format_title(text)
    text.titleize
  end
end

Dependency injection reduces coupling by providing dependencies from outside rather than hardcoding them internally. Ruby's dynamic nature makes this straightforward through constructor injection, setter injection, or parameter injection.

# Tight coupling - hardcoded dependency
class OrderNotifier
  def notify(order)
    SmtpMailer.new.send(
      to: order.customer.email,
      subject: "Order Confirmation",
      body: format_order(order)
    )
  end
end

# Loose coupling - injected dependency
class OrderNotifier
  def initialize(mailer: SmtpMailer.new)
    @mailer = mailer
  end
  
  def notify(order)
    @mailer.send(
      to: order.customer.email,
      subject: "Order Confirmation",
      body: format_order(order)
    )
  end
end

Ruby's blocks, procs, and lambdas enable strategy pattern implementations that reduce coupling by abstracting algorithms. Objects can accept behavior as parameters, avoiding tight coupling to specific implementations.

class DataProcessor
  def initialize(validator:, transformer:)
    @validator = validator
    @transformer = transformer
  end
  
  def process(data)
    return [] unless @validator.call(data)
    data.map(&@transformer)
  end
end

processor = DataProcessor.new(
  validator: ->(data) { data.is_a?(Array) && !data.empty? },
  transformer: ->(item) { item.to_s.upcase }
)

The single responsibility principle guides cohesion in Ruby classes. Each class should have one reason to change, meaning it addresses one specific concern. Ruby's expressive syntax supports this through clear method names and focused classes.

# Low cohesion - multiple responsibilities
class UserAccount
  def authenticate(password)
    # Authentication logic
  end
  
  def generate_pdf_report
    # PDF generation
  end
  
  def send_sms(message)
    # SMS sending
  end
end

# High cohesion - separated responsibilities
class Authenticator
  def authenticate(user, password)
    # Authentication logic
  end
end

class ReportGenerator
  def generate_pdf(user)
    # PDF generation
  end
end

class NotificationService
  def send_sms(recipient, message)
    # SMS sending
  end
end

Practical Examples

Consider an e-commerce system handling order processing. A tightly coupled design creates dependencies between orders, inventory, shipping, and payment systems.

# Tightly coupled implementation
class Order
  def complete
    # Direct database access
    inventory = ActiveRecord::Base.connection.execute(
      "SELECT * FROM inventory WHERE product_id = #{@product_id}"
    )
    
    # Direct manipulation of other classes' internals
    @product.inventory_count -= @quantity
    @product.save!
    
    # Hardcoded payment processing
    stripe_charge = Stripe::Charge.create(
      amount: @total * 100,
      currency: 'usd',
      source: @customer.payment_token
    )
    
    # Direct shipping API call
    shipping_response = HTTP.post(
      'https://shipping-api.example.com/create',
      json: {
        address: @customer.address,
        weight: @product.weight
      }
    )
    
    @status = 'completed'
    save!
  end
end

This design exhibits multiple coupling problems: content coupling through direct database access, common coupling through shared global state, and control coupling through hardcoded API interactions. Changes to payment processing, shipping providers, or inventory management require modifying the Order class.

A loosely coupled alternative isolates concerns through interfaces:

# Loosely coupled implementation
class OrderCompletionService
  def initialize(
    inventory_service:,
    payment_service:,
    shipping_service:,
    notification_service:
  )
    @inventory_service = inventory_service
    @payment_service = payment_service
    @shipping_service = shipping_service
    @notification_service = notification_service
  end
  
  def complete(order)
    return failure('Insufficient inventory') unless @inventory_service.reserve(
      product_id: order.product_id,
      quantity: order.quantity
    )
    
    payment_result = @payment_service.charge(
      customer_id: order.customer_id,
      amount: order.total
    )
    return failure('Payment failed') unless payment_result.success?
    
    shipping_result = @shipping_service.schedule(
      order_id: order.id,
      address: order.shipping_address
    )
    
    order.complete!(
      payment_id: payment_result.id,
      shipping_id: shipping_result.id
    )
    
    @notification_service.order_confirmed(order)
    
    success(order)
  end
  
  private
  
  def success(order)
    Result.new(success: true, order: order)
  end
  
  def failure(reason)
    Result.new(success: false, error: reason)
  end
end

Each service handles a cohesive set of responsibilities. The inventory service manages stock levels, the payment service processes transactions, and the shipping service coordinates delivery. Order completion orchestrates these services without knowing their internal implementations.

Authentication and authorization demonstrate cohesion principles. A low-cohesion approach mixes concerns:

# Low cohesion - mixed concerns
class UserController
  def login
    user = User.find_by(email: params[:email])
    
    if user && BCrypt::Password.new(user.password_hash) == params[:password]
      session[:user_id] = user.id
      session[:login_time] = Time.now
      
      # Logging mixed with authentication
      Rails.logger.info "User #{user.id} logged in"
      
      # Email notification mixed with authentication
      UserMailer.login_notification(user).deliver_later
      
      # Permission checking mixed with authentication
      if user.admin?
        session[:admin] = true
      end
      
      redirect_to dashboard_path
    else
      # Analytics mixed with authentication
      Analytics.track('failed_login', email: params[:email])
      
      flash[:error] = 'Invalid credentials'
      render :login
    end
  end
end

Separating these concerns improves cohesion:

# High cohesion - separated concerns
class SessionsController
  def create
    result = AuthenticationService.authenticate(
      email: params[:email],
      password: params[:password]
    )
    
    if result.success?
      SessionManager.create_session(result.user, session)
      redirect_to dashboard_path
    else
      flash[:error] = 'Invalid credentials'
      render :new
    end
  end
end

class AuthenticationService
  def self.authenticate(email:, password:)
    user = User.find_by(email: email)
    
    return failure unless user && user.authenticate(password)
    
    notify_successful_login(user)
    
    success(user)
  end
  
  private
  
  def self.notify_successful_login(user)
    LoginNotificationJob.perform_later(user.id)
    LoginLogger.log(user)
    LoginAnalytics.track(user)
  end
  
  def self.success(user)
    Result.new(success: true, user: user)
  end
  
  def self.failure
    Result.new(success: false, user: nil)
  end
end

class SessionManager
  def self.create_session(user, session)
    session[:user_id] = user.id
    session[:login_time] = Time.now
    session[:permissions] = PermissionResolver.resolve(user)
  end
end

Design Considerations

Coupling and cohesion tradeoffs affect design decisions throughout development. Reducing coupling often requires adding abstractions, which increases code complexity. The decision depends on change frequency and testing requirements.

Direct coupling minimizes abstraction overhead but creates rigid systems:

class ReportExporter
  def export(report, format)
    case format
    when 'pdf'
      PdfGenerator.new.generate(report)
    when 'csv'
      CsvGenerator.new.generate(report)
    when 'excel'
      ExcelGenerator.new.generate(report)
    end
  end
end

This approach works when formats rarely change and generators share no testing concerns. Adding formats requires modifying ReportExporter, but the simplicity benefits small, stable systems.

Interface-based coupling adds flexibility at the cost of indirection:

class ReportExporter
  def initialize(generators: {})
    @generators = generators
  end
  
  def export(report, format)
    generator = @generators[format]
    raise UnknownFormat, format unless generator
    
    generator.generate(report)
  end
end

# Configuration
exporter = ReportExporter.new(
  generators: {
    'pdf' => PdfGenerator.new,
    'csv' => CsvGenerator.new,
    'excel' => ExcelGenerator.new
  }
)

This design supports testing with mock generators and adding formats without modifying ReportExporter. The tradeoff: more moving parts and configuration complexity.

Cohesion decisions balance granularity against coordination overhead. Fine-grained classes with high cohesion create more objects to coordinate:

class OrderProcessor
  def initialize
    @validator = OrderValidator.new
    @inventory = InventoryChecker.new
    @pricer = PriceCalculator.new
    @persister = OrderPersister.new
  end
  
  def process(order_params)
    errors = @validator.validate(order_params)
    return failure(errors) unless errors.empty?
    
    return failure('Out of stock') unless @inventory.available?(order_params)
    
    order = build_order(order_params)
    order.total = @pricer.calculate(order)
    
    @persister.save(order)
    
    success(order)
  end
end

Coarser-grained classes reduce coordination but risk mixed responsibilities:

class OrderProcessor
  def process(order_params)
    return failure('Invalid order') unless valid?(order_params)
    return failure('Out of stock') unless available?(order_params)
    
    order = create_order(order_params)
    calculate_total(order)
    save_order(order)
    
    success(order)
  end
  
  private
  
  def valid?(params)
    # Validation logic
  end
  
  def available?(params)
    # Inventory logic
  end
  
  def create_order(params)
    # Creation logic
  end
  
  def calculate_total(order)
    # Pricing logic
  end
  
  def save_order(order)
    # Persistence logic
  end
end

The choice depends on reuse requirements, testing strategies, and team structure. Systems with multiple order types benefit from fine-grained separation, while simple CRUD applications favor consolidation.

Module boundaries affect coupling propagation. Placing related classes in the same module reduces coupling distances but increases module coupling. Spreading classes across modules creates loose coupling between modules but complicates related changes.

# Single module - tight internal coupling, loose external coupling
module OrderManagement
  class Order
    def calculate_shipping
      ShippingCalculator.calculate(self)
    end
  end
  
  class ShippingCalculator
    def self.calculate(order)
      # Calculation logic with direct Order access
    end
  end
  
  class InventoryChecker
    def self.check(order)
      # Checking logic with direct Order access
    end
  end
end

# Separate modules - loose internal coupling, more coordination
module Orders
  class Order
    def calculate_shipping
      Shipping::Calculator.calculate(shipping_params)
    end
    
    private
    
    def shipping_params
      {
        weight: total_weight,
        destination: shipping_address,
        method: shipping_method
      }
    end
  end
end

module Shipping
  class Calculator
    def self.calculate(params)
      # Calculation logic using only params
    end
  end
end

Common Patterns

The facade pattern reduces coupling by providing a simplified interface to complex subsystems. Clients interact with the facade rather than multiple subsystem classes.

class PaymentFacade
  def initialize
    @validator = PaymentValidator.new
    @processor = PaymentProcessor.new
    @notifier = PaymentNotifier.new
    @logger = PaymentLogger.new
  end
  
  def process_payment(amount:, source:, customer:)
    validation = @validator.validate(amount, source)
    return failure(validation.errors) unless validation.valid?
    
    result = @processor.charge(amount: amount, source: source)
    
    if result.success?
      @notifier.payment_succeeded(customer, amount)
      @logger.log_success(result.transaction_id)
      success(result)
    else
      @notifier.payment_failed(customer, result.reason)
      @logger.log_failure(result.error)
      failure(result.reason)
    end
  end
end

The adapter pattern decouples clients from specific implementations by wrapping incompatible interfaces:

# External service with incompatible interface
class ThirdPartyShipping
  def create_shipment(data)
    # Expects specific data structure
  end
end

# Adapter providing application interface
class ShippingAdapter
  def initialize(service = ThirdPartyShipping.new)
    @service = service
  end
  
  def ship(order)
    shipment_data = {
      origin: format_address(order.origin_address),
      destination: format_address(order.destination_address),
      packages: format_packages(order.items),
      service_level: map_service_level(order.shipping_speed)
    }
    
    @service.create_shipment(shipment_data)
  end
  
  private
  
  def format_address(address)
    # Transform application address to service format
  end
  
  def format_packages(items)
    # Transform items to package format
  end
  
  def map_service_level(speed)
    # Map application speed to service levels
  end
end

The mediator pattern reduces coupling between components by centralizing communication:

class ChatRoom
  def initialize
    @participants = []
  end
  
  def join(participant)
    @participants << participant
    participant.chat_room = self
  end
  
  def send_message(sender, message)
    @participants.each do |participant|
      participant.receive(message) unless participant == sender
    end
  end
end

class Participant
  attr_accessor :chat_room
  attr_reader :name
  
  def initialize(name)
    @name = name
    @messages = []
  end
  
  def send(message)
    @chat_room.send_message(self, message)
  end
  
  def receive(message)
    @messages << message
  end
end

The observer pattern decouples subjects from observers, allowing dynamic subscription:

class OrderSubject
  def initialize
    @observers = []
  end
  
  def attach(observer)
    @observers << observer
  end
  
  def detach(observer)
    @observers.delete(observer)
  end
  
  def notify(event, data)
    @observers.each { |observer| observer.update(event, data) }
  end
end

class Order < OrderSubject
  def complete
    # Order completion logic
    
    notify(:order_completed, self)
  end
end

class InventoryObserver
  def update(event, order)
    return unless event == :order_completed
    
    InventoryService.reduce_stock(order.items)
  end
end

class EmailObserver
  def update(event, order)
    return unless event == :order_completed
    
    OrderMailer.confirmation(order).deliver_later
  end
end

class AnalyticsObserver
  def update(event, order)
    return unless event == :order_completed
    
    Analytics.track('order_completed', order_id: order.id)
  end
end

Dependency injection containers manage object creation and wiring, reducing configuration coupling:

class Container
  def initialize
    @services = {}
    @factories = {}
  end
  
  def register(name, &factory)
    @factories[name] = factory
  end
  
  def get(name)
    @services[name] ||= @factories[name].call(self)
  end
end

# Configuration
container = Container.new

container.register(:database) do
  Database.connect(ENV['DATABASE_URL'])
end

container.register(:user_repository) do |c|
  UserRepository.new(database: c.get(:database))
end

container.register(:authentication_service) do |c|
  AuthenticationService.new(
    repository: c.get(:user_repository),
    encryptor: c.get(:encryptor)
  )
end

Common Pitfalls

Premature abstraction creates unnecessary coupling to abstractions that provide no value. Adding interfaces before variation exists complicates code without benefit.

# Premature abstraction
class UserRepositoryInterface
  def find(id)
    raise NotImplementedError
  end
  
  def save(user)
    raise NotImplementedError
  end
end

class DatabaseUserRepository < UserRepositoryInterface
  def find(id)
    User.find(id)
  end
  
  def save(user)
    user.save!
  end
end

# Simpler approach until variation appears
class UserRepository
  def find(id)
    User.find(id)
  end
  
  def save(user)
    user.save!
  end
end

Callback coupling creates hidden dependencies through lifecycle hooks. Objects register callbacks with other objects, creating implicit coupling that complicates tracing.

# Callback coupling
class Order
  attr_accessor :status
  
  def initialize
    @callbacks = Hash.new { |h, k| h[k] = [] }
  end
  
  def on_status_change(&block)
    @callbacks[:status_change] << block
  end
  
  def status=(new_status)
    old_status = @status
    @status = new_status
    @callbacks[:status_change].each { |cb| cb.call(old_status, new_status) }
  end
end

# Hidden dependencies throughout codebase
order.on_status_change do |old, new|
  InventoryService.update if new == 'completed'
end

order.on_status_change do |old, new|
  EmailService.notify if new == 'shipped'
end

# Explicit dependencies
class OrderStatusHandler
  def initialize(inventory:, email:)
    @inventory = inventory
    @email = email
  end
  
  def handle_change(order, old_status, new_status)
    @inventory.update if new_status == 'completed'
    @email.notify if new_status == 'shipped'
  end
end

Shared mutable state creates coupling between components that modify the same data:

# Shared mutable state
class GlobalCache
  @@data = {}
  
  def self.set(key, value)
    @@data[key] = value
  end
  
  def self.get(key)
    @@data[key]
  end
end

class ServiceA
  def process
    GlobalCache.set('result', compute)
  end
end

class ServiceB
  def process
    result = GlobalCache.get('result')
    transform(result)
  end
end

# Explicit dependencies
class ServiceA
  def process
    compute
  end
end

class ServiceB
  def process(input)
    transform(input)
  end
end

class Coordinator
  def initialize(service_a:, service_b:)
    @service_a = service_a
    @service_b = service_b
  end
  
  def execute
    result_a = @service_a.process
    @service_b.process(result_a)
  end
end

Temporal coupling occurs when operations must execute in specific sequences due to implicit state dependencies:

# Temporal coupling - order matters
class DocumentProcessor
  def process(document)
    parse(document)
    validate  # Depends on parse being called first
    transform  # Depends on validate being called
    save  # Depends on transform being called
  end
  
  private
  
  def parse(document)
    @parsed_document = Parser.parse(document)
  end
  
  def validate
    raise 'Invalid' unless @parsed_document.valid?
  end
  
  def transform
    @transformed = Transformer.transform(@parsed_document)
  end
  
  def save
    Repository.save(@transformed)
  end
end

# Explicit dependencies
class DocumentProcessor
  def process(document)
    parsed = parse(document)
    validated = validate(parsed)
    transformed = transform(validated)
    save(transformed)
  end
  
  private
  
  def parse(document)
    Parser.parse(document)
  end
  
  def validate(parsed_document)
    raise 'Invalid' unless parsed_document.valid?
    parsed_document
  end
  
  def transform(validated_document)
    Transformer.transform(validated_document)
  end
  
  def save(transformed_document)
    Repository.save(transformed_document)
  end
end

God objects accumulate responsibilities, creating low cohesion and coupling to many other classes:

# God object
class Application
  def start
    # Initialization logic
  end
  
  def process_request(request)
    # Request handling
  end
  
  def authenticate_user(credentials)
    # Authentication logic
  end
  
  def send_email(recipient, message)
    # Email sending
  end
  
  def generate_report(type)
    # Reporting logic
  end
  
  def cleanup
    # Cleanup logic
  end
end

# Separated responsibilities
class Application
  def initialize(
    request_handler:,
    authenticator:,
    email_service:,
    report_generator:
  )
    @request_handler = request_handler
    @authenticator = authenticator
    @email_service = email_service
    @report_generator = report_generator
  end
  
  def start
    # Only initialization logic
  end
  
  def process_request(request)
    @request_handler.handle(request)
  end
end

Reference

Coupling Types

Type Description Impact
Content Module modifies or relies on internal workings of another module Highest coupling, changes ripple through system
Common Modules share global data High coupling, race conditions, testing difficulties
Control One module controls flow of another by passing control flags Medium-high coupling, complicates understanding
Stamp Modules share composite data structures, use only portions Medium coupling, unnecessary dependencies
Data Modules share only primitive data through parameters Low coupling, preferred form
Message Modules communicate through message passing with no shared state Lowest coupling, highest flexibility

Cohesion Types

Type Description Strength
Coincidental Elements have no meaningful relationship Weakest, avoid
Logical Elements perform similar operations on different data Weak, creates confusion
Temporal Elements execute at the same time Weak, mixed concerns
Procedural Elements execute in sequence Medium-weak, processing focus
Communicational Elements operate on same data Medium, data focus
Sequential Output of one element feeds next Medium-strong, workflow focus
Functional All elements contribute to single well-defined task Strongest, preferred

Metrics and Indicators

Metric Measurement Target
Afferent Coupling Number of classes depending on this class Varies by role, stable classes higher
Efferent Coupling Number of classes this class depends on Lower is better
Instability Efferent / (Afferent + Efferent) 0 for stable, 1 for unstable
Lack of Cohesion (LCOM) Number of method pairs not sharing instance variables Lower is better
Cyclomatic Complexity Number of linearly independent paths Below 10 per method

Design Guidelines

Principle Application Benefit
Depend on abstractions, not concretions Use interfaces and dependency injection Enables testing, reduces coupling
Law of Demeter Talk only to immediate neighbors Prevents coupling chains
Single Responsibility One reason to change per class Increases cohesion
Open/Closed Open for extension, closed for modification Reduces modification coupling
Interface Segregation Many client-specific interfaces over one general Reduces interface coupling

Ruby-Specific Considerations

Feature Coupling Impact Recommendation
Open classes Global modifications create coupling Use refinements for scoped changes
Dynamic method definition Hidden dependencies, difficult to trace Document dynamic behavior, prefer explicit methods
Global variables Common coupling across system Avoid, use dependency injection
Class variables Shared mutable state Prefer instance variables or constants
Modules as mixins Multiple inheritance creates coupling Use composition over inheritance where possible

Refactoring Patterns

Pattern Purpose Application
Extract Class Reduce class size, increase cohesion When class has multiple responsibilities
Extract Interface Reduce coupling to implementation When multiple implementations exist or needed for testing
Replace Conditional with Polymorphism Reduce control coupling When conditionals check object types
Introduce Parameter Object Reduce stamp coupling When methods share multiple parameters
Hide Delegate Reduce coupling chains When client navigates multiple associations
Remove Middle Man Reduce unnecessary abstraction When class only delegates to another