Index

Case Study 1 Designing a Library System

Java Object-Oriented Design

19.1 Requirements Analysis

The Importance of Requirements Analysis

Before diving into design or coding, thoroughly understanding and defining the system requirements is essential. Requirements analysis serves as the foundation for successful software development, ensuring that the final product meets user needs and business goals. It helps clarify what the system must do, sets realistic expectations, and guides architectural and design decisions.

Poorly gathered or ambiguous requirements often lead to costly rework, missed deadlines, and systems that don’t fulfill their intended purpose. In the context of a library system, this phase involves close collaboration with stakeholders such as librarians, patrons, and administrators to capture a clear and comprehensive picture of the system’s desired features and constraints.

Typical Functional Requirements of a Library System

A library system, while seemingly straightforward, involves various functions that support daily operations and user interactions. Key functional requirements generally include:

These functional requirements define the core capabilities that the system must deliver to support library operations effectively.

Non-Functional Requirements

While functional requirements describe what the system should do, non-functional requirements specify how the system performs and behaves under various conditions. They address quality attributes critical for system acceptance:

Capturing these non-functional aspects early guides technology choices and architectural patterns that support long-term system viability.

Translating Requirements into Use Cases and User Stories

A practical step after gathering requirements is to convert them into use cases or user stories, which describe interactions between users and the system in concrete scenarios. This approach helps clarify requirements from an end-user perspective and facilitates communication among stakeholders and developers.

Use Case Example: “Borrow Book”

User Story Example: As a library member, I want to reserve a book that is currently checked out so that I can borrow it once it becomes available.

Breaking down requirements into such scenarios helps in designing classes, interfaces, and workflows that directly support user needs.

Sample Requirements Checklist for a Library System

Requirement ID Description Priority Notes
FR-001 Add, update, delete books High CRUD operations on catalog
FR-002 Register and authenticate users High Support roles: member, staff
FR-003 Borrow and return books High Enforce borrowing rules
FR-004 Search catalog High Support multiple filters
FR-005 Reserve books Medium Notify users on availability
FR-006 Calculate overdue fines Medium Fine structure configurable
NFR-001 Response time < 2 seconds High Under 1000 concurrent users
NFR-002 Secure login with encrypted passwords High Use industry-standard hashing
NFR-003 Mobile-friendly UI Low Future enhancement

Summary

Effective requirements analysis is a critical first step in designing any software system, including a library management system. Clear functional and non-functional requirements, expressed through use cases or user stories, provide a solid foundation for architecture and implementation. This structured approach minimizes ambiguity, aligns stakeholder expectations, and enables designing a maintainable, scalable, and user-friendly system.

Index

19.2 Domain Modeling

Introduction to Domain Modeling

After gathering clear requirements, the next crucial step in designing a software system is domain modeling. Domain modeling bridges the gap between real-world concepts and their software representations. It involves identifying key entities, their attributes, and the relationships between them, mapping the problem domain into a conceptual model that guides design and implementation.

This process helps developers and stakeholders build a shared understanding of the system’s structure and behavior. By visually representing entities and their connections, domain modeling lays a foundation for creating robust, maintainable, and extensible software.

Visualizing the Domain: UML Class Diagrams

Unified Modeling Language (UML) class diagrams are the most common tool used to depict domain models. These diagrams illustrate classes (entities), their attributes, methods (optional at this stage), and relationships such as associations, aggregations, compositions, and inheritance hierarchies.

A UML class diagram provides a bird’s-eye view of the domain structure, making it easier to analyze and communicate design decisions. It acts as a blueprint for implementing classes in code, ensuring that the design aligns with requirements.

Key Domain Entities for a Library System

For our library system, several core domain entities naturally emerge from the requirements analysis. These include:

Additional supporting entities might include Reservation, Fine, or Catalog, depending on the scope, but the above cover the core functionality.

Defining Relationships Among Entities

With entities identified, it’s important to define how they relate. Key types of relationships include:

Example Domain Model Diagram

Below is a conceptual UML class diagram illustrating these entities and relationships:

+----------------+              +-----------------+
|     Library    |<>------------|      Book       |
+----------------+ 1        *   +-----------------+
| name           |              | isbn            |
| address        |              | title           |
+----------------+              | author          |
                                | publicationYear |
                                +-----------------+
                                      ^
                                      |
                             +-----------------+
                             |      Loan       |
                             +-----------------+
                             | loanId          |
                             | borrowDate      |
                             | dueDate         |
                             | returnDate      |
                             +-----------------+
                               /           \
                              /             \
+----------------+      1    /               \   1     +-----------------+
|    Member      |----------/                 \--------|   BookCopy      |
+----------------+                            1..*     +-----------------+
| memberId       |                                     | copyNumber      |
| name           |                                     | status          |
| email          |                                     +-----------------+
+----------------+

+----------------+
|   Librarian    |
+----------------+
| employeeId     |
| name           |
+----------------+
     ^
     |
+----------------+
|     User       |
+----------------+
| userId         |
| name           |
+----------------+

Justification of Modeling Decisions

This structure supports scalability and future extension. For example, adding reservations or fines can build on top of this foundation.

Conclusion

Domain modeling is a vital step in object-oriented design that maps real-world concepts to software structures. By identifying key entities like Book, Member, Loan, and Librarian, defining their attributes, and establishing clear relationships through UML diagrams, developers create a blueprint that guides implementation. Thoughtful modeling decisions around aggregation, association, and inheritance improve system clarity, maintainability, and extensibility—paving the way for a robust library system.

Index

19.3 Code Implementation

Translating Domain Model into Java Code

With the domain model established, the next step is to translate it into concrete Java classes that encapsulate data and behavior. This involves defining fields for attributes, constructors for object creation, and methods to represent key actions and enforce rules. Proper encapsulation ensures that internal state is protected and modified only through well-defined interfaces.

Implementing Key Entities

Below are core classes from the domain model with examples of fields, constructors, methods, and interactions.

The Book Class

public class Book {
    private final String isbn;
    private final String title;
    private final String author;
    private final int publicationYear;
    private int totalCopies;
    private int copiesAvailable;

    public Book(String isbn, String title, String author, int publicationYear, int totalCopies) {
        if (isbn == null || isbn.isEmpty()) {
            throw new IllegalArgumentException("ISBN must not be empty");
        }
        if (totalCopies < 0) {
            throw new IllegalArgumentException("Total copies cannot be negative");
        }
        this.isbn = isbn;
        this.title = title;
        this.author = author;
        this.publicationYear = publicationYear;
        this.totalCopies = totalCopies;
        this.copiesAvailable = totalCopies;
    }

    public String getIsbn() { return isbn; }
    public String getTitle() { return title; }
    public String getAuthor() { return author; }
    public int getPublicationYear() { return publicationYear; }

    public int getCopiesAvailable() { return copiesAvailable; }

    public boolean checkoutCopy() {
        if (copiesAvailable > 0) {
            copiesAvailable--;
            return true;
        }
        return false;
    }

    public void returnCopy() {
        if (copiesAvailable < totalCopies) {
            copiesAvailable++;
        } else {
            throw new IllegalStateException("All copies are already in library");
        }
    }

    @Override
    public String toString() {
        return String.format("%s by %s (%d) - Available: %d/%d",
            title, author, publicationYear, copiesAvailable, totalCopies);
    }
}

The Member Class

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

public class Member {
    private final String memberId;
    private final String name;
    private final List<Loan> loans = new ArrayList<>();
    private final int maxBooksAllowed;

    public Member(String memberId, String name, int maxBooksAllowed) {
        if (memberId == null || memberId.isEmpty()) {
            throw new IllegalArgumentException("Member ID required");
        }
        this.memberId = memberId;
        this.name = name;
        this.maxBooksAllowed = maxBooksAllowed;
    }

    public String getMemberId() { return memberId; }
    public String getName() { return name; }
    public List<Loan> getLoans() { return new ArrayList<>(loans); } // Defensive copy

    public boolean canBorrow() {
        return loans.size() < maxBooksAllowed;
    }

    public void borrowBook(Book book) throws IllegalStateException {
        if (!canBorrow()) {
            throw new IllegalStateException("Max book limit reached");
        }
        if (book.checkoutCopy()) {
            Loan loan = new Loan(this, book);
            loans.add(loan);
            System.out.println(name + " borrowed " + book.getTitle());
        } else {
            throw new IllegalStateException("No copies available to borrow");
        }
    }

    public void returnBook(Loan loan) {
        if (loans.remove(loan)) {
            loan.returnBook();
            System.out.println(name + " returned " + loan.getBook().getTitle());
        } else {
            throw new IllegalArgumentException("Loan not found for member");
        }
    }
}

The Loan Class

import java.time.LocalDate;

public class Loan {
    private static final int LOAN_PERIOD_DAYS = 14;

    private final Member member;
    private final Book book;
    private final LocalDate borrowDate;
    private LocalDate returnDate;

    public Loan(Member member, Book book) {
        this.member = member;
        this.book = book;
        this.borrowDate = LocalDate.now();
        this.returnDate = null;
    }

    public Member getMember() { return member; }
    public Book getBook() { return book; }
    public LocalDate getBorrowDate() { return borrowDate; }
    public LocalDate getReturnDate() { return returnDate; }

    public boolean isReturned() {
        return returnDate != null;
    }

    public void returnBook() {
        if (isReturned()) {
            throw new IllegalStateException("Book already returned");
        }
        this.returnDate = LocalDate.now();
        book.returnCopy();
    }

    public boolean isOverdue() {
        if (isReturned()) {
            return false;
        }
        return LocalDate.now().isAfter(borrowDate.plusDays(LOAN_PERIOD_DAYS));
    }

    @Override
    public String toString() {
        return String.format("Loan of '%s' by %s on %s%s",
            book.getTitle(),
            member.getName(),
            borrowDate,
            isReturned() ? " (Returned)" : "");
    }
}

Encapsulation and Constructor Best Practices

Each class uses private fields and exposes accessors where needed, protecting internal state. Constructors validate inputs to prevent illegal states. For example, the Book class ensures that isbn is not empty, and copies are non-negative. Such defensive programming reduces runtime errors and enforces invariants.

Handling Exceptions and Input Validation

The above classes illustrate how exceptions help guard against invalid operations:

This explicit failure signaling aids debugging and promotes robust, predictable behavior.

Incremental Development and Testing

Building a library system benefits from an incremental approach:

  1. Implement core entities (Book, Member, Loan) first.
  2. Write unit tests verifying constructors, getters, and key methods like borrowBook().
  3. Extend functionality with supporting classes (Librarian, Catalog, Reservation).
  4. Add exception handling and edge case tests.
  5. Integrate components gradually to test interactions.

Writing automated tests at each step ensures the system behaves as expected and allows safe refactoring later.

Sample Usage Scenario

public class LibraryDemo {
    public static void main(String[] args) {
        Book book = new Book("978-0134685991", "Effective Java", "Joshua Bloch", 2018, 3);
        Member alice = new Member("M001", "Alice Johnson", 5);

        try {
            alice.borrowBook(book);
            alice.borrowBook(book); // Borrow second copy
            System.out.println(book);

            // Return one copy
            Loan loan = alice.getLoans().get(0);
            alice.returnBook(loan);
            System.out.println(book);

        } catch (IllegalStateException | IllegalArgumentException ex) {
            System.err.println("Operation failed: " + ex.getMessage());
        }
    }
}

Output:

Alice Johnson borrowed Effective Java
Alice Johnson borrowed Effective Java
Effective Java by Joshua Bloch (2018) - Available: 1/3
Alice Johnson returned Effective Java
Effective Java by Joshua Bloch (2018) - Available: 2/3
Click to view full runnable Code

import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;

// Book class
class Book {
    private final String isbn;
    private final String title;
    private final String author;
    private final int publicationYear;
    private int totalCopies;
    private int copiesAvailable;

    public Book(String isbn, String title, String author, int publicationYear, int totalCopies) {
        if (isbn == null || isbn.isEmpty()) {
            throw new IllegalArgumentException("ISBN must not be empty");
        }
        if (totalCopies < 0) {
            throw new IllegalArgumentException("Total copies cannot be negative");
        }
        this.isbn = isbn;
        this.title = title;
        this.author = author;
        this.publicationYear = publicationYear;
        this.totalCopies = totalCopies;
        this.copiesAvailable = totalCopies;
    }

    public String getIsbn() { return isbn; }
    public String getTitle() { return title; }
    public String getAuthor() { return author; }
    public int getPublicationYear() { return publicationYear; }
    public int getCopiesAvailable() { return copiesAvailable; }

    public boolean checkoutCopy() {
        if (copiesAvailable > 0) {
            copiesAvailable--;
            return true;
        }
        return false;
    }

    public void returnCopy() {
        if (copiesAvailable < totalCopies) {
            copiesAvailable++;
        } else {
            throw new IllegalStateException("All copies are already in library");
        }
    }

    @Override
    public String toString() {
        return String.format("%s by %s (%d) - Available: %d/%d",
            title, author, publicationYear, copiesAvailable, totalCopies);
    }
}

// Loan class
class Loan {
    private static final int LOAN_PERIOD_DAYS = 14;

    private final Member member;
    private final Book book;
    private final LocalDate borrowDate;
    private LocalDate returnDate;

    public Loan(Member member, Book book) {
        this.member = member;
        this.book = book;
        this.borrowDate = LocalDate.now();
        this.returnDate = null;
    }

    public Member getMember() { return member; }
    public Book getBook() { return book; }
    public LocalDate getBorrowDate() { return borrowDate; }
    public LocalDate getReturnDate() { return returnDate; }

    public boolean isReturned() {
        return returnDate != null;
    }

    public void returnBook() {
        if (isReturned()) {
            throw new IllegalStateException("Book already returned");
        }
        this.returnDate = LocalDate.now();
        book.returnCopy();
    }

    public boolean isOverdue() {
        if (isReturned()) {
            return false;
        }
        return LocalDate.now().isAfter(borrowDate.plusDays(LOAN_PERIOD_DAYS));
    }

    @Override
    public String toString() {
        return String.format("Loan of '%s' by %s on %s%s",
            book.getTitle(),
            member.getName(),
            borrowDate,
            isReturned() ? " (Returned)" : "");
    }
}

// Member class
class Member {
    private final String memberId;
    private final String name;
    private final List<Loan> loans = new ArrayList<>();
    private final int maxBooksAllowed;

    public Member(String memberId, String name, int maxBooksAllowed) {
        if (memberId == null || memberId.isEmpty()) {
            throw new IllegalArgumentException("Member ID required");
        }
        this.memberId = memberId;
        this.name = name;
        this.maxBooksAllowed = maxBooksAllowed;
    }

    public String getMemberId() { return memberId; }
    public String getName() { return name; }
    public List<Loan> getLoans() { return new ArrayList<>(loans); } // Defensive copy

    public boolean canBorrow() {
        return loans.size() < maxBooksAllowed;
    }

    public void borrowBook(Book book) {
        if (!canBorrow()) {
            throw new IllegalStateException("Max book limit reached");
        }
        if (book.checkoutCopy()) {
            Loan loan = new Loan(this, book);
            loans.add(loan);
            System.out.println(name + " borrowed " + book.getTitle());
        } else {
            throw new IllegalStateException("No copies available to borrow");
        }
    }

    public void returnBook(Loan loan) {
        if (loans.remove(loan)) {
            loan.returnBook();
            System.out.println(name + " returned " + loan.getBook().getTitle());
        } else {
            throw new IllegalArgumentException("Loan not found for member");
        }
    }
}

// Demo main class
public class LibraryDemo {
    public static void main(String[] args) {
        Book book = new Book("978-0134685991", "Effective Java", "Joshua Bloch", 2018, 3);
        Member alice = new Member("M001", "Alice Johnson", 5);

        try {
            alice.borrowBook(book);
            alice.borrowBook(book); // Borrow second copy
            System.out.println(book);

            // Return one copy
            Loan loan = alice.getLoans().get(0);
            alice.returnBook(loan);
            System.out.println(book);

        } catch (IllegalStateException | IllegalArgumentException ex) {
            System.err.println("Operation failed: " + ex.getMessage());
        }
    }
}

Summary and Best Practices

This approach ensures your library system remains flexible to future changes, easier to understand, and reliable in operation.

Index