6

State, Validation, Modules, and Composition

Ruby source files are converted directly from the English source tree.

en/topic_06_classes_modules_composition

Overview

Why this topic matters ๐Ÿ’ก

This is where students begin designing small cooperating objects instead of isolated functions. The topic introduces internal state, validation, reusable module behavior, and dependency injection through composition.

Learning outcomes ๐ŸŽฏ

By the end of this topic, students should be able to:

  • use attr_reader to expose state intentionally;
  • protect object invariants with private validation methods;
  • explain the role of modules for shared behavior;
  • use composition and injected collaborators to keep classes focused;
  • compare composition with more rigid inheritance-heavy designs.

Assessment focus โœ…

Students should be able to explain why the object owns its state and why the calculator dependency is injected rather than created internally.

Short Note

Ruby's object model supports very small classes that still do meaningful work. Students should learn that a class is not justified by size alone; it is justified by responsibility, state, and collaboration boundaries.

This topic matters because it introduces two powerful habits:

  • keep validation close to the state it protects;
  • inject collaborators instead of hard-coding them.

Ruby beauty in this topic:

  • state and behavior can live together without boilerplate;
  • private helpers express invariants cleanly;
  • composition makes testing easier and design more modular.

Ruby caution in this topic:

  • modules can hide too much if mixed in carelessly;
  • tiny classes are only helpful when their responsibilities are clear.

Reflection prompt:

  • Why is dependency injection useful even in a small exercise?

Worked Examples

Example 1: Wallet or account balance ๐Ÿ’ก

A balance-holding object is a strong teaching example because it has real state and clear invariants.

Questions students should ask:

  • who owns the balance?
  • what counts as invalid input?
  • where should validation live?

Example 2: Checkout with injected calculator ๐Ÿ’ก

Checkout logic is often composed from pricing rules, discount engines, tax services, or promotional logic. That makes it a natural composition example.

checkout = Checkout.new(calculator: calculator)
checkout.final_total(cart)
worked_examples.md
ruby

Why this is useful:

  • the checkout object does not need to know how totals are computed;
  • tests can replace the calculator with a double;
  • the design stays open to new pricing strategies.

Cheatsheet

Reader and state

attr_reader :balance
cheatsheet.md
ruby

Private validation

private

def validate_positive!(amount)
  raise ArgumentError unless amount.positive?
end
cheatsheet.md
ruby

Constructor injection

def initialize(calculator:)
  @calculator = calculator
end
cheatsheet.md
ruby

Design reminders โš–๏ธ

  • Let the object protect its own invariants.
  • Inject collaborators when behavior may vary or needs isolated testing.

Study Guide

Topic purpose ๐ŸŽฏ

Learn how Ruby objects manage state and collaborate with other objects. The goal is to design small, testable objects rather than write procedural code inside classes.

Study sequence ๐Ÿชœ

  1. Read overview.md.
  2. Read shortnote.md.
  3. Study worked_examples.md before reviewing the code.
  4. Use cheatsheet.md while reading example.rb.
  5. Complete the wallet exercise.
  6. Complete the checkout exercise with an injected collaborator.

What to notice ๐Ÿ”Ž

  • State changes should remain controlled by the owning object.
  • Private validation methods communicate invariants clearly.
  • Composition reduces coupling and improves testability.

Reflection questions ๐Ÿค”

  • Why should Wallet validate its own amounts?
  • Why is Checkout cleaner when it depends on calculator: instead of creating one?
  • When is a module the right reuse mechanism, and when is it too implicit?

Source Files and Tests

Ruby source files are converted directly from the English source tree.

# EXAMPLE CODE
# Topic: topic_06_classes_modules_composition
#
# Purpose:
# - This file demonstrates reference implementation for the concept.
# - It should pass tests from the beginning.
# - Read and understand it before solving exercises.

class BankAccount
  attr_reader :balance

  def initialize(balance: 0)
    @balance = balance
  end

  def deposit(amount)
    validate_amount!(amount)
    @balance += amount
  end

  private

  def validate_amount!(amount)
    raise ArgumentError, "amount must be positive" unless amount.positive?
  end
end
en/topic_06_classes_modules_composition/example.rb
Ruby course source
# STUDENT TASK (BASIC)
# Topic: topic_06_classes_modules_composition
#
# What to do:
# - Implement or improve the class/methods in this file.
# - Read tests in tests/topic_06_classes_modules_composition_spec.rb under the "basic exercise" examples.
# - Make tests pass without breaking the example/advanced sections.
#
# Expected outcome:
# - You can run this topic tests and see all examples green after implementation.

class Wallet
  attr_reader :balance

  def initialize(balance: 0)
    @balance = balance
  end

  def top_up(amount)
    validate_positive!(amount)
    @balance += amount
  end

  def spend(amount)
    validate_positive!(amount)
    raise ArgumentError, "insufficient balance" if amount > @balance

    @balance -= amount
  end

  private

  def validate_positive!(amount)
    raise ArgumentError, "amount must be positive" unless amount.positive?
  end
end
en/topic_06_classes_modules_composition/basic_exercise.rb
Ruby course source
# STUDENT TASK (ADVANCED)
# Topic: topic_06_classes_modules_composition
#
# Academic purpose:
# - Learn composition as a design choice, not just constructor syntax.
# - See how injected collaborators create simpler, more testable classes.
#
# Real-world use case:
# - Checkout systems rarely calculate totals by themselves.
# - Real applications may delegate to tax calculators, discount engines, or pricing services.
# - Injecting a calculator keeps checkout focused on orchestration rather than pricing policy.
#
# Why Ruby is beautiful here:
# - Keyword-based dependency injection is compact and explicit.
# - The collaborator contract can remain small: in this case, just `total(cart)`.
# - Tests can substitute a double easily, making the design discussion visible.
#
# What to do:
# - Complete the challenge behavior requested by the guide.
# - Focus on why composition improves the design, not only how dependency injection works.
# - Use tests in tests/topic_06_classes_modules_composition_spec.rb under the "advanced exercise" examples.
#
# Expected outcome:
# - Advanced tests pass and you can explain the value of injected collaborators.

class Checkout
  def initialize(calculator:)
    @calculator = calculator
  end

  def final_total(cart)
    @calculator.total(cart)
  end
end
en/topic_06_classes_modules_composition/adv_exercise.rb
Ruby course source
# ANSWER KEY (BASIC)
# Topic: topic_06_classes_modules_composition
#
# Solution idea:
# - The wallet owns the balance state.
# - Both top-up and spend operations reuse the same positivity validation.
# - Spending also checks that the wallet has enough balance before mutating state.

class Wallet
  attr_reader :balance

  def initialize(balance: 0)
    @balance = balance
  end

  def top_up(amount)
    validate_positive!(amount)
    @balance += amount
  end

  def spend(amount)
    validate_positive!(amount)
    raise ArgumentError, "insufficient balance" if amount > @balance

    @balance -= amount
  end

  private

  def validate_positive!(amount)
    raise ArgumentError, "amount must be positive" unless amount.positive?
  end
end
en/topic_06_classes_modules_composition/answer_basic_exercise.rb
Ruby course source
# ANSWER KEY (ADVANCED)
# Topic: topic_06_classes_modules_composition
#
# Solution idea:
# - `Checkout` should not calculate prices by itself.
# - Inject the calculator so checkout stays responsible only for orchestration.
# - The method simply delegates to the collaborator's contract.

class Checkout
  def initialize(calculator:)
    @calculator = calculator
  end

  def final_total(cart)
    @calculator.total(cart)
  end
end
en/topic_06_classes_modules_composition/answer_adv_exercise.rb
Ruby course source
# This spec is your learning companion for topic_06_classes_modules_composition.
#
# How to use this file:
# 1) Run tests and observe failures/successes.
# 2) Keep EXAMPLE specs green from the beginning.
# 3) Implement BASIC exercise until BASIC specs pass.
# 4) Implement ADVANCED exercise and pass edge cases.
#
# Expected final result:
# - All examples in this file pass.
# - You understand both the concept and the implementation tradeoffs.

require_relative "../example"
require_relative "../basic_exercise"
require_relative "../adv_exercise"

RSpec.describe "topic_06_classes_modules_composition" do
  describe "EXAMPLE purpose: understand the reference implementation" do
      it "manages bank account deposits" do
        a = BankAccount.new(balance: 10)
        expect(a.deposit(5)).to eq(15)
        expect { a.deposit(0) }.to raise_error(ArgumentError)
      end
  end

  describe "BASIC EXERCISE purpose: implement the comparable task" do
      it "supports wallet top up and spend validations" do
        w = Wallet.new(balance: 20)
  end

  describe "ADVANCED EXERCISE purpose: solve challenge and edge cases" do
      it "uses composition with injected calculator" do
        calculator = instance_double("PriceCalculator", total: 120)
        checkout = Checkout.new(calculator: calculator)
        expect(checkout.final_total([:item])).to eq(120)
      end
    end
  end
end
en/topic_06_classes_modules_composition/tests/topic_06_classes_modules_composition_spec.rb
Ruby course source
#!/usr/bin/env bash
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"

exec "${ROOT_DIR}/run_tests.sh" "$(basename "${SCRIPT_DIR}")"
en/topic_06_classes_modules_composition/run_topic_tests.sh
Ruby course source

Study Prompts

  1. Read the spec first and identify which behaviors belong to the example, basic exercise, and advanced exercise.

  2. Attempt the exercises before opening the answer files, then compare the design choices.