この演習で目指すこと
ここでは「小さいアプリ」を題材にしながら、
オブジェクト指向でどう設計していくかを、最初から最後まで追体験してもらいます。
難しいフレームワークは使いません。
シンプルな 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;
}
}
JavaLoan は「貸出記録」の役割を担当し、
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);
}
}
JavaLoan がこれを使うようになると、
どこに「期限計算のルール」があるかがはっきりします。
演習として自分でやってみてほしいこと
ここまでの流れを真似しながら、
自分で「別の小さなアプリ」を設計してみると力がつきます。
例えば、こんな題材を考えてみてください。
簡易 ToDo リスト
ポイントカード管理(ユーザとポイント履歴)
簡単な家計簿(収入・支出・カテゴリ)
やるときのポイントは、いきなりコードを書かずに、
必ず日本語でこう整理することです。
どんな「名詞」が出てくるか(クラス候補)
それぞれは「何を知っていて」「何ができるべきか」
ルールはどのクラスが知るべきか
そして、「何でも屋」になりそうなクラスができ始めたら、
この会話を思い出して分割するかどうかを考えてみてください。
