CrackedRuby CrackedRuby

Overview

Information hiding separates the public interface of a software module from its internal implementation. The principle dictates that modules should expose only what other parts of the system need to interact with them, while keeping implementation details private and subject to change without affecting external code.

David Parnas introduced information hiding in his 1972 paper "On the Criteria To Be Used in Decomposing Systems into Modules." The principle addresses the problem of software maintenance and evolution by creating boundaries between modules based on design decisions that are likely to change.

The core idea involves two distinct concepts that work together: abstraction and encapsulation. Abstraction defines what a module does by specifying its public interface. Encapsulation hides how the module accomplishes its tasks by restricting access to internal state and implementation details.

Information hiding reduces coupling between modules. When Module A uses Module B, Module A depends only on B's public interface, not its internal workings. Changes to B's implementation do not require changes to A, as long as the interface remains stable.

# Public interface exposes what the module does
class BankAccount
  def initialize(balance)
    @balance = balance
  end
  
  def deposit(amount)
    @balance += amount
  end
  
  def balance
    @balance
  end
end

# Client code depends only on public interface
account = BankAccount.new(1000)
account.deposit(500)
puts account.balance  # => 1500

The principle applies at multiple levels: within classes through method visibility, across classes through interface design, and across modules or packages through public APIs. Each level creates a boundary that protects implementation details from external access.

Key Principles

Information hiding rests on three foundational principles: interface segregation, implementation independence, and access control.

Interface Segregation means exposing the minimal interface necessary for clients to accomplish their tasks. A well-designed interface includes only operations that external code legitimately needs. Every public method represents a commitment to maintain that interface across future versions, so expanding interfaces should be done deliberately.

The interface defines a contract between the module and its clients. This contract specifies what operations are available, what parameters they accept, and what results they produce. The contract says nothing about how operations are implemented internally.

Implementation Independence allows internal implementation to change without affecting external code. A module can modify algorithms, data structures, and internal state management freely, as long as the public interface behavior remains consistent.

Consider a cache implementation. The interface might offer get and set operations. Internally, the cache could use a hash table, an LRU list, a tree structure, or any other mechanism. Clients neither know nor care about the internal structure because they interact exclusively through the interface.

class Cache
  def initialize
    @storage = {}  # Could be replaced with any structure
  end
  
  def get(key)
    @storage[key]
  end
  
  def set(key, value)
    @storage[key] = value
  end
end

Access Control enforces information hiding through language mechanisms that restrict which code can access which parts of a module. Without access control, developers could bypass interfaces and directly manipulate internal state, creating dependencies on implementation details.

Access control operates at compile time or runtime. Static languages typically enforce access control at compile time, preventing code from compiling if it violates access restrictions. Dynamic languages like Ruby enforce access control at runtime, raising errors when code attempts to call restricted methods.

The principle of least privilege applies here: code should have access only to the minimum information and operations it needs. If a method does not need to modify internal state, it should not have access to do so. If external code does not need to call a method, that method should remain private.

Information hiding creates levels of abstraction. Each level exposes an interface to the level above while hiding implementation details. The level below can change its implementation without affecting higher levels, as long as interfaces remain stable.

The principle also enforces the Single Responsibility Principle. When implementation details hide behind interfaces, each module has a clear, focused purpose defined by its public interface. Internal complexity does not leak out to confuse the module's role in the larger system.

Ruby Implementation

Ruby provides three visibility levels for methods: public, private, and protected. These keywords control which code can invoke methods, creating access boundaries within classes.

Public Methods form the class's external interface. Any code with a reference to an object can call its public methods. Public methods define what the class does from an external perspective.

class Document
  def initialize(content)
    @content = content
  end
  
  # Public method - external interface
  def word_count
    count_words(@content)
  end
  
  private
  
  # Private method - internal implementation
  def count_words(text)
    text.split.length
  end
end

doc = Document.new("Hello world from Ruby")
doc.word_count  # => 4
doc.count_words("test")  # NoMethodError: private method called

Private Methods cannot be called with an explicit receiver, even within the same class. This restriction enforces information hiding by preventing both external code and subclasses from depending on private implementation details. Private methods represent internal implementation that could change at any time.

Prior to Ruby 2.7, private methods could not be called with self as the receiver. Ruby 2.7 changed this to allow self.private_method calls, which helps when private methods need to be called from within blocks or when clarity requires explicit receivers.

class Account
  def initialize(balance)
    @balance = balance
    @transaction_log = []
  end
  
  def transfer(amount, recipient)
    return false unless validate_transfer(amount)
    
    deduct(amount)
    recipient.deposit(amount)
    log_transaction("transfer", amount, recipient)
    true
  end
  
  private
  
  def validate_transfer(amount)
    amount > 0 && @balance >= amount
  end
  
  def deduct(amount)
    @balance -= amount
  end
  
  def log_transaction(type, amount, recipient)
    @transaction_log << {
      type: type,
      amount: amount,
      recipient: recipient.object_id,
      timestamp: Time.now
    }
  end
end

Protected Methods occupy a middle ground. Protected methods can be called by any instance of the same class or its subclasses, but not by external code. This visibility level supports scenarios where related objects need to access each other's internal state.

class Employee
  attr_reader :name
  
  def initialize(name, salary)
    @name = name
    @salary = salary
  end
  
  def salary_higher_than?(other_employee)
    salary > other_employee.salary
  end
  
  protected
  
  attr_reader :salary
end

alice = Employee.new("Alice", 75000)
bob = Employee.new("Bob", 80000)

alice.salary_higher_than?(bob)  # => false
alice.salary  # NoMethodError: protected method called

Instance Variables hide automatically in Ruby. No external code can directly access instance variables without explicitly defined accessor methods. This default hiding enforces encapsulation at the data level.

class Configuration
  def initialize
    @settings = {}
    @loaded = false
  end
  
  def get(key)
    load_if_needed
    @settings[key]
  end
  
  def set(key, value)
    load_if_needed
    validate_key(key)
    @settings[key] = value
  end
  
  private
  
  def load_if_needed
    return if @loaded
    
    # Load configuration from file or environment
    @settings = load_from_source
    @loaded = true
  end
  
  def validate_key(key)
    raise ArgumentError, "Invalid key" unless key.is_a?(String)
  end
  
  def load_from_source
    # Implementation detail
    {}
  end
end

Module Encapsulation creates boundaries at a higher level. Ruby modules can group related classes and define module-level private methods that remain hidden from external code.

module PaymentProcessing
  class Transaction
    def initialize(amount)
      @amount = amount
      @id = PaymentProcessing.generate_transaction_id
    end
    
    def process
      return false unless PaymentProcessing.validate_amount(@amount)
      
      # Process transaction
      true
    end
  end
  
  class << self
    def generate_transaction_id
      # Internal implementation
      "TXN-#{Time.now.to_i}-#{rand(10000)}"
    end
    
    def validate_amount(amount)
      amount.is_a?(Numeric) && amount > 0
    end
    
    private :generate_transaction_id, :validate_amount
  end
end

Accessor Methods control how external code accesses internal state. Ruby provides attr_reader, attr_writer, and attr_accessor to generate accessor methods, but custom accessors offer more control.

class Temperature
  def initialize(celsius)
    @celsius = celsius
  end
  
  # Controlled read access with computed value
  def fahrenheit
    @celsius * 9.0 / 5.0 + 32
  end
  
  # Controlled write access with validation
  def celsius=(value)
    raise ArgumentError, "Invalid temperature" if value < -273.15
    @celsius = value
  end
  
  def celsius
    @celsius
  end
end

Class Instance Variables hide information at the class level. Unlike class variables, class instance variables do not share across inheritance hierarchies, providing better encapsulation for class-level state.

class Counter
  class << self
    attr_reader :count
    
    def initialize_counter
      @count = 0
    end
    
    def increment
      @count += 1
    end
    
    private :initialize_counter
  end
  
  initialize_counter
end

Practical Examples

Information hiding appears in scenarios ranging from simple data validation to complex system architectures. These examples demonstrate how the principle applies across different problem domains.

Database Connection Management hides connection pooling, query optimization, and connection lifecycle from application code.

class DatabaseManager
  def initialize(config)
    @config = config
    @connection_pool = []
    @max_connections = config[:max_connections] || 5
    initialize_pool
  end
  
  def execute_query(sql, params = [])
    connection = acquire_connection
    begin
      result = connection.execute(sql, params)
      process_result(result)
    ensure
      release_connection(connection)
    end
  end
  
  def transaction(&block)
    connection = acquire_connection
    begin
      connection.begin_transaction
      result = block.call(connection)
      connection.commit
      result
    rescue => e
      connection.rollback
      raise e
    ensure
      release_connection(connection)
    end
  end
  
  private
  
  def initialize_pool
    @max_connections.times do
      @connection_pool << create_connection
    end
  end
  
  def create_connection
    # Connection creation implementation
    Connection.new(@config)
  end
  
  def acquire_connection
    # Connection pool management logic
    @connection_pool.pop || create_connection
  end
  
  def release_connection(connection)
    @connection_pool.push(connection) if @connection_pool.size < @max_connections
  end
  
  def process_result(result)
    # Result processing and transformation
    result.map(&:to_h)
  end
end

Authentication System hides password hashing, token generation, and session management details from application controllers.

class AuthenticationService
  def initialize
    @sessions = {}
    @failed_attempts = {}
  end
  
  def authenticate(username, password)
    return nil if account_locked?(username)
    
    user = find_user(username)
    return handle_failed_attempt(username) unless user
    
    return handle_failed_attempt(username) unless verify_password(user, password)
    
    clear_failed_attempts(username)
    create_session(user)
  end
  
  def validate_session(token)
    session = @sessions[token]
    return nil unless session
    return nil if session_expired?(session)
    
    refresh_session(token)
    session[:user]
  end
  
  def logout(token)
    @sessions.delete(token)
  end
  
  private
  
  def find_user(username)
    # User lookup implementation
    User.find_by(username: username)
  end
  
  def verify_password(user, password)
    BCrypt::Password.new(user.password_hash) == password
  end
  
  def create_session(user)
    token = generate_secure_token
    @sessions[token] = {
      user: user,
      created_at: Time.now,
      last_accessed: Time.now
    }
    token
  end
  
  def generate_secure_token
    SecureRandom.hex(32)
  end
  
  def session_expired?(session)
    Time.now - session[:last_accessed] > 3600
  end
  
  def refresh_session(token)
    @sessions[token][:last_accessed] = Time.now
  end
  
  def account_locked?(username)
    attempts = @failed_attempts[username]
    attempts && attempts[:count] >= 5 && 
      Time.now - attempts[:last_attempt] < 900
  end
  
  def handle_failed_attempt(username)
    @failed_attempts[username] ||= { count: 0, last_attempt: Time.now }
    @failed_attempts[username][:count] += 1
    @failed_attempts[username][:last_attempt] = Time.now
    nil
  end
  
  def clear_failed_attempts(username)
    @failed_attempts.delete(username)
  end
end

File Processing Pipeline hides format detection, encoding handling, and transformation logic from client code.

class FileProcessor
  def initialize
    @processors = register_processors
  end
  
  def process_file(filepath)
    content = read_file(filepath)
    format = detect_format(filepath, content)
    processor = select_processor(format)
    
    processor.process(content)
  end
  
  private
  
  def read_file(filepath)
    encoding = detect_encoding(filepath)
    File.read(filepath, encoding: encoding)
  end
  
  def detect_encoding(filepath)
    sample = File.read(filepath, 1024, mode: 'rb')
    
    return 'UTF-8' if sample.force_encoding('UTF-8').valid_encoding?
    return 'ISO-8859-1' if sample.force_encoding('ISO-8859-1').valid_encoding?
    
    'ASCII-8BIT'
  end
  
  def detect_format(filepath, content)
    return :json if filepath.end_with?('.json')
    return :xml if filepath.end_with?('.xml')
    return :csv if filepath.end_with?('.csv')
    
    detect_format_from_content(content)
  end
  
  def detect_format_from_content(content)
    return :json if content.strip.start_with?('{', '[')
    return :xml if content.strip.start_with?('<')
    
    :text
  end
  
  def select_processor(format)
    @processors[format] || @processors[:text]
  end
  
  def register_processors
    {
      json: JSONProcessor.new,
      xml: XMLProcessor.new,
      csv: CSVProcessor.new,
      text: TextProcessor.new
    }
  end
end

Cache Implementation hides eviction policies, storage mechanisms, and cache warming strategies.

class LRUCache
  def initialize(capacity)
    @capacity = capacity
    @cache = {}
    @access_order = []
  end
  
  def get(key)
    return nil unless @cache.key?(key)
    
    update_access_order(key)
    @cache[key]
  end
  
  def set(key, value)
    if @cache.key?(key)
      @cache[key] = value
      update_access_order(key)
    else
      evict_if_needed
      @cache[key] = value
      @access_order.push(key)
    end
  end
  
  def size
    @cache.size
  end
  
  private
  
  def update_access_order(key)
    @access_order.delete(key)
    @access_order.push(key)
  end
  
  def evict_if_needed
    return if @cache.size < @capacity
    
    lru_key = @access_order.shift
    @cache.delete(lru_key)
  end
end

Design Considerations

Information hiding requires careful interface design. The public interface determines how flexible the implementation can be while maintaining compatibility with client code.

Interface Stability versus flexibility represents the central trade-off. Stable interfaces minimize breaking changes but constrain implementation options. Flexible interfaces accommodate evolution but require more frequent client updates.

Design interfaces based on what operations clients need to perform, not based on current implementation details. An interface designed around implementation tends to expose methods that make sense for the internal data structures but not for client needs.

# Implementation-oriented interface (problematic)
class UserRegistry
  def get_hash_table
    @users
  end
  
  def set_hash_table(table)
    @users = table
  end
end

# Task-oriented interface (better)
class UserRegistry
  def find_user(id)
    @users[id]
  end
  
  def register_user(user)
    @users[user.id] = user
  end
  
  def all_users
    @users.values
  end
end

Granularity affects both usability and flexibility. Fine-grained interfaces expose many small methods, each doing one specific thing. Coarse-grained interfaces expose fewer methods that do more work. Fine-grained interfaces offer more flexibility but require more method calls to accomplish tasks. Coarse-grained interfaces simplify client code but may not fit all use cases.

The appropriate granularity depends on the module's role. Low-level utilities benefit from fine-grained interfaces that compose into complex operations. High-level services benefit from coarse-grained interfaces that encapsulate common workflows.

Information Leakage occurs when implementation details inadvertently appear in the public interface. Common sources include returning internal data structures, accepting internal representations as parameters, or exposing error conditions tied to implementation choices.

# Leaks implementation (array internals exposed)
class TaskQueue
  def tasks
    @tasks  # Returns internal array - clients could modify it
  end
end

# Hides implementation
class TaskQueue
  def tasks
    @tasks.dup  # Returns copy, protects internal state
  end
  
  def each_task(&block)
    @tasks.each(&block)  # Controlled iteration
  end
end

Versioning Strategies help evolve interfaces without breaking client code. Add new methods while maintaining old ones, mark deprecated methods clearly, and provide migration paths when removing functionality.

class PaymentGateway
  def process_payment(amount, card_number)
    warn "process_payment is deprecated, use process_payment_secure instead"
    process_payment_secure(amount, card_number, {})
  end
  
  def process_payment_secure(amount, card_number, options = {})
    # New implementation with additional security options
  end
end

Dependency Injection supports information hiding by allowing implementations to be substituted without changing client code. Rather than creating dependencies internally, modules accept them through constructors or setters.

class OrderProcessor
  def initialize(payment_gateway, inventory_service, notification_service)
    @payment_gateway = payment_gateway
    @inventory_service = inventory_service
    @notification_service = notification_service
  end
  
  def process_order(order)
    return false unless @inventory_service.reserve_items(order.items)
    
    payment_result = @payment_gateway.charge(order.amount, order.payment_info)
    return false unless payment_result.success?
    
    @notification_service.send_confirmation(order.customer_email)
    true
  end
end

Interface Segregation Principle suggests splitting large interfaces into smaller, focused interfaces. Clients should not depend on methods they do not use. Large interfaces couple clients to functionality they do not need, making changes more difficult.

Law of Demeter guides information hiding at the method call level. A method should only call methods on objects it directly knows about: its parameters, objects it creates, its instance variables, and globally available objects. Following this law prevents reaching through objects to access their internal structure.

Common Patterns

Several established patterns implement information hiding across different scenarios. Each pattern addresses specific needs while maintaining encapsulation boundaries.

Facade Pattern provides a simplified interface to a complex subsystem. The facade hides the subsystem's internal complexity behind a cohesive interface tailored to common use cases.

module MediaLibrary
  class Facade
    def initialize
      @file_reader = FileReader.new
      @decoder = Decoder.new
      @audio_mixer = AudioMixer.new
      @video_processor = VideoProcessor.new
    end
    
    def play_media(filepath)
      data = @file_reader.read(filepath)
      format = @file_reader.detect_format(filepath)
      
      if audio_format?(format)
        play_audio(data, format)
      elsif video_format?(format)
        play_video(data, format)
      else
        raise "Unsupported format: #{format}"
      end
    end
    
    private
    
    def play_audio(data, format)
      decoded = @decoder.decode_audio(data, format)
      @audio_mixer.play(decoded)
    end
    
    def play_video(data, format)
      decoded = @decoder.decode_video(data, format)
      audio_track = @decoder.extract_audio(decoded)
      video_track = @decoder.extract_video(decoded)
      
      @audio_mixer.play(audio_track)
      @video_processor.display(video_track)
    end
    
    def audio_format?(format)
      [:mp3, :wav, :flac].include?(format)
    end
    
    def video_format?(format)
      [:mp4, :avi, :mkv].include?(format)
    end
  end
end

Strategy Pattern hides algorithm implementation behind a common interface. Clients work with strategies through the interface without knowing implementation details.

class DataExporter
  def initialize(strategy)
    @strategy = strategy
  end
  
  def export(data)
    @strategy.export(data)
  end
end

class JSONExportStrategy
  def export(data)
    require 'json'
    JSON.generate(data)
  end
end

class XMLExportStrategy
  def export(data)
    build_xml_document(data)
  end
  
  private
  
  def build_xml_document(data)
    # XML building implementation
  end
end

class CSVExportStrategy
  def export(data)
    require 'csv'
    CSV.generate do |csv|
      data.each { |row| csv << row }
    end
  end
end

Template Method Pattern hides variation points in an algorithm while exposing the overall structure. Subclasses override specific steps without changing the algorithm's framework.

class ReportGenerator
  def generate_report(data)
    prepare_data(data)
    formatted = format_report(data)
    add_header(formatted)
    add_footer(formatted)
    finalize_report(formatted)
  end
  
  private
  
  def prepare_data(data)
    # Common preparation logic
  end
  
  def format_report(data)
    raise NotImplementedError, "Subclasses must implement format_report"
  end
  
  def add_header(formatted)
    # Optional hook - default implementation
  end
  
  def add_footer(formatted)
    # Optional hook - default implementation
  end
  
  def finalize_report(formatted)
    formatted
  end
end

class PDFReportGenerator < ReportGenerator
  private
  
  def format_report(data)
    # PDF-specific formatting
  end
  
  def add_header(formatted)
    # Add PDF header
  end
end

Adapter Pattern hides incompatible interfaces behind a consistent interface. The adapter translates between the interface clients expect and the interface the adapted class provides.

class ModernPaymentGateway
  def process(amount, token)
    # New API implementation
  end
end

class LegacyPaymentGateway
  def charge_card(card_details, amount_cents)
    # Old API implementation
  end
end

class PaymentGatewayAdapter
  def initialize(gateway)
    @gateway = gateway
  end
  
  def process_payment(amount, payment_info)
    if @gateway.respond_to?(:process)
      @gateway.process(amount, payment_info[:token])
    else
      amount_cents = (amount * 100).to_i
      @gateway.charge_card(payment_info[:card_details], amount_cents)
    end
  end
end

Builder Pattern hides complex object construction behind a fluent interface. The builder manages construction steps and validation internally.

class QueryBuilder
  def initialize
    @table = nil
    @columns = []
    @conditions = []
    @joins = []
    @order_by = []
  end
  
  def from(table)
    @table = table
    self
  end
  
  def select(*columns)
    @columns.concat(columns)
    self
  end
  
  def where(condition)
    @conditions << condition
    self
  end
  
  def join(table, condition)
    @joins << { table: table, condition: condition }
    self
  end
  
  def order_by(column, direction = :asc)
    @order_by << { column: column, direction: direction }
    self
  end
  
  def build
    validate_query
    construct_sql
  end
  
  private
  
  def validate_query
    raise "Table name required" unless @table
  end
  
  def construct_sql
    sql = "SELECT #{build_select_clause} FROM #{@table}"
    sql += build_joins_clause if @joins.any?
    sql += build_where_clause if @conditions.any?
    sql += build_order_clause if @order_by.any?
    sql
  end
  
  def build_select_clause
    @columns.empty? ? '*' : @columns.join(', ')
  end
  
  def build_joins_clause
    @joins.map { |j| " JOIN #{j[:table]} ON #{j[:condition]}" }.join
  end
  
  def build_where_clause
    " WHERE " + @conditions.join(' AND ')
  end
  
  def build_order_clause
    order_parts = @order_by.map { |o| "#{o[:column]} #{o[:direction].upcase}" }
    " ORDER BY " + order_parts.join(', ')
  end
end

Common Pitfalls

Information hiding fails when implementation details leak through interfaces or when access control mechanisms are misused.

Returning Mutable Internal State breaks encapsulation by allowing external code to modify private data structures.

# Problematic: Returns internal array reference
class TodoList
  def initialize
    @items = []
  end
  
  def items
    @items  # External code can call @items.clear
  end
end

# Fixed: Returns defensive copy
class TodoList
  def initialize
    @items = []
  end
  
  def items
    @items.dup
  end
  
  def each_item(&block)
    @items.each(&block)
  end
end

Accepting Mutable Parameters creates similar problems when storing references to objects that external code retains and modifies.

# Problematic: Stores reference to external array
class Configuration
  def initialize(settings)
    @settings = settings  # External code keeps reference
  end
end

# Fixed: Creates defensive copy
class Configuration
  def initialize(settings)
    @settings = settings.dup
  end
end

Overusing Public Methods creates large interfaces that constrain future changes. Methods become public by default in Ruby, leading developers to leave methods public unnecessarily.

# Problematic: Everything public
class DataProcessor
  def initialize
    @cache = {}
  end
  
  def process(data)
    cached = lookup_cache(data)
    return cached if cached
    
    result = perform_calculation(data)
    store_cache(data, result)
    result
  end
  
  def lookup_cache(data)
    @cache[data]
  end
  
  def perform_calculation(data)
    # Complex calculation
  end
  
  def store_cache(data, result)
    @cache[data] = result
  end
end

# Fixed: Minimal public interface
class DataProcessor
  def initialize
    @cache = {}
  end
  
  def process(data)
    cached = lookup_cache(data)
    return cached if cached
    
    result = perform_calculation(data)
    store_cache(data, result)
    result
  end
  
  private
  
  def lookup_cache(data)
    @cache[data]
  end
  
  def perform_calculation(data)
    # Complex calculation
  end
  
  def store_cache(data, result)
    @cache[data] = result
  end
end

Exposing Implementation Types couples clients to internal representations. Public methods that return or accept implementation-specific types create dependencies on those types.

# Problematic: Exposes hash structure
class UserDatabase
  def get_user_record(id)
    @users[id]  # Returns hash with specific structure
  end
end

# Fixed: Returns domain object
class UserDatabase
  def find_user(id)
    data = @users[id]
    return nil unless data
    
    User.new(data)
  end
end

Misunderstanding Protected leads to confusion about visibility. Protected methods can be called on other instances of the same class, which sometimes exposes more than intended.

class BankAccount
  def initialize(balance)
    @balance = balance
  end
  
  def transfer_to(recipient, amount)
    return false if @balance < amount
    
    @balance -= amount
    recipient.receive_deposit(amount)  # Can call protected method
    true
  end
  
  protected
  
  def receive_deposit(amount)
    @balance += amount
  end
end

Testing Private Methods Directly creates dependencies on implementation details in tests. Tests should verify behavior through public interfaces, not test private methods directly.

# Problematic: Tests private implementation
class CalculatorTest < Minitest::Test
  def test_internal_calculation
    calc = Calculator.new
    result = calc.send(:internal_calculation, 5)  # Testing private method
    assert_equal 25, result
  end
end

# Fixed: Tests public behavior
class CalculatorTest < Minitest::Test
  def test_calculation_result
    calc = Calculator.new
    result = calc.calculate(5)
    assert_equal 25, result
  end
end

Getter/Setter Proliferation defeats encapsulation when every instance variable gets automatic accessor methods. This creates no real boundary between external and internal representation.

# Problematic: No real encapsulation
class Rectangle
  attr_accessor :width, :height, :x, :y, :color, :border_width
end

# Better: Controlled interface
class Rectangle
  def initialize(x, y, width, height)
    @x = x
    @y = y
    @width = width
    @height = height
    @color = :black
    @border_width = 1
  end
  
  def move(dx, dy)
    @x += dx
    @y += dy
  end
  
  def resize(new_width, new_height)
    @width = new_width if new_width > 0
    @height = new_height if new_height > 0
  end
  
  def area
    @width * @height
  end
end

Reference

Ruby Method Visibility

Visibility Level Accessible From Use Case
public Any code with object reference External interface, public API
private Same instance only Internal implementation, helper methods
protected Same class and subclasses Inter-instance communication, template methods

Access Control Syntax

Syntax Pattern Effect Example
private All following methods private private
private :method_name Specific method private private :calculate
private def method_name Method definition private private def calculate

Information Hiding Checklist

Aspect Guideline
Instance Variables Never expose directly, use accessor methods
Public Methods Keep minimal, each method is long-term commitment
Private Methods Mark explicitly, can change freely
Return Values Return copies or immutable objects for internal state
Parameters Copy mutable parameters before storing
Error Messages Avoid exposing internal structure details
Method Names Describe what, not how

Common Interface Patterns

Pattern Purpose Visibility
Query Methods Return information without side effects Public
Command Methods Modify state, return success indication Public
Factory Methods Create and configure objects Public
Template Methods Define algorithm structure Public/Protected
Helper Methods Support public interface Private
Validation Methods Check preconditions Private
Callback Methods Hook points for subclasses Protected

Design Guidelines

Principle Implementation
Minimal Interface Expose only essential operations
Stable Contracts Public interface should rarely change
Implementation Hiding Private methods can change freely
Type Abstraction Return domain objects, not data structures
Defensive Copying Copy mutable data crossing boundaries
Single Responsibility Each public method has one clear purpose
Fail Fast Validate parameters in public methods

Encapsulation Strategies

Strategy Application Benefit
Value Objects Wrap primitive types Type safety, validation
Builder Pattern Complex construction Hide construction complexity
Facade Subsystem access Simplify complex interfaces
Strategy Algorithm variation Hide algorithm implementation
Template Method Algorithm structure Control variation points
Adapter Interface translation Hide incompatible interfaces