Putting it together: a small library
A capstone that uses almost everything: classes with state, a mixin, Enumerable, blocks, sorted and grouped reports, a custom error, and JSON out of the standard library.
The shape of the program
Two classes and a module. Book is a value: three readers, a to_s that lines up in a table, and a to_h that says what it is when the outside world asks for data. Library holds books and — this is the interesting part — includes Enumerable, so defining each is enough for it to be sorted, grouped and searched with the same methods an array answers.
The module Summarisable supplies the report. It is a mixin because summarising is a thing a list of books can do, not a kind of book — and it uses map and join, which the class only has because it mixed in Enumerable. Modules feeding on modules is the ordinary texture of Ruby, not a trick.
Rules that refuse
add validates before it stores: a duplicate title is a LibraryError, and a year outside the range printing has existed is a LibraryError too. Raising by name, with a message written for a person, is what makes the failure readable when it arrives three calls away — and the capstone catches its own error at the bottom to show the message travelling out and being handled.
Notice what is absent. No loops build arrays by hand — blocks do that. No type checks — the objects know what they are. No manual string assembly for the output — format lines up the columns and JSON.generate writes the machine-readable version, both from the standard library.
Where a real project goes next
A real library system would add persistence, a to_s that is not for humans, tests, and a gem specification so the code can be installed. The techniques are the ones from this course: another class for the store, another module for the behaviour it shares, blocks for the queries, and exceptions for the rules.
The habit worth keeping is the one this capstone shows at the top: give every step a name — a class, a method, a module — and the program reads like the description of what it does. That is what Ruby was designed to make possible.
The seams that keep it readable
The capstone separates three jobs that are easy to tangle: a value class holds data, a collection class holds the rules for adding and finding, and a module holds a report that any collection can produce. Each seam is a place where a change stays local. Change the table layout in the report module, and nothing about storage moves; add a validation to Library, and every caller gets it.
Enumerable is the contract that makes the collection feel native. Defining each — yielding every item in turn — is the whole price of admission; map, select, sort_by, count and min_by arrive from the module and are written in terms of that one method. A collection that honours each can be handed to any code that expects something enumerable, which is a quieter kind of reuse than inheritance.
A capstone is a rehearsal
The point of building the library end to end is not the library. It is meeting the seams: where a value becomes a row, where a rule refuses, where a collection becomes enumerable. Those seams appear in every program of any size, and recognising them is most of the design work.
Read the code in the order it runs: the class definitions first, then the three books, then the report, then the refusals. Each line should be explainable by a chapter you have read, and the ones that are not are the ones to revisit.
The next step is a test
The natural next addition is a file of assertions: adding a duplicate raises, a bad year raises, the oldest book is the 1961 one, the JSON round trip keeps the titles. Those claims pin the behaviour, and they let you change the report layout without wondering whether the rules still hold.
Three things to watch as the program grows. to_s is for people and to_h is for data — putting both in one method makes a change to the table break the JSON. Returning the internal array from an accessor lets a caller add books without passing the validation. And a library should raise its own named error rather than rescue everything at the edge: the caller decides what a failure means, not the code that discovered it.
The whole capstone, one page
require 'json'
class LibraryError < StandardError; end
class Book
attr_reader :title, :author, :year
def initialize(title, author, year)
@title = title
@author = author
@year = year
end
def to_s
format('%-22s %-16s %d', @title, @author, @year)
end
def to_h
{ title: @title, author: @author, year: @year }
end
end
module Summarisable
def summary
rows = map { |book| book.to_s }
rows.unshift(format('%-22s %-16s %s', 'TITLE', 'AUTHOR', 'YEAR'))
rows.join("
")
end
end
class Library
include Enumerable
include Summarisable
def initialize
@books = []
end
def add(book)
raise LibraryError, "#{book.title} is already on the shelf" if any? { |b| b.title == book.title }
raise LibraryError, "#{book.year} is not a year" unless (1450..2100).cover?(book.year)
@books << book
self
end
def each
@books.each { |book| yield book }
end
def oldest
min_by { |book| book.year }
end
end
library = Library.new
library.add(Book.new('Dune', 'Frank Herbert', 1965))
library.add(Book.new('Solaris', 'Stanislaw Lem', 1961))
library.add(Book.new('Kindred', 'Octavia Butler', 1979))
puts library.summary
p library.group_by { |book| book.year < 1970 ? 'sixties' : 'seventies' }.keys
puts JSON.generate(library.map { |book| book.to_h })The library, end to end
Run it and check each report against the four books. Then add a fifth book, a `top_by_year` method that returns the three most recent, and one more validation rule of your own.
A capstone is a reunion, not an exam: every line above is something an earlier chapter taught. If one of them feels unfamiliar, that chapter is worth one more read — the code will still be here.
Try these next
- Add a
remove(title)method that raises the library error when the title is not on the shelf. - Write a second report module that lists books by decade, and include it beside the first.
- Export the library as JSON, then parse it back and rebuild the objects.
A playlist, end to end
Run it and check the report and the JSON against the three tracks. Then add a fourth track, a `longest` method, and one more validation rule of your own.
Check what you learned
One question at a time. A wrong answer explains itself — take another go.
Part 1 of 5
Why does Library only define each and yet answer sort_by?
