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 日数で扱うのではなく、
LoanPeriod や DueDate のような値オブジェクトにすると、コードがぐっと分かりやすくなります。

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をコピーしました