On Your Own: Reading Tracker

The Course
•
4 min read

Close the planner solution. This independent brief uses a different domain so you can check whether the ideas transfer. Spend time writing a contract and acceptance table before opening hints. It is fine to revisit a particular earlier lesson, but try not to copy and rename the final project wholesale.

The brief

Model a Book with a trimmed nonblank title, a fixed total page count and a changing number of pages read. The constructor accepts title and pages; pages must be nonnegative. Start with zero read. Throw IllegalArgumentException for a null or blank title or a negative page count. A zero-page book is allowed and has zero remaining pages immediately.

Provide readPages(int amount) returning boolean. Accept a nonnegative amount only when it fits in the unread remainder; otherwise return false without changing state. Provide summary() returning title followed by the remaining-page count. Repeated reads are genuine additional work, unlike the planner’s complete action. Decide which state changes and explain this difference.

Acceptance plan

For a 100-page Java book, reading 25 succeeds and leaves 75. Reading -1 or 80 next fails and still leaves 75. Reading exactly 75 succeeds and leaves 0. A read of 0 succeeds without changing state. A second book must remain independent. Check constructor rejection separately from rejected updates.

For a multi-file version, use Main.java for your checks and Book.java for the model, with the class matching its filename. The two-tab workspace below supplies those filenames and deliberately incomplete methods. Implement the model in Book.java and add checks in Main.java. You can also download this starter for local work; all core practice still works in the browser.

A minimal transfer example: fixed capacity and accepted changes Java
public class Main {
    public static void main(String[] args) {
        int capacity = 100;
        int used = 25;
        int request = 80;
        boolean fits = request >= 0 && request <= capacity - used;

        System.out.println(fits);
        System.out.println(capacity - used);
    }
}
Check your prediction
false
75

The fit test uses the remaining capacity before mutation. It does not add request to used first. Testing against capacity-used also avoids overflow from adding two potentially large positive ints.

Your turn

Exercise

Build and check Book

Implement the brief and print the following for a 100-page Java book: successful read of 25 (true), rejected read of 80 (false), summary Java: 75 pages left. Add the remaining acceptance checks yourself after this first case passes.

Your workspace: Main.java and Book.java Java
public class Main { public static void main(String[] args) { Book book = new Book(" Java ", 100); System.out.println(book.readPages(25)); System.out.println(book.readPages(80)); System.out.println(book.summary()); // Add independent checks for the other acceptance cases here. } } public class Book { // Add the private fixed values and changing progress here. public Book(String title, int pages) { // Validate and store the supplied values. } public boolean readPages(int amount) { // Reject an invalid update before changing progress. return false; } public String summary() { // Return a description based on the current state. return ""; } }
Hint 1: the idea

Keep fixed values final and the progress value mutable. Validate a request before adding it.

Hint 2: a first step

Use pages-read as the available remainder. If a request is outside its allowed range, return false before assigning to read.

Show one solution

This reference is shown as one Main.java file so you can read it together. In the two-tab workspace, put the Book fields and method bodies in Book.java, keeping its public class Book declaration. Put only the Main code in Main.java. Do not paste both class declarations into both tabs.

public class Main {
    public static void main(String[] args) {
        Book book = new Book(" Java ", 100);

        System.out.println(book.readPages(25));
        System.out.println(book.readPages(80));
        System.out.println(book.summary());
    }
}

class Book {
    private final String title;
    private final int pages;
    private int read;

    Book(String title, int pages) {
        if (title == null || title.trim().isEmpty()) {
            throw new IllegalArgumentException("Title is required");
        }

        if (pages < 0) {
            throw new IllegalArgumentException("Pages must be nonnegative");
        }

        this.title = title.trim();
        this.pages = pages;
    }

    public boolean readPages(int amount) {
        if (amount < 0 || amount > pages - read) {
            return false;
        }

        read += amount;
        return true;
    }

    public String summary() {
        return title + ": " + (pages - read) + " pages left";
    }
}
true
false
Java: 75 pages left

Change it and explain

Turn the acceptance plan into a test program with labeled checks. For a multi-file version, verify the same Book from a separate Main entry. Add one deliberate mutation—accept requests above the remainder—and make sure a check detects it.

Explain why a setter such as setRead(anyNumber) is not part of this model’s API. The public operation expresses progress and preserves a bound. If you needed corrections or undo later, design that behavior explicitly rather than quietly allowing every caller to bypass the current rule.

Was this article helpful?

Your response is saved on this device.