9

Mini Capstone in Domain Modeling

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

en/topic_09_mini_capstone

Overview

Why this topic matters ๐Ÿ’ก

The capstone exists to gather the course ideas into one small domain. Students move from isolated features to a coherent miniature system with domain objects, state changes, validation, collaboration, and export behavior.

Learning outcomes ๐ŸŽฏ

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

  • model a small domain with objects that have distinct responsibilities;
  • preserve domain rules such as borrowing limits;
  • compose services around existing domain objects instead of collapsing everything into one class;
  • use a duck-typed exporter to separate representation from domain logic;
  • explain how the capstone demonstrates the broader Ruby style taught across the course.

Assessment focus โœ…

Students should be able to explain why the model is split across Book, Member, Library, and LibraryReport.

Short Note

A capstone should not be large. It should be large enough to reveal whether the student can connect the ideas from earlier topics:

  • clear object responsibilities;
  • validation and domain rules;
  • readable state changes;
  • collaboration through composition;
  • extension through duck-typed exporters.

Ruby beauty in this topic:

  • small objects can model a meaningful workflow clearly;
  • business rules can stay close to the objects they belong to;
  • alternate output behavior can be added without rewriting the core domain.

Ruby caution in this topic:

  • tiny domains still become messy if responsibility boundaries are weak;
  • "simple" code becomes procedural quickly if everything is pushed into one class.

Reflection prompt:

  • Which object owns which rule, and why?

Worked Examples

Example 1: Borrowing rule on the member ๐Ÿ’ก

The borrowing limit belongs to Member, not to a controller-like script, because it is part of the member's domain constraint.

This is a useful teaching decision:

  • the rule stays close to the state it protects;
  • the object communicates responsibility clearly.

Example 2: Exporting lending state ๐Ÿ’ก

The library may need to expose lending state in multiple forms:

  • a hash for internal use;
  • JSON for an API;
  • CSV for operations reporting.

That makes the exporter boundary realistic and shows the payoff from Topic 7.

Cheatsheet

Domain objects

  • Book: data about a lendable item
  • Member: borrowing rule and borrowed collection
  • Library: inventory and lending coordination
  • LibraryReport: representation/export boundary

Collaboration pattern

lib = Library.new
member = Member.new
lib.lend("isbn-1", member)
cheatsheet.md
ruby

Export boundary

class HashExporter
  def export(state)
    state
  end
end
cheatsheet.md
ruby

Design reminders โš–๏ธ

  • Put rules close to the object that owns them.
  • Keep reporting/output logic outside the core domain workflow.

Study Guide

Topic purpose ๐ŸŽฏ

Use a small lending domain to consolidate the course. This topic is less about new syntax and more about whether the student can design a clean Ruby model.

Study sequence ๐Ÿชœ

  1. Read overview.md.
  2. Read shortnote.md.
  3. Study worked_examples.md and identify where previous topics reappear.
  4. Keep cheatsheet.md open while reading example.rb and basic_exercise.rb.
  5. Complete the library workflow exercise.
  6. Complete the reporting/export exercise.

What to notice ๐Ÿ”Ž

  • Domain rules should live in the object that owns them.
  • Coordination code should remain separate from representation code.
  • The exporter boundary makes the domain open to new outputs without distortion.

Reflection questions ๐Ÿค”

  • Why is the borrow limit enforced in Member rather than Library?
  • What earlier topics reappear in the report exporter?
  • If this capstone grew larger, where would you expect the next abstraction boundary?

Source Files and Tests

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

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

class Book
  attr_reader :title, :author, :isbn

  def initialize(title:, author:, isbn:)
    @title = title
    @author = author
    @isbn = isbn
  end
end

class Member
  MAX_BORROW = 3
  attr_reader :borrowed

  def initialize
    @borrowed = []
  end

  def borrow(book)
    raise ArgumentError, "borrow limit reached" if @borrowed.size >= MAX_BORROW

    @borrowed << book
  end

  def return_book(book)
    @borrowed.delete(book)
  end
end
en/topic_09_mini_capstone/example.rb
Ruby course source
# STUDENT TASK (BASIC)
# Topic: topic_09_mini_capstone
#
# What to do:
# - Implement or improve the class/methods in this file.
# - Read tests in tests/topic_09_mini_capstone_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 Library
  def initialize
    @books = []
    @lent = {}
  end

  def add_book(book)
    @books << book
  end

  def lend(isbn, member)
    return false if @lent.key?(isbn)

    book = @books.find { |b| b.isbn == isbn }
    return false unless book

    member.borrow(book)
    @lent[isbn] = member
    true
  end

  def return_book(isbn, member)
    return false unless @lent[isbn] == member

    book = @books.find { |b| b.isbn == isbn }
    member.return_book(book)
    @lent.delete(isbn)
    true
  end

  def lending_state
    @lent.transform_values { |member| member.object_id }
  end
end
en/topic_09_mini_capstone/basic_exercise.rb
Ruby course source
# STUDENT TASK (ADVANCED)
# Topic: topic_09_mini_capstone
#
# Academic purpose:
# - Consolidate the course by separating domain behavior from output behavior.
# - Show that even a small domain benefits from composition and duck typing.
#
# Real-world use case:
# - Library, inventory, and lending systems often need multiple reports for operators, APIs,
#   and downstream systems.
# - Exporters are useful because the core lending model should not care whether the output is a
#   hash, JSON document, CSV file, or some future integration payload.
# - This mirrors a common production design boundary.
#
# Why Ruby is beautiful here:
# - The domain model stays small and readable.
# - The report object depends only on a tiny exporter contract.
# - Earlier course ideas combine naturally rather than feeling isolated.
#
# What to do:
# - Complete the challenge behavior requested by the guide.
# - Be able to explain how this file reuses ideas from composition and duck typing.
# - Use tests in tests/topic_09_mini_capstone_spec.rb under the "advanced exercise" examples.
#
# Expected outcome:
# - Advanced tests pass and you can describe the architecture, not only the syntax.

class HashExporter
  def export(state)
    state
  end
end

class LibraryReport
  def initialize(library:, exporter:)
    @library = library
    @exporter = exporter
  end

  def call
    @exporter.export(@library.lending_state)
  end
end
en/topic_09_mini_capstone/adv_exercise.rb
Ruby course source
# ANSWER KEY (BASIC)
# Topic: topic_09_mini_capstone
#
# Solution idea:
# - `Library` coordinates inventory and lending state.
# - Lending succeeds only if the book exists and is not already lent out.
# - Returning succeeds only when the same member currently holds the book.
# - `lending_state` exposes a simple reporting-friendly view of the current loans.

class Library
  def initialize
    @books = []
    @lent = {}
  end

  def add_book(book)
    @books << book
  end

  def lend(isbn, member)
    return false if @lent.key?(isbn)

    book = @books.find { |candidate| candidate.isbn == isbn }
    return false unless book

    member.borrow(book)
    @lent[isbn] = member
    true
  end

  def return_book(isbn, member)
    return false unless @lent[isbn] == member

    book = @books.find { |candidate| candidate.isbn == isbn }
    member.return_book(book)
    @lent.delete(isbn)
    true
  end

  def lending_state
    @lent.transform_values { |member| member.object_id }
  end
end
en/topic_09_mini_capstone/answer_basic_exercise.rb
Ruby course source
# ANSWER KEY (ADVANCED)
# Topic: topic_09_mini_capstone
#
# Solution idea:
# - Keep reporting outside the core `Library` domain object.
# - The exporter is duck-typed: any object with `export(state)` can be used.
# - This keeps the domain model open to future output formats without changing the lending logic.

class HashExporter
  def export(state)
    state
  end
end

class LibraryReport
  def initialize(library:, exporter:)
    @library = library
    @exporter = exporter
  end

  def call
    @exporter.export(@library.lending_state)
  end
end
en/topic_09_mini_capstone/answer_adv_exercise.rb
Ruby course source
# This spec is your learning companion for topic_09_mini_capstone.
#
# 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_09_mini_capstone" do
  describe "EXAMPLE purpose: understand the reference implementation" do
    it "creates books and enforces member borrow limits" do
      member = Member.new
      3.times { |i| member.borrow(Book.new(title: "T#{i}", author: "A", isbn: i.to_s)) }
      expect { member.borrow(Book.new(title: "Overflow", author: "A", isbn: "x")) }.to raise_error(ArgumentError, /limit/)
    end
  end

  describe "BASIC EXERCISE purpose: implement the comparable task" do
    it "lends and returns books through library" do
      lib = Library.new
      member = Member.new
      book = Book.new(title: "Practical Ruby", author: "A", isbn: "isbn-1")

      lib.add_book(book)

      expect(lib.lend("isbn-1", member)).to eq(true)
      expect(member.borrowed).to eq([book])
      expect(lib.return_book("isbn-1", member)).to eq(true)
      expect(member.borrowed).to eq([])
    end
  end

  describe "ADVANCED EXERCISE purpose: solve challenge and edge cases" do
    it "exports lending state via duck-typed exporter" do
      lib = Library.new
      member = Member.new
      book = Book.new(title: "Practical Ruby", author: "A", isbn: "isbn-1")

      lib.add_book(book)
      lib.lend("isbn-1", member)

      report = LibraryReport.new(library: lib, exporter: HashExporter.new)
      expect(report.call).to eq({ "isbn-1" => member.object_id })
    end
  end
end
en/topic_09_mini_capstone/tests/topic_09_mini_capstone_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_09_mini_capstone/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.