ドメイン駆動って結局なんなの? 実践してみよう!

docs

前回の記事 (ドメイン駆動って結局なんなの?(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 ではなく、PriceStock といった専用の値オブジェクトにしている。こうすることで、値段がマイナスにならないようにしたり、在庫が足りないのに購入できないようにしたり、といったビジネスルールを 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 を使って本を取得し、それぞれの Bookpurchase メソッドを呼び出している。そして、最終的に 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/

何か具体的な疑問があれば、気軽に質問してね!

コメント

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