📚

📝 アプリケーションサービスをPythonで学ぶ

に公開

はじめに

ドメイン駆動設計 (DDD) の書籍に出てくる 「アプリケーションサービス」 という概念。抽象的な説明だけでピンとこないという読者も多いはずです。

この記事では、Pythonで手を動かしながら理解できるように解説します。

アプリケーションサービスとは?

こんな説明がよくあります:

  • アプリケーションサービスは ユースケースの窓口
  • ドメイン(ビジネスルール)の呼び出しをまとめる場所
  • 永続化や外部システムとの連携はリポジトリに委譲する

つまり、「何をどう順番にやるか」を組み立てる層です。

ここでは 図書館の貸出 を題材にします。

3層の役割分担

DDDの流れを図で表すとこんな感じ:

  • UI/Controller: ユーザーやAPIリクエストから呼ばれる入口
  • アプリケーションサービス: ユースケースを実現するための手順を記述
  • ドメインモデル: ビジネスルールの本体
  • リポジトリ: 保存や検索の詳細を隠蔽

Python で実装してみよう

ファイル構成は下記の様になる様にします。

% tree ./           
./
├── application_service.py
├── domain.py
├── main.py
└── repository.py

1 directory, 4 files

ドメインモデル

domain.py

from dataclasses import dataclass
from datetime import date

@dataclass(frozen=True)
class Book:
    id: str
    title: str
    is_borrowed: bool = False

    def mark_as_borrowed(self) -> "Book":
        if self.is_borrowed:
            raise ValueError("すでに貸出中です")
        return Book(id=self.id, title=self.title, is_borrowed=True)
    
@dataclass(frozen=True)
class Member:
    id: str
    name: str

@dataclass(frozen=True)
class Loan:
    book_id: str
    member_id: str
    borrowed_on: date

ポイント:

  • ルールをモデルに閉じ込める(貸出中ならエラー)
  • 不変オブジェクトにして安全に状態遷移

リポジトリ(保存場所)

repository.py

class InMemoryBookRepository:
    def __init__(self):
        self._store  = {}
    
    def find_by_id(self, book_id):
        return self._store.get(book_id)
    
    def save(self, book):
        self._store[book.id] = book

    def list_all(self):
        return list(self._store.values())
    
class InMemoryLoanRepository:
    def __init__(self):
        self._store = []

    def save(self, loan):
        self._store.append(loan)

    def list_by_member(self, member_id):
        return [l for l in self._store if l.member_id == member_id]

アプリケーションサービス(ユースケースの窓口)

application_service.py

from datetime import date
from domain import Loan

class LibraryAppService:
    def __init__(self, books, loans):
        self.books = books
        self.loans = loans

    def borrow_book(self, book_id, member_id):
        book = self.books.find_by_id(book_id)
        if book is None:
            return {"ok": False, "message": "本が見つかりません"}
        
        try:
            updated = book.mark_as_borrowed()
        except ValueError as e:
            return {"ok": False, "message": str(e)}
        
        self.books.save(updated)
        loan = Loan(book_id=book_id, member_id=member_id, borrowed_on=date.today())
        self.loans.save(loan)

        return {"ok": True, "message": "貸出しました", "loan": loan}

実行用コード

main.py

from repository import InMemoryBookRepository, InMemoryLoanRepository
from applicationservice import LibraryAppService
from domain import Book, Member

if __name__ == "__main__":
    books = InMemoryBookRepository()
    loans = InMemoryLoanRepository()
    app = LibraryAppService(books, loans)

    books.save(Book(id="b1", title="Python入門"))
    member = Member(id="m1", name="Alice")

    print(app.borrow_book("b1", member.id))  # 成功
    print(app.borrow_book("b1", member.id))  # 失敗(すでに貸出中)

## 実行例

% python ./main.py
{'ok': True, 'message': '貸出しました', 'loan': Loan(book_id='b1', member_id='m1', borrowed_on=datetime.date(2025, 9, 25))}
{'ok': False, 'message': 'すでに貸出中です'}

まとめ

  • アプリケーションサービスは「ユースケースの窓口」
  • ドメインモデルにルールを閉じ込め、リポジトリで保存を隠す
  • Pythonで手を動かせば「Scala本の抽象的な説明」が腑に落ちる

DDDの実践は「分けて考える」ことが肝心です。

Discussion