前回の記事 (ドメイン駆動って結局なんなの?(Java初心者向け) – Hello Java World) では、ドメイン駆動設計(DDD)って何?っていう基本的な考え方について話したよね。今回は、そのドメイン駆動設計を実際にどうやってコードに落とし込んでいくのか、具体的な例を交えながら見ていこう!
ドメインモデルってどう作るの?
DDDで一番大事なのが「ドメインモデル」を作ること。これは、ビジネスのルールや振る舞いをそのままコードで表現したものなんだ。例えば、オンライン書店を想像してみて。
悪い例:ただのデータクラス
Java
class Book {
String title;
String author;
int price;
int stock;
}
これだと、ただのデータの入れ物だよね。本のタイトルや著者、値段、在庫を管理するだけ。でも、本には「購入される」とか「在庫が減る」とか、いろんな「振る舞い」があるはずだよね?
良い例:振る舞いを持つドメインモデル
Java
class Book {
private String title;
private String author;
private Price price; // 値段も専用のクラスにするのがDDDっぽい
private Stock stock; // 在庫も専用のクラスに
// コンストラクタ
public Book(String title, String author, Price price, Stock stock) {
// ここで値のバリデーションとかもできる
if (title == null || title.isEmpty()) {
throw new IllegalArgumentException("タイトルは必須です。");
}
this.title = title;
this.author = author;
this.price = price;
this.stock = stock;
}
// 本を購入する振る舞い
public void purchase(int quantity) {
// 在庫が足りるかチェック
if (!stock.hasEnough(quantity)) {
throw new IllegalArgumentException("在庫が足りません。");
}
stock.decrease(quantity); // 在庫を減らす
System.out.println(quantity + "冊の「" + title + "」を購入しました。残り在庫:" + stock.getValue());
}
// 在庫を補充する振る舞い
public void addStock(int quantity) {
stock.increase(quantity);
System.out.println(quantity + "冊の「" + title + "」を追加しました。現在在庫:" + stock.getValue());
}
// getter (安易なsetterは避けることが多い。振る舞いを介して状態を変更する)
public String getTitle() { return title; }
public String getAuthor() { return author; }
public Price getPrice() { return price; }
public Stock getStock() { return stock; }
}
// 値オブジェクト:Price
class Price {
private final int value; // finalにすることで不変にする
public Price(int value) {
if (value < 0) {
throw new IllegalArgumentException("値段は0以上である必要があります。");
}
this.value = value;
}
public int getValue() { return value; }
// 等価性の比較もオーバーライドすると良い
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Price price = (Price) o;
return value == price.value;
}
@Override
public int hashCode() {
return Integer.hashCode(value);
}
}
// 値オブジェクト:Stock
class Stock {
private int value;
public Stock(int value) {
if (value < 0) {
throw new IllegalArgumentException("在庫は0以上である必要があります。");
}
this.value = value;
}
public boolean hasEnough(int quantity) {
return this.value >= quantity;
}
public void decrease(int quantity) {
if (!hasEnough(quantity)) {
throw new IllegalArgumentException("在庫が足りません。");
}
this.value -= quantity;
}
public void increase(int quantity) {
this.value += quantity;
}
public int getValue() { return value; }
// Stockは状態が変わるので値オブジェクトではない場合もあるけど、ここでは分かりやすさ優先
}
どう? Book クラスの中に purchase (購入) や addStock (在庫追加) といった「振る舞い」が入ってるよね。そして、値段や在庫もただの int ではなく、Price や Stock といった専用の値オブジェクトにしている。こうすることで、値段がマイナスにならないようにしたり、在庫が足りないのに購入できないようにしたり、といったビジネスルールを Book クラスの中でしっかり管理できるんだ。
リポジトリって何?
ドメインモデルは、ビジネスの振る舞いを表現したものだけど、これをデータベースに保存したり、データベースから読み込んだりする必要があるよね。ここで登場するのが「リポジトリ」だ。リポジトリは、ドメインモデルの永続化(保存)と再構築(読み込み)の責任を持つインターフェースなんだ。
良い例:リポジトリのインターフェースと実装
Java
// BookRepository.java (インターフェース)
// 「Bookを保存したり、IDからBookを見つけたりする」という契約
interface BookRepository {
void save(Book book); // 本を保存する
Book findById(String bookId); // IDから本を見つける
// 他にも findAll(), remove() など
}
// InMemoryBookRepository.java (インターフェースの実装例)
// リポジトリの実装は、データベースの種類に依存する部分
class InMemoryBookRepository implements BookRepository {
private Map<String, Book> storage = new HashMap<>();
@Override
public void save(Book book) {
// 実際はここでDBに保存する処理を書く
storage.put(book.getTitle(), book); // 仮にタイトルをキーに
System.out.println("書籍「" + book.getTitle() + "」を保存しました。");
}
@Override
public Book findById(String bookId) {
// 実際はここでDBから読み込む処理を書く
System.out.println("書籍ID「" + bookId + "」で検索中...");
return storage.get(bookId); // 仮にタイトルをキーに
}
}
// DatabaseBookRepository.java (DBの実装例のイメージ)
/*
class DatabaseBookRepository implements BookRepository {
private Connection connection; // DB接続
public DatabaseBookRepository(Connection connection) {
this.connection = connection;
}
@Override
public void save(Book book) {
// SQLを使ってDBに保存する処理
// PreparedStatement ps = connection.prepareStatement("INSERT INTO books (...) VALUES (...)");
// ...
}
@Override
public Book findById(String bookId) {
// SQLを使ってDBから読み込む処理
// ResultSet rs = statement.executeQuery("SELECT * FROM books WHERE id = ?");
// ...
return null; // 読み込んだデータからBookオブジェクトを再構築
}
}
*/
リポジトリのポイントは、ドメインモデルはデータベースの仕組みを知らなくていいってこと。ドメインモデルはあくまでビジネスロジックに集中する。データベースとのやり取りはリポジトリの仕事なんだ。これによって、データベースをPostgreSQLからMongoDBに変えても、ドメインモデルのコードはほとんど変えなくて済むようになるんだ!
ドメインサービスって何?
DDDでは、特定のエンティティ(ここでは Book)の振る舞いには収まらない、複数のエンティティをまたがるようなビジネスロジックや、特定のエンティティに属さない共通の処理を扱うために「ドメインサービス」を使うことがあるよ。
例えば、「複数の本をまとめて注文する」という機能があったとする。これは、Book クラス単独の振る舞いではなく、Order という別のエンティティとのやり取りも発生するよね。
良い例:ドメインサービス
Java
// OrderService.java (ドメインサービス)
class OrderService {
private BookRepository bookRepository;
// 実際には、UserRepository や OrderRepository なども必要になるかも
public OrderService(BookRepository bookRepository) {
this.bookRepository = bookRepository;
}
// 複数冊の本をまとめて購入する処理
public Order createOrder(String userId, Map<String, Integer> bookQuantities) { // bookQuantitiesは書籍IDと数量のマップ
List<Book> purchasedBooks = new ArrayList<>();
double totalPrice = 0;
for (Map.Entry<String, Integer> entry : bookQuantities.entrySet()) {
String bookId = entry.getKey();
int quantity = entry.getValue();
Book book = bookRepository.findById(bookId); // リポジトリを使って本を取得
if (book == null) {
throw new IllegalArgumentException("書籍ID: " + bookId + " の本は見つかりませんでした。");
}
book.purchase(quantity); // 本の購入振る舞いを呼び出す
purchasedBooks.add(book);
totalPrice += book.getPrice().getValue() * quantity;
bookRepository.save(book); // 在庫が減ったので保存し直す
}
// ここでOrderオブジェクトを生成して、OrderRepositoryで保存する処理などが続く
Order newOrder = new Order(userId, purchasedBooks, totalPrice);
System.out.println(userId + "さんの注文が完了しました。合計金額:" + totalPrice);
return newOrder;
}
}
// 仮のOrderクラス
class Order {
private String userId;
private List<Book> books;
private double totalAmount;
public Order(String userId, List<Book> books, double totalAmount) {
this.userId = userId;
this.books = books;
this.totalAmount = totalAmount;
}
// getter など
}
OrderService は、BookRepository を使って本を取得し、それぞれの Book の purchase メソッドを呼び出している。そして、最終的に Order を作成するという一連のビジネスプロセスを扱っているよね。このように、複数のエンティティを協調させるようなロジックはドメインサービスで表現するのが適しているんだ。
アプリケーションサービスって何?
DDDでは、ユーザーからの入力(Web APIのリクエストとか)を受け取って、ドメインモデルやリポジトリ、ドメインサービスを使って処理を実行し、結果をユーザーに返すのが「アプリケーションサービス」の役割だ。
良い例:アプリケーションサービス
Java
// BookApplicationService.java
class BookApplicationService {
private BookRepository bookRepository;
private OrderService orderService; // ドメインサービスも使う
public BookApplicationService(BookRepository bookRepository, OrderService orderService) {
this.bookRepository = bookRepository;
this.orderService = orderService;
}
// 書籍を購入するユースケース
public void purchaseBook(String bookId, int quantity, String userId) {
try {
// トランザクション開始(実際はフレームワークの機能を使うことが多い)
Book book = bookRepository.findById(bookId);
if (book == null) {
throw new IllegalArgumentException("指定された書籍は見つかりません。");
}
book.purchase(quantity); // ドメインモデルの振る舞いを呼び出す
bookRepository.save(book); // 変更されたドメインモデルを保存
// トランザクションコミット
System.out.println(book.getTitle() + "を" + quantity + "冊購入処理しました。");
} catch (Exception e) {
// トランザクションロールバック
System.err.println("書籍購入中にエラーが発生しました: " + e.getMessage());
throw e; // エラーを上位に伝える
}
}
// 複数の書籍をまとめて購入するユースケース(ドメインサービスを使う)
public void purchaseMultipleBooks(String userId, Map<String, Integer> bookQuantities) {
try {
// トランザクション開始
orderService.createOrder(userId, bookQuantities); // ドメインサービスを呼び出す
// トランザクションコミット
System.out.println(userId + "さんの複数書籍の購入処理が完了しました。");
} catch (Exception e) {
// トランザクションロールバック
System.err.println("複数書籍の購入中にエラーが発生しました: " + e.getMessage());
throw e;
}
}
// 新しい書籍を登録するユースケース
public void registerNewBook(String title, String author, int priceValue, int stockValue) {
try {
// 値オブジェクトを生成
Price price = new Price(priceValue);
Stock stock = new Stock(stockValue);
// ドメインモデルを生成
Book newBook = new Book(title, author, price, stock);
bookRepository.save(newBook); // リポジトリを使って保存
System.out.println("新しい書籍「" + title + "」を登録しました。");
} catch (Exception e) {
System.err.println("書籍登録中にエラーが発生しました: " + e.getMessage());
throw e;
}
}
}
アプリケーションサービスは、具体的なユースケース(「書籍を購入する」とか「新しい書籍を登録する」とか)に対応するメソッドを持つんだ。ここから、ドメインモデルやリポジトリ、ドメインサービスを呼び出して、一連のビジネス処理を実行する。アプリケーションサービスは、ビジネスロジック自体は持たず、ドメインモデルたちに処理を「依頼」するイメージだね。トランザクション管理などもここで行われることが多いよ。
まとめ
どうだったかな? DDDの基本的な要素をざっと見てきたけど、なんとなくイメージは掴めたかな?
- ドメインモデル:ビジネスのルールや振る舞いをコードで表現したもの。
- 値オブジェクト:概念的なまとまりを持つ値の集合で、不変(一度作ったら変更しない)にするのが基本。
- リポジトリ:ドメインモデルの永続化と再構築を担当する。
- ドメインサービス:複数のエンティティをまたがるようなビジネスロジックを扱う。
- アプリケーションサービス:ユーザーからのリクエストを受け取り、ドメイン層を調整してビジネスユースケースを実行する。
これらがそれぞれ役割分担することで、ビジネスロジックがカオスにならず、変更にも強くなるんだ。最初は難しく感じるかもしれないけど、実践を重ねるうちにDDDの良さがきっとわかってくるはず! 焦らず、一つずつ試してみてね。
次回の記事もぜひ見てみてね! -> https://moritama321.hatenablog.com/
何か具体的な疑問があれば、気軽に質問してね!


コメント