Java | オブジェクト指向:小規模アプリの設計演習

Java Java
スポンサーリンク

この演習で目指すこと

ここでは「小さいアプリ」を題材にしながら、
オブジェクト指向でどう設計していくかを、最初から最後まで追体験してもらいます。

難しいフレームワークは使いません。
シンプルな Java のクラスだけで、
「要求を読む → クラスを考える → メソッドを考える → 実装する → 改善する」
という流れを、一緒にゆっくり辿っていきます。

完全に正解の設計を目指すというより、
「こう考えるとオブジェクト指向に寄っていくよ」という思考パターンを掴んでほしい、という演習です。


題材を決める:小さな「本の貸出」アプリ

シンプルな要件を日本語で整理する

題材は「本の貸出管理」の超シンプル版にします。
機能は次くらいに絞りましょう。

ユーザは本を借りられる
本は「貸出中」か「貸出可能」の状態を持つ
同じ本を、同時に2人が借りることはできない
簡単なコンソールアプリとして、Java から使える形を目指す

ここで大事なのは、いきなり「クラス」「メソッド」を考えないことです。
まず日本語で、どんな「もの」が登場するかを感じ取ります。

登場人物(クラス候補)を見つける

日本語の要件から見える「名詞」を拾います。


ユーザ
貸出(借りた情報)
図書館(全体を扱うサービス的なもの)

このへんがクラス候補になります。
最初はだいたいで構いません。「後で変えればいいや」くらいで進めます。


ドメインの中心クラスから作ってみる

Book クラスを設計する

まずは「本」という概念から形にします。
本に必要そうな情報を考えます。

ID(識別子)
タイトル
著者
貸出中かどうか

これをそのままクラスにしてみます。

public class Book {

    private final long id;
    private final String title;
    private final String author;
    private boolean lentOut;

    public Book(long id, String title, String author) {
        if (title == null || title.isBlank()) {
            throw new IllegalArgumentException("タイトルは必須です");
        }
        if (author == null || author.isBlank()) {
            throw new IllegalArgumentException("著者は必須です");
        }
        this.id = id;
        this.title = title;
        this.author = author;
        this.lentOut = false;
    }

    public long id() {
        return id;
    }

    public String title() {
        return title;
    }

    public String author() {
        return author;
    }

    public boolean isLentOut() {
        return lentOut;
    }

    public void lend() {
        if (lentOut) {
            throw new IllegalStateException("すでに貸出中です");
        }
        this.lentOut = true;
    }

    public void returnBack() {
        if (!lentOut) {
            throw new IllegalStateException("貸出中ではありません");
        }
        this.lentOut = false;
    }
}
Java

ここで重要なのは、
「本が貸出可能かどうか」「貸出中かどうかのルール」を Book 自身が知っていることです。

外から book.lentOut = true; と直接書き換えるのではなく、
lend()returnBack() という「意味のあるメソッド」を通じて状態を変えます。

「本は自分の状態を自分で守る」というのが、オブジェクト指向的な考え方です。

User クラスを設計する

次にユーザです。今回は「誰が借りたか」を区別するための最小限にします。

public class User {

    private final long id;
    private final String name;

    public User(long id, String name) {
        if (name == null || name.isBlank()) {
            throw new IllegalArgumentException("名前は必須です");
        }
        this.id = id;
        this.name = name;
    }

    public long id() {
        return id;
    }

    public String name() {
        return name;
    }
}
Java

今はシンプルですが、「何冊まで借りられるか」「延滞中か」などのルールを、
将来的には User に持たせていけます。


貸出という行為をどう表現するか

Loan クラスを導入する

「誰がどの本を、いつ借りたか」を表すクラスを用意します。
これは「貸出」という出来事そのものです。

import java.time.LocalDate;

public class Loan {

    private final long id;
    private final Book book;
    private final User user;
    private final LocalDate loanDate;
    private LocalDate returnDate;

    public Loan(long id, Book book, User user, LocalDate loanDate) {
        if (book == null || user == null || loanDate == null) {
            throw new IllegalArgumentException("必須情報が欠けています");
        }
        this.id = id;
        this.book = book;
        this.user = user;
        this.loanDate = loanDate;
        this.returnDate = null;
    }

    public long id() {
        return id;
    }

    public Book book() {
        return book;
    }

    public User user() {
        return user;
    }

    public LocalDate loanDate() {
        return loanDate;
    }

    public LocalDate returnDate() {
        return returnDate;
    }

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

    public void markReturned(LocalDate date) {
        if (date == null) {
            throw new IllegalArgumentException("返却日が null");
        }
        if (this.returnDate != null) {
            throw new IllegalStateException("すでに返却済みです");
        }
        if (date.isBefore(loanDate)) {
            throw new IllegalArgumentException("返却日は貸出日以降である必要があります");
        }
        this.returnDate = date;
    }
}
Java

Loan は「貸出記録」の役割を担当し、
Book は「本として貸出中かどうか」を知っています。

「誰が何を知るべきか」を分けていくのが設計のポイントです。


全体を操作する「サービス役」を作る

LibraryService のような調整役を用意する

Book、User、Loan は「ドメインの登場人物」です。
これらを使って「借りる」「返す」といったユースケースを実現する調整役として、
LibraryService のようなクラスを用意します。

今回は簡単のため「メモリ上のコレクション」を使った実装にします。

import java.time.LocalDate;
import java.util.HashMap;
import java.util.Map;

public class LibraryService {

    private final Map<Long, Book> books = new HashMap<>();
    private final Map<Long, User> users = new HashMap<>();
    private final Map<Long, Loan> loans = new HashMap<>();
    private long nextLoanId = 1;

    public void addBook(Book book) {
        books.put(book.id(), book);
    }

    public void addUser(User user) {
        users.put(user.id(), user);
    }

    public Loan lend(long bookId, long userId) {
        Book book = books.get(bookId);
        if (book == null) {
            throw new IllegalArgumentException("存在しない本です: " + bookId);
        }
        User user = users.get(userId);
        if (user == null) {
            throw new IllegalArgumentException("存在しないユーザです: " + userId);
        }
        if (book.isLentOut()) {
            throw new IllegalStateException("この本は貸出中です");
        }

        book.lend();
        Loan loan = new Loan(nextLoanId++, book, user, LocalDate.now());
        loans.put(loan.id(), loan);
        return loan;
    }

    public void returnBook(long loanId) {
        Loan loan = loans.get(loanId);
        if (loan == null) {
            throw new IllegalArgumentException("存在しない貸出IDです: " + loanId);
        }
        if (loan.isReturned()) {
            throw new IllegalStateException("すでに返却済みです");
        }

        loan.markReturned(LocalDate.now());
        Book book = loan.book();
        book.returnBack();
    }
}
Java

ここで大事なのは、責務の分担です。

LibraryService は
本やユーザの検索
貸出の生成と保存
Book や Loan のメソッド呼び出し

を担当しますが、
「貸出可能かどうか」「返却済みかどうか」の判定ロジックは、
Book と Loan に任せています。

「ルールを知っているのは誰か?」を意識すると、
どんどんオブジェクト指向っぽくなっていきます。


コンソールアプリから使ってみる

main から LibraryService を呼び出す例

ここまでのクラスを、簡単な main で動かしてみます。

public class Main {

    public static void main(String[] args) {
        LibraryService library = new LibraryService();

        User user = new User(1L, "山田太郎");
        library.addUser(user);

        Book book = new Book(1L, "Java入門", "著者A");
        library.addBook(book);

        Loan loan = library.lend(1L, 1L);
        System.out.println("貸出ID: " + loan.id());
        System.out.println("貸出: " + loan.user().name() + " -> " + loan.book().title());

        library.returnBook(loan.id());
        System.out.println("返却済み: " + loan.isReturned());
        System.out.println("本の貸出状態: " + book.isLentOut());
    }
}
Java

この Main は「アプリケーションの入口」であり、
本当はここに UI や入出力を色々書いていくこともできます。

でも、設計の中心はあくまで

Book
User
Loan
LibraryService

たちです。
Main は「それらを組み合わせて使うだけ」に留めます。


ここからの発展的な改善ポイント(深掘り)

1. ルールが増えたときにどう耐えるか

例えば新しい要件が来たとします。

ユーザは最大3冊まで借りられる
期限を2週間にする
期限を過ぎた本があるユーザは新しい貸出ができない

これを全部 LibraryService の lend に書き始めると、
すぐに lend メソッドがパンパンになります。

そこで発想を変えます。

冊数制限は User の責務
期限切れの判定は Loan の責務
「貸出可能かどうか」の総合判定は User に持たせる

というふうに、「誰が何を知るべきか」を考え直します。

User が「自分の貸出一覧」を持つように構造を変える
Loan に「期限日」「期限切れかどうか」のメソッドを追加する

などのリファクタリングをしていくと、
ルールが増えても1箇所が巨大化しにくくなります。

2. 値オブジェクトを導入してみる

例えば、「貸出期限」を単なる LocalDate や int 日数で扱うのではなく、
LoanPeriodDueDate のような値オブジェクトにすると、コードがぐっと分かりやすくなります。

public class LoanPeriod {

    private final int days;

    public LoanPeriod(int days) {
        if (days <= 0) {
            throw new IllegalArgumentException("日数は1以上");
        }
        this.days = days;
    }

    public LocalDate calculateDueDate(LocalDate loanDate) {
        return loanDate.plusDays(days);
    }
}
Java

Loan がこれを使うようになると、
どこに「期限計算のルール」があるかがはっきりします。


演習として自分でやってみてほしいこと

ここまでの流れを真似しながら、
自分で「別の小さなアプリ」を設計してみると力がつきます。

例えば、こんな題材を考えてみてください。

簡易 ToDo リスト
ポイントカード管理(ユーザとポイント履歴)
簡単な家計簿(収入・支出・カテゴリ)

やるときのポイントは、いきなりコードを書かずに、
必ず日本語でこう整理することです。

どんな「名詞」が出てくるか(クラス候補)
それぞれは「何を知っていて」「何ができるべきか」
ルールはどのクラスが知るべきか

そして、「何でも屋」になりそうなクラスができ始めたら、
この会話を思い出して分割するかどうかを考えてみてください。

タイトルとURLをコピーしました