Project: a small library system

Introduction

This lesson builds one small program that uses most of the course: a library that lends items. Read the code first, then step through it.

Knowing the features is not the same as designing with them. This lesson reads one complete program and asks why each part is built the way it is, so that you can make the same decisions in your own programs.

How it works

The complete program is in the Example section below, where you can run it and step through it. These notes explain the decisions behind it, and are easiest to follow with the code open beside them.

How it is designed

  • Item is abstract. Every item has a title and can be borrowed or returned, but each kind describes itself differently and has its own loan period, so describe() and loanDays() are pure virtual. Book and DVD fill them in.
  • Encapsulation. borrowed is private. The only way to change it is borrow() and giveBack(), which enforce the rules, and throw an exception when someone breaks them.
  • The library owns its items. Library keeps a vector<Item*> and deletes the items in its destructor. Copying a Library would make two owners, so its copy constructor and assignment are deleted (= delete): the compiler refuses to copy it.
  • Polymorphism. list() loops over Item * and calls describe(); each item prints itself in its own way.
  • Errors are exceptions. lend throws for an unknown title or an item that is already out. main catches them and carries on.

Design questions to ask

QuestionAnswer here
What are the things (nouns)?items, books, DVDs, the library: the classes
What can each do (verbs)?borrow, give back, describe: the member functions
What is shared, what differs?shared in Item; differences in virtual functions
Who owns what?the library owns the items and deletes them
What can go wrong?bad titles, double loans: exceptions

Try changing it: add a Magazine class with a 3-day loan, or a member function that counts the items that are out.

Syntax

The design uses features from the whole course. The ones to look for:

virtual string describe() const = 0;          // each kind fills this in
Library(const Library&) = delete;             // no copies: one owner
throw invalid_argument("no item called " + title);
for (Item* it : items) delete it;             // the owner cleans up

Example

#include <iostream>
#include <string>
#include <vector>
#include <stdexcept>
using namespace std;

class Item {
    string title;
    bool borrowed;
public:
    Item(string t) : title(t), borrowed(false) {}
    virtual ~Item() {}
    string getTitle() const { return title; }
    bool isBorrowed() const { return borrowed; }
    void borrow() {
        if (borrowed) throw runtime_error(title + " is already borrowed");
        borrowed = true;
    }
    void giveBack() { borrowed = false; }
    virtual string describe() const = 0;
    virtual int loanDays() const = 0;
};

class Book : public Item {
    string author;
public:
    Book(string t, string a) : Item(t), author(a) {}
    string describe() const override { return getTitle() + " by " + author; }
    int loanDays() const override { return 14; }
};

class DVD : public Item {
    int minutes;
public:
    DVD(string t, int m) : Item(t), minutes(m) {}
    string describe() const override { return getTitle() + " (" + to_string(minutes) + " min)"; }
    int loanDays() const override { return 7; }
};

class Library {
    vector<Item*> items;
public:
    Library() {}
    Library(const Library&) = delete;
    Library& operator=(const Library&) = delete;
    ~Library() {
        for (Item* it : items) delete it;
    }
    void add(Item* it) { items.push_back(it); }
    Item& find(const string& title) {
        for (Item* it : items)
            if (it->getTitle() == title) return *it;
        throw invalid_argument("no item called " + title);
    }
    void lend(const string& title) {
        Item& it = find(title);
        it.borrow();
        cout << "lent " << it.describe() << " for " << it.loanDays() << " days" << endl;
    }
    void list() const {
        for (Item* it : items) cout << (it->isBorrowed() ? "[out] " : "[in]  ") << it->describe() << endl;
    }
};

int main() {
    Library lib;
    lib.add(new Book("Clean Code", "Martin"));
    lib.add(new DVD("Cosmos", 60));
    lib.lend("Cosmos");
    try {
        lib.lend("Cosmos");
    } catch (const exception& e) {
        cout << "sorry: " << e.what() << endl;
    }
    try {
        lib.lend("Dune");
    } catch (const exception& e) {
        cout << "sorry: " << e.what() << endl;
    }
    lib.list();
    return 0;
}
Open in compiler

The program models a small library that lends books and DVDs. main adds two items, lends one, tries two loans that break the rules, and lists the items.

Line by line

  1. Item holds the title and whether the item is out, both private, and its pure virtual functions make it abstract.
  2. borrow throws a runtime_error if the item is already out, so the rule cannot be broken quietly.
  3. Book and DVD fill in describe and loanDays, with 14 and 7 days.
  4. Library owns its items: it deletes them in its destructor, and copying it is deleted with = delete.
  5. find returns a reference to an item, or throws invalid_argument for an unknown title.
  6. In main, the second loan of Cosmos and the loan of Dune both throw, and each catch prints a message.
  7. list prints each item, marked [out] or [in], through the virtual describe.

Expected result

lent Cosmos (60 min) for 7 days
sorry: Cosmos is already borrowed
sorry: no item called Dune
[in]  Clean Code by Martin
[out] Cosmos (60 min)

In the real world

Real library, shop and booking systems are designed the same way, with many more classes. Writing down the classes, their responsibilities and who owns what before coding is the first step of object-oriented design in industry.

Common mistakes

  • Putting every feature into one large class, so that nothing can change without touching everything.
  • Leaving it unclear who deletes an object, which leads to leaks or double deletes.
  • Using inheritance for "has a" relationships, such as making Library derive from Item.

Best practices

  • Sketch the classes and their relationships before writing code.
  • Give each class one clear responsibility.
  • Make ownership obvious, and delete copying when it would create two owners.

Summary

Object-oriented design turns a problem's nouns into classes and its verbs into member functions. Abstract classes capture what varies, encapsulation protects each object's rules, one object owns each resource, and exceptions report broken rules.

Quick check

1. Why is Item abstract?
2. Why does Library delete its copy constructor?
3. What happens when lend throws?

Key takeaways

  • Nouns suggest classes, and verbs suggest member functions.
  • Shared behaviour goes in a base class, and differences in virtual functions.
  • Exactly one object should own each resource.

Practice problems for this lesson

My notes

Write down what you want to remember from this lesson. Notes are saved on this device as you type, and appear on your dashboard.