Goal: make LibraryItem abstract, introduce the interfaces Loanable and Reservable, add Magazine, and make Member sortable.

Target design for Step 3
Target design for Step 3

1. Two contracts

src/lms/model/Loanable.java
package lms.model;

/**
 * Loanable - a contract: "anything that can be loaned to a Member".
 *
 * An interface says WHAT must be possible, never HOW. Any class that
 * implements Loanable promises to provide both methods below.
 */
public interface Loanable {
    void loan(Member member);
    void returnItem();
}
src/lms/model/Reservable.java
package lms.model;

/**
 * Reservable - a second, separate contract.
 *
 * Only some items can be reserved (Books, but not DVDs or Magazines in
 * our library), so this capability does NOT belong on LibraryItem.
 * An interface lets us attach it to exactly the classes that need it.
 */
public interface Reservable {
    void reserve(Member member);
    void cancelReservation();
    boolean isReserved();
}

2. The abstract class

calculateLateFee now has no body. LibraryItem also implements Loanable, so the loan logic lives here once.

src/lms/model/LibraryItem.java
package lms.model;

/**
 * LibraryItem - Week 4 version.
 *
 * Two changes from Week 3:
 *   1. It now IMPLEMENTS the Loanable interface (a contract it fulfils).
 *   2. calculateLateFee() is now truly ABSTRACT - no default body.
 *      Every subclass must write its own, or it will not compile.
 */
public abstract class LibraryItem implements Loanable {
    private String id;
    private String title;
    private boolean isBorrowed;

    public LibraryItem(String id, String title) {
        setId(id);
        setTitle(title);
        this.isBorrowed = false;
    }

    public String getId() { return id; }
    public String getTitle() { return title; }
    public boolean isBorrowed() { return isBorrowed; }

    public void setId(String id) {
        if (id == null || id.isBlank()) throw new IllegalArgumentException("Id cannot be blank");
        this.id = id;
    }

    public void setTitle(String title) {
        if (title == null || title.isBlank()) throw new IllegalArgumentException("Title cannot be blank");
        this.title = title;
    }

    // ---- Loanable contract (shared implementation lives here) ----

    @Override
    public void loan(Member member) {
        if (isBorrowed) {
            System.out.println(title + " is already on loan.");
        } else if (member.getBorrowedCount() >= member.getMaxBorrowLimit()) {
            System.out.println(member.getName() + " has reached the limit of " + member.getMaxBorrowLimit() + " items.");
        } else {
            isBorrowed = true;
            member.incrementBorrowedCount();
            System.out.println(title + " loaned to " + member.getName());
        }
    }

    @Override
    public void returnItem() {
        isBorrowed = false;
        System.out.println(title + " returned.");
    }

    /** No body: each subclass MUST define its own late-fee rule. */
    public abstract double calculateLateFee(int daysLate);

    @Override
    public String toString() {
        return getClass().getSimpleName() + "{id='" + id + "', title='" + title + "', borrowed=" + isBorrowed + "}";
    }
}

3. Three concrete items

Book is also Reservable; DVD is not; Magazine is new, and the compiler forces its fee rule.

src/lms/model/Book.javaextends LibraryItem implements Reservable
package lms.model;

/** Book - a LibraryItem that can ALSO be reserved (implements Reservable). */
public class Book extends LibraryItem implements Reservable {
    private String author;
    private Member reservedBy;

    public Book(String id, String title, String author) {
        super(id, title);
        setAuthor(author);
    }

    public String getAuthor() { return author; }

    public void setAuthor(String author) {
        if (author == null || author.isBlank()) throw new IllegalArgumentException("Author cannot be blank");
        this.author = author;
    }

    /** Books used to inherit a default rate; now each class states its own. */
    @Override
    public double calculateLateFee(int daysLate) {
        return daysLate * 10.0;
    }

    // ---- Reservable contract ----

    @Override
    public void reserve(Member member) {
        if (reservedBy != null) {
            System.out.println(getTitle() + " is already reserved by " + reservedBy.getName());
        } else {
            reservedBy = member;
            System.out.println(getTitle() + " reserved for " + member.getName());
        }
    }

    @Override
    public void cancelReservation() {
        reservedBy = null;
        System.out.println("Reservation cancelled for " + getTitle());
    }

    @Override
    public boolean isReserved() { return reservedBy != null; }
}
src/lms/model/Magazine.java
package lms.model;

/**
 * Magazine - a NEW kind of item added this week.
 *
 * Try this experiment: delete the calculateLateFee() method below and
 * try to compile. Because LibraryItem declares it abstract, the compiler
 * refuses: "Magazine is not abstract and does not override abstract method".
 */
public class Magazine extends LibraryItem {
    private int issueNumber;

    public Magazine(String id, String title, int issueNumber) {
        super(id, title);
        this.issueNumber = issueNumber;
    }

    public int getIssueNumber() { return issueNumber; }

    @Override
    public double calculateLateFee(int daysLate) {
        return daysLate * 5.0;
    }
}

4. Sorting members with Comparable

src/lms/model/Member.javacompareTo: most active first
package lms.model;

/**
 * Member - parent of Student and Staff. Week 4: implements Comparable so a
 * list of members can be sorted with Collections.sort().
 */
public class Member implements Comparable<Member> {
    private String memberId;
    private String name;
    private int borrowedCount;
    private int maxBorrowLimit;

    public Member(String memberId, String name, int maxBorrowLimit) {
        setMemberId(memberId);
        setName(name);
        this.maxBorrowLimit = maxBorrowLimit;
        this.borrowedCount = 0;
    }

    public String getMemberId() { return memberId; }
    public String getName() { return name; }
    public int getBorrowedCount() { return borrowedCount; }
    public int getMaxBorrowLimit() { return maxBorrowLimit; }

    public void setMemberId(String memberId) {
        if (memberId == null || memberId.isBlank()) throw new IllegalArgumentException("Member ID cannot be blank");
        this.memberId = memberId;
    }

    public void setName(String name) {
        if (name == null || name.isBlank()) throw new IllegalArgumentException("Name cannot be blank");
        this.name = name;
    }

    public void setBorrowedCount(int borrowedCount) {
        if (borrowedCount < 0) throw new IllegalArgumentException("Borrowed count cannot be negative");
        this.borrowedCount = borrowedCount;
    }

    public void incrementBorrowedCount() { setBorrowedCount(this.borrowedCount + 1); }

    /**
     * Natural order: MOST active member first (highest borrowedCount).
     * Collections.sort(list) calls this method to decide which of two
     * members comes first.
     */
    @Override
    public int compareTo(Member other) {
        return Integer.compare(other.borrowedCount, this.borrowedCount);
    }

    @Override
    public String toString() {
        return getClass().getSimpleName() + "{id='" + memberId + "', name='" + name
                + "', borrowedCount=" + borrowedCount + ", limit=" + maxBorrowLimit + "}";
    }
}

5. Library talks to interfaces

src/lms/service/Library.java
package lms.service;

import lms.model.Book;
import lms.model.DVD;
import lms.model.LibraryItem;
import lms.model.Loanable;
import lms.model.Member;
import lms.model.Reservable;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * Library - Week 4 version (Abstract Classes & Interfaces).
 *
 * Kept from Week 3: printAllLateFees(), the addItem() overloads.
 * New this week: methods that talk to INTERFACE types (Loanable,
 * Reservable) instead of concrete classes, and a Comparable-based sort.
 */
public class Library {

    private final List<LibraryItem> items = new ArrayList<>();
    private final List<Member> members = new ArrayList<>();

    // ---- from Week 3 (unchanged) ----

    public void addItem(LibraryItem item) {
        items.add(item);
        System.out.println("Added via addItem(LibraryItem): " + item.getTitle());
    }

    public void addItem(Book book) {
        items.add(book);
        System.out.println("Added via addItem(Book): " + book.getTitle());
    }

    public void addItem(DVD dvd) {
        items.add(dvd);
        System.out.println("Added via addItem(DVD): " + dvd.getTitle());
    }

    public void addMember(Member member) { members.add(member); }

    public void printAllLateFees(int daysLate) {
        System.out.println("--- Late fees if " + daysLate + " day(s) late ---");
        for (LibraryItem item : items) {
            System.out.println(item.getTitle() + ": " + item.calculateLateFee(daysLate));
        }
    }

    // ---- NEW in Week 4 ----

    /**
     * The parameter type is the INTERFACE Loanable, not LibraryItem.
     * This method would work for anything that promises to be loanable,
     * even a class that is not a LibraryItem at all.
     */
    public void lend(Loanable thing, Member member) {
        thing.loan(member);
    }

    /**
     * Only some items are Reservable. Checking for the INTERFACE (not for
     * Book) means a future Reservable class works here with no changes.
     */
    public void reserveIfPossible(LibraryItem item, Member member) {
        if (item instanceof Reservable r) {
            r.reserve(member);
        } else {
            System.out.println(item.getTitle() + " cannot be reserved.");
        }
    }

    /** Prints members ranked by activity, using Member's compareTo(). */
    public void printMembersByActivity() {
        List<Member> sorted = new ArrayList<>(members);
        Collections.sort(sorted);
        System.out.println("--- Members ranked by activity ---");
        for (Member m : sorted) {
            System.out.println(m.getName() + " (" + m.getBorrowedCount() + " borrowed)");
        }
    }

    public void listItems() {
        System.out.println("--- Items in the library ---");
        for (LibraryItem item : items) System.out.println(item);
    }

    public void listMembers() {
        System.out.println("--- Registered members ---");
        for (Member m : members) System.out.println(m);
    }

    public List<LibraryItem> getItems() { return items; }
    public List<Member> getMembers() { return members; }
}
src/lms/Main.java
package lms;

import lms.model.*;
import lms.service.Library;

/**
 * Main - Week 4 demo: Abstract Classes & Interfaces.
 *
 * Try this yourself:
 *   1. Delete calculateLateFee() from Magazine and compile. Read the error.
 *   2. Add "implements Reservable" to DVD and satisfy the compiler.
 *   3. Write a new class Poster that implements Loanable but is NOT a
 *      LibraryItem, then pass it to library.lend(...).
 *   4. Change compareTo() so the LEAST active member sorts first.
 */
public class Main {
    public static void main(String[] args) {
        Library library = new Library();

        Book book = new Book("B001", "Clean Code", "Robert C. Martin");
        DVD dvd = new DVD("D001", "Introduction to Algorithms", 120);
        Magazine mag = new Magazine("G001", "Software Weekly", 42);

        library.addItem(book);
        library.addItem(dvd);
        library.addItem(mag);

        Student nadeesha = new Student("M001", "Nadeesha Perera");
        Staff kasun = new Staff("M002", "Mr. Kasun Silva");
        library.addMember(nadeesha);
        library.addMember(kasun);

        // Abstract method: three classes, three different fee rules
        library.printAllLateFees(3);

        // Loanable interface
        System.out.println("--- Loans ---");
        library.lend(book, nadeesha);
        library.lend(book, kasun);      // already on loan
        library.lend(dvd, kasun);
        library.lend(mag, kasun);

        // Reservable interface: only Book qualifies
        System.out.println("--- Reservations ---");
        library.reserveIfPossible(book, kasun);
        library.reserveIfPossible(dvd, nadeesha);

        // Comparable
        library.printMembersByActivity();
    }
}
mkdir -p out javac -d out $(find src -name "*.java") java -cp out lms.Main
Added via addItem(Book): Clean Code Added via addItem(DVD): Introduction to Algorithms Added via addItem(LibraryItem): Software Weekly --- Late fees if 3 day(s) late --- Clean Code: 30.0 Introduction to Algorithms: 75.0 Software Weekly: 15.0 --- Loans --- Clean Code loaned to Nadeesha Perera Clean Code is already on loan. Introduction to Algorithms loaned to Mr. Kasun Silva Software Weekly loaned to Mr. Kasun Silva --- Reservations --- Clean Code reserved for Mr. Kasun Silva Introduction to Algorithms cannot be reserved. --- Members ranked by activity --- Mr. Kasun Silva (2 borrowed) Nadeesha Perera (1 borrowed)

Capture these compiler errors for your PR

You do this The compiler says
new LibraryItem("X","Y") LibraryItem is abstract; cannot be instantiated
delete calculateLateFee from Magazine Magazine is not abstract and does not override abstract method calculateLateFee(int) in LibraryItem
add implements Reservable to DVD DVD is not abstract and does not override abstract method isReserved() in Reservable
item.getAuthor() on a LibraryItem variable cannot find symbol

Tip These errors are your friends: the compiler is enforcing the contract you designed.

Checkpoint: is it done?

  • new LibraryItem(...) fails to compile and you saved the message
  • DVD is not Reservable, yet reserveIfPossible(dvd, …) works
  • Members are ranked most-active first
  • UML saved as docs/uml-week4.png; PR: Week 4: Abstract Classes & Interfaces