Skip to content
Published on

クリーンアーキテクチャ完全ガイド:SOLIDからヘキサゴナル、DDD、レイヤードまで — シニアへの設計原則

Authors

はじめに

ソフトウェア開発(かいはつ)(もっと)もコストがかかるのは(なん)でしょうか?初期(しょき)開発(かいはつ)ではありません。保守(ほしゅ)です。研究(けんきゅう)によると、ソフトウェアライフサイクルコストの60〜80%が保守(ほしゅ)(つい)やされます。そして保守(ほしゅ)コストを決定(けってい)する最大(さいだい)要因(よういん)アーキテクチャです。

Robert C. Martin(Uncle Bob)はこう()べました。「()いアーキテクチャはシステムを理解(りかい)しやすく、開発(かいはつ)しやすく、保守(ほしゅ)しやすく、デプロイしやすくする。」これは(たん)なる格言(かくげん)ではなく、数十年(すうじゅうねん)にわたるソフトウェアプロジェクトの成功(せいこう)失敗(しっぱい)(つらぬ)核心的(かくしんてき)原則(げんそく)です。

シニアエンジニアの面接(めんせつ)で「クリーンアーキテクチャとヘキサゴナルアーキテクチャの(ちが)いを説明(せつめい)してください」「DDDのBounded Contextとは(なん)ですか?」といった質問(しつもん)(かなら)()理由(りゆう)がここにあります。アーキテクチャを理解(りかい)することは、コードを()技術(ぎじゅつ)()えて、**システムを設計(せっけい)する能力(のうりょく)**を()つということです。

この記事(きじ)では、SOLID原則(げんそく)からレイヤード、クリーン、ヘキサゴナル、DDDまで — 現代(げんだい)ソフトウェア設計(せっけい)核心(かくしん)パターンを実践(じっせん)コードと(とも)深層(しんそう)分析(ぶんせき)します。


1. なぜアーキテクチャが重要(じゅうよう)なのか

1.1 技術的(ぎじゅつてき)負債(ふさい)実態(じったい)

技術的

( ぎじゅつてき )

負債

( ふさい )

(Technical Debt)は

時間

( じかん )

とともに

複利

( ふくり )

( ふく )

らみます。

初期

( しょき )

にアーキテクチャなしで

素早

( すばや )

開発

( かいはつ )

したプロジェクトは

以下

( いか )

のようなコスト

曲線

( きょくせん )

( えが )

きます。

コスト
  ^
  |           /  アーキテクチャなし
  |          /
  |         /
  |        /
  |      /    ___--- 良いアーキテクチャ
  |    /  ---
  |  / --
  | /--
  +-------------------> 時間

Stripeの2018年の調査(ちょうさ)によると、開発者(かいはつしゃ)業務(ぎょうむ)時間(じかん)(やく)33%を技術的(ぎじゅつてき)負債(ふさい)処理(しょり)(つい)やしています。

1.2 ()いアーキテクチャの4つの特性(とくせい)

Uncle Bobが定義(ていぎ)した()いアーキテクチャの特性(とくせい)以下(いか)(とお)りです。

  1. 理解(りかい)しやすい(あたら)しいチームメンバーが素早(すばや)くシステムを把握(はあく)
  2. 開発(かいはつ)しやすい機能(きのう)追加(ついか)影響(えいきょう)範囲(はんい)限定的(げんていてき)
  3. 保守(ほしゅ)しやすい — バグ修正(しゅうせい)とリファクタリングが安全(あんぜん)
  4. デプロイしやすい独立(どくりつ)したデプロイとロールバックが可能(かのう)

1.3 シニア面接(めんせつ)でのアーキテクチャ

シニアエンジニアの面接(めんせつ)でアーキテクチャの質問(しつもん)必須(ひっす)です。よく()るテーマは以下(いか)です。

  • SOLID原則(げんそく)実際(じっさい)のプロジェクトでどう適用(てきよう)したか
  • レイヤード vs クリーン vs ヘキサゴナルの(ちが)いと選択(せんたく)基準(きじゅん)
  • DDDの適用(てきよう)経験(けいけん)とBounded Context設計(せっけい)
  • 技術的

    ( ぎじゅつてき )

    負債

    ( ふさい )

    をどう

    管理

    ( かんり )

    解消

    ( かいしょう )

    したか

**実践(じっせん)経験(けいけん)(もと)づいた(ふか)回答(かいとう)**ができなければなりません。


2. SOLID原則(げんそく)深掘(ふかぼ)

SOLIDはRobert C. Martinが整理(せいり)したオブジェクト指向(しこう)設計(せっけい)の5つの核心(かくしん)原則(げんそく)です。すべてのアーキテクチャパターンの基礎(きそ)になります。

2.1 S — 単一(たんいつ)責任(せきにん)原則(げんそく)(Single Responsibility Principle)

クラスは変更(へんこう)理由(りゆう)が1つであるべきだ。

核心

( かくしん )

は「1つのことだけを

( おこな )

う」ではなく、**「

変更

( へんこう )

理由

( りゆう )

が1つ」**ということです。

BAD: 複数(ふくすう)変更(へんこう)理由(りゆう)()つクラス

// BAD: UserServiceがビジネスロジック、永続化、通知をすべて担当
class UserService {
  createUser(name: string, email: string) {
    // バリデーション
    if (!email.includes('@')) throw new Error('Invalid email')

    // DB保存(永続化の関心事)
    const query = `INSERT INTO users (name, email) VALUES ('${name}', '${email}')`
    database.execute(query)

    // メール送信(通知の関心事)
    const mailClient = new SmtpClient()
    mailClient.send(email, 'Welcome!', 'Thanks for joining')

    // ロギング(インフラの関心事)
    console.log(`User created: ${name}`)
  }
}

GOOD: 責任(せきにん)分離(ぶんり)した構造(こうぞう)

// GOOD: 各クラスが1つの変更理由のみを持つ
class UserValidator {
  validate(name: string, email: string): void {
    if (!email.includes('@')) throw new Error('Invalid email')
    if (name.length < 2) throw new Error('Name too short')
  }
}

class UserRepository {
  save(user: User): Promise<User> {
    return this.db.users.create({ data: user })
  }
}

class WelcomeEmailSender {
  send(email: string): Promise<void> {
    return this.mailService.send({
      to: email,
      subject: 'Welcome!',
      body: 'Thanks for joining',
    })
  }
}

class CreateUserUseCase {
  constructor(
    private validator: UserValidator,
    private repository: UserRepository,
    private emailSender: WelcomeEmailSender
  ) {}

  async execute(name: string, email: string): Promise<User> {
    this.validator.validate(name, email)
    const user = await this.repository.save({ name, email })
    await this.emailSender.send(email)
    return user
  }
}

2.2 O — 開放(かいほう)閉鎖(へいさ)原則(げんそく)(Open/Closed Principle)

拡張

( かくちょう )

( たい )

しては

( ひら )

いており、

修正

( しゅうせい )

( たい )

しては

( )

じていなければならない。

BAD: (あたら)しい決済(けっさい)手段(しゅだん)追加(ついか)のたびに既存(きそん)コード修正(しゅうせい)必要(ひつよう)

// BAD: 新しい決済手段ごとにif文追加が必要
class PaymentProcessor {
  process(payment: Payment) {
    if (payment.type === 'credit_card') {
      this.processCreditCard(payment)
    } else if (payment.type === 'paypal') {
      this.processPayPal(payment)
    } else if (payment.type === 'bitcoin') {
      // 新しい決済手段追加のたびに既存コード修正!
      this.processBitcoin(payment)
    }
  }
}

GOOD: ストラテジーパターンで拡張(かくちょう)(ひら)

// GOOD: 新しい決済手段追加時に既存コード修正不要
interface PaymentStrategy {
  process(payment: Payment): Promise<PaymentResult>
}

class CreditCardStrategy implements PaymentStrategy {
  async process(payment: Payment): Promise<PaymentResult> {
    return { success: true, transactionId: 'cc-123' }
  }
}

class PayPalStrategy implements PaymentStrategy {
  async process(payment: Payment): Promise<PaymentResult> {
    return { success: true, transactionId: 'pp-456' }
  }
}

// 新しい決済手段 = 新しいクラスを追加するだけ
class PayPayStrategy implements PaymentStrategy {
  async process(payment: Payment): Promise<PaymentResult> {
    return { success: true, transactionId: 'pp-789' }
  }
}

class PaymentProcessor {
  private strategies: Map<string, PaymentStrategy>

  constructor(strategies: Map<string, PaymentStrategy>) {
    this.strategies = strategies
  }

  async process(payment: Payment): Promise<PaymentResult> {
    const strategy = this.strategies.get(payment.type)
    if (!strategy) throw new Error(`Unknown payment type: ${payment.type}`)
    return strategy.process(payment)
  }
}

2.3 L — リスコフの置換(ちかん)原則(げんそく)(Liskov Substitution Principle)

サブタイプはベースタイプの()わりに使(つか)えなければならない。

BAD: 正方形(せいほうけい)-長方形(ちょうほうけい)問題(もんだい)

// BAD: SquareがRectangleの不変条件を違反
class Rectangle {
    protected int width;
    protected int height;

    public void setWidth(int w) { this.width = w; }
    public void setHeight(int h) { this.height = h; }
    public int getArea() { return width * height; }
}

class Square extends Rectangle {
    // LSP違反:setWidthがheightも変更
    @Override
    public void setWidth(int w) {
        this.width = w;
        this.height = w; // 親と異なる動作!
    }

    @Override
    public void setHeight(int h) {
        this.width = h;
        this.height = h;
    }
}

// クライアントコードが壊れる
void resize(Rectangle r) {
    r.setWidth(5);
    r.setHeight(10);
    assert r.getArea() == 50; // Squareだと失敗!(100)
}

GOOD: 共通(きょうつう)インターフェースで解決(かいけつ)

// GOOD: 共通インターフェースで設計
interface Shape {
    int getArea();
}

class Rectangle implements Shape {
    private final int width;
    private final int height;

    Rectangle(int width, int height) {
        this.width = width;
        this.height = height;
    }

    public int getArea() { return width * height; }
}

class Square implements Shape {
    private final int side;

    Square(int side) { this.side = side; }

    public int getArea() { return side * side; }
}

2.4 I — インターフェース分離(ぶんり)原則(げんそく)(Interface Segregation Principle)

クライアントは使(つか)わないメソッドに依存(いぞん)すべきではない。

BAD: (ふと)いインターフェース

// BAD: すべてのデバイスがすべての機能を実装しなければならない
interface SmartDevice {
  print(): void
  scan(): void
  fax(): void
  copyDocument(): void
}

// プリンターはscan、faxを実装できない
class SimplePrinter implements SmartDevice {
  print() {
    /* OK */
  }
  scan() {
    throw new Error('Not supported')
  } // 強制実装
  fax() {
    throw new Error('Not supported')
  } // 強制実装
  copyDocument() {
    throw new Error('Not supported')
  }
}

GOOD: 分離(ぶんり)されたインターフェース

// GOOD: 役割別に分離されたインターフェース
interface Printer {
  print(): void
}

interface Scanner {
  scan(): void
}

interface FaxMachine {
  fax(): void
}

// 必要なインターフェースのみ実装
class SimplePrinter implements Printer {
  print() {
    /* 印刷処理 */
  }
}

class AllInOnePrinter implements Printer, Scanner, FaxMachine {
  print() {
    /* 印刷 */
  }
  scan() {
    /* スキャン */
  }
  fax() {
    /* ファックス */
  }
}

2.5 D — 依存性(いぞんせい)逆転(ぎゃくてん)原則(げんそく)(Dependency Inversion Principle)

上位

( じょうい )

モジュールは

下位

( かい )

モジュールに

依存

( いぞん )

してはならない。

両方

( りょうほう )

とも

抽象

( ちゅうしょう )

依存

( いぞん )

すべきだ。

すべてのアーキテクチャパターンの核心(かくしん)です。

BAD: 上位(じょうい)下位(かい)直接(ちょくせつ)依存(いぞん)

// BAD: ビジネスロジックが具体的な実装に依存
class OrderService {
  private mysqlDb = new MySQLDatabase() // 具象クラスに依存
  private smtpMail = new SmtpEmailClient() // 具象クラスに依存

  createOrder(order: Order) {
    this.mysqlDb.insert('orders', order)
    this.smtpMail.send(order.userEmail, 'Order confirmed')
  }
}
// MySQLをPostgreSQLに変えるにはOrderServiceの修正が必要!

GOOD: 抽象(ちゅうしょう)依存(いぞん)

// GOOD: インターフェース(抽象)に依存
interface OrderRepository {
  save(order: Order): Promise<Order>
}

interface NotificationService {
  notify(userId: string, message: string): Promise<void>
}

class OrderService {
  constructor(
    private repository: OrderRepository, // 抽象に依存
    private notification: NotificationService // 抽象に依存
  ) {}

  async createOrder(order: Order): Promise<Order> {
    const saved = await this.repository.save(order)
    await this.notification.notify(order.userId, 'Order confirmed')
    return saved
  }
}

// インフラ層で実装
class PostgresOrderRepository implements OrderRepository {
  async save(order: Order): Promise<Order> {
    return this.prisma.order.create({ data: order })
  }
}

3. レイヤードアーキテクチャ

3.1 基本(きほん)構造(こうぞう)

レイヤードアーキテクチャは(もっと)伝統的(でんとうてき)(ひろ)使(つか)われているパターンです。(そう)間の明確(めいかく)責任(せきにん)分離(ぶんり)核心(かくしん)です。

+-----------------------------+
|   Presentation Layer        |  <- Controller, View
+-----------------------------+
|   Application Layer         |  <- Service, DTO
+-----------------------------+
|     Domain Layer            |  <- Entity, Business Logic
+-----------------------------+
|   Infrastructure Layer      |  <- Repository, External API
+-----------------------------+

依存性の方向:->  (Presentation -> Infrastructure)

3.2 実装(じっそう)(れい)

// Presentation Layer
@RestController
@RequestMapping("/api/orders")
public class OrderController {

    private final OrderService orderService;

    @PostMapping
    public ResponseEntity<OrderResponse> createOrder(
            @RequestBody CreateOrderRequest request) {
        OrderResponse response = orderService.createOrder(request);
        return ResponseEntity.status(201).body(response);
    }
}

// Application Layer
@Service
@Transactional
public class OrderService {

    private final OrderRepository orderRepository;
    private final ProductRepository productRepository;

    public OrderResponse createOrder(CreateOrderRequest request) {
        Product product = productRepository.findById(request.getProductId())
            .orElseThrow(() -> new NotFoundException("Product not found"));

        Order order = Order.create(
            request.getUserId(),
            product,
            request.getQuantity()
        );

        Order saved = orderRepository.save(order);
        return OrderResponse.from(saved);
    }
}

// Domain Layer
@Entity
public class Order {
    @Id @GeneratedValue
    private Long id;
    private String userId;
    private BigDecimal totalAmount;
    private OrderStatus status;

    public static Order create(String userId, Product product, int quantity) {
        Order order = new Order();
        order.userId = userId;
        order.totalAmount = product.getPrice().multiply(BigDecimal.valueOf(quantity));
        order.status = OrderStatus.CREATED;
        return order;
    }

    public void cancel() {
        if (this.status != OrderStatus.CREATED) {
            throw new IllegalStateException("Only CREATED orders can be cancelled");
        }
        this.status = OrderStatus.CANCELLED;
    }
}

3.3 長所(ちょうしょ)短所(たんしょ)

長所(ちょうしょ):

  • 理解

    ( りかい )

    しやすい — ほとんどの

    開発者

    ( かいはつしゃ )

    馴染

    ( なじ )

    みがある
  • 開発

    ( かいはつ )

    ( はや )

    い — シンプルなCRUDアプリに

    ( てき )

    している
  • 関心事

    ( かんしんじ )

    分離

    ( ぶんり )

    基本的

    ( きほんてき )

    なレイヤー

    分離

    ( ぶんり )

    提供

    ( ていきょう )

短所(たんしょ):

  • ドメインロジックがサービス(そう)()らばりやすい(Anemic Domain Model)
  • DB変更(へんこう)影響(えいきょう)範囲(はんい)(おお)きい
  • テスト()にインフラ依存性(いぞんせい)のモックが必要(ひつよう)

4. クリーンアーキテクチャ(Uncle Bob)

4.1 核心(かくしん)概念(がいねん)同心円(どうしんえん)構造(こうぞう)

2012年にRobert C. Martinが発表(はっぴょう)したクリーンアーキテクチャは、依存性(いぞんせい)ルール(Dependency Rule)核心(かくしん)とします。すべての依存性(いぞんせい)内側(うちがわ)()必要(ひつよう)があります。

+--------------------------------------------------+
|  Frameworks & Drivers (Web, DB, External)        |
|  +------------------------------------------+    |
|  |  Interface Adapters (Controllers, GW)    |    |
|  |  +----------------------------------+    |    |
|  |  |  Application (Use Cases)         |    |    |
|  |  |  +------------------------+      |    |    |
|  |  |  |     Entities           |      |    |    |
|  |  |  |   (Domain Models)      |      |    |    |
|  |  |  +------------------------+      |    |    |
|  |  +----------------------------------+    |    |
|  +------------------------------------------+    |
+--------------------------------------------------+

依存性の方向: 外側 -> 内側 (Frameworks -> Entities)

4.2 各層(かくそう)役割(やくわり)

Entities(エンティティ): ビジネスルールをカプセル()(もっと)変化(へんか)しにくい核心(かくしん)ロジック。

// Entities - いかなるフレームワークにも依存しない
class Order {
  private items: OrderItem[] = []
  private status: OrderStatus = 'CREATED'

  addItem(product: Product, quantity: number): void {
    if (quantity <= 0) throw new Error('Quantity must be positive')
    if (this.status !== 'CREATED') throw new Error('Cannot modify confirmed order')
    this.items.push(new OrderItem(product, quantity))
  }

  get totalAmount(): Money {
    return this.items.reduce((sum, item) => sum.add(item.subtotal), Money.zero('KRW'))
  }

  confirm(): void {
    if (this.items.length === 0) throw new Error('Cannot confirm empty order')
    this.status = 'CONFIRMED'
  }
}

Use Cases(ユースケース): アプリケーション固有(こゆう)のビジネスルール。エンティティを()()わせてシナリオを実装(じっそう)

// Use Cases
interface OrderRepository {
  save(order: Order): Promise<Order>
  findById(id: string): Promise<Order | null>
}

interface PaymentGateway {
  charge(amount: Money, paymentMethod: PaymentMethod): Promise<PaymentResult>
}

class CreateOrderUseCase {
  constructor(
    private orderRepo: OrderRepository,
    private paymentGateway: PaymentGateway,
    private eventPublisher: DomainEventPublisher
  ) {}

  async execute(input: CreateOrderInput): Promise<CreateOrderOutput> {
    const order = new Order(input.userId)

    for (const item of input.items) {
      order.addItem(item.product, item.quantity)
    }

    order.confirm()

    const paymentResult = await this.paymentGateway.charge(order.totalAmount, input.paymentMethod)

    if (!paymentResult.success) {
      throw new PaymentFailedError(paymentResult.reason)
    }

    const saved = await this.orderRepo.save(order)
    await this.eventPublisher.publish(new OrderCreatedEvent(saved.id))

    return { orderId: saved.id, totalAmount: order.totalAmount.toString() }
  }
}

4.3 Spring Boot プロジェクト構造(こうぞう)

src/main/java/com/example/shop/
  domain/                    # Entities
    model/
      Order.java
      OrderItem.java
      Money.java
    event/
      OrderCreatedEvent.java
  application/               # Use Cases
    port/
      in/
        CreateOrderUseCase.java
      out/
        OrderRepository.java
        PaymentGateway.java
    service/
      OrderApplicationService.java
  adapter/                   # Interface Adapters + Frameworks
    in/
      web/
        OrderController.java
        CreateOrderRequest.java
    out/
      persistence/
        JpaOrderRepository.java
        OrderJpaEntity.java
      payment/
        StripePaymentGateway.java
  config/
    BeanConfiguration.java

4.4 依存性(いぞんせい)ルールの威力(いりょく)

クリーンアーキテクチャの核心(かくしん)は**依存性(いぞんせい)ルール**です。内側(うちがわ)(えん)外側(そとがわ)(えん)について(なに)()りません。

// domain層 - 外部依存性 ZERO
// Order.tsのimport文を見ると:
// - express なし
// - prisma なし
// - typeorm なし
// 純粋なTypeScript/Javaコードのみ

class Order {
  // 純粋なビジネスロジックのみ含む
  // DBがMySQLでもMongoDBでも関係ない
  // WebフレームワークがExpressでもFastifyでも関係ない
}

5. ヘキサゴナルアーキテクチャ(ポート&アダプター)

5.1 Alistair Cockburnの元々(もともと)概念(がいねん)

ヘキサゴナルアーキテクチャは2005年にAlistair Cockburnが提案(ていあん)したパターンで、正式(せいしき)名前(なまえ)は「Ports and Adapters」です。六角形(ろっかくけい)(かたち)はポートの(かず)柔軟(じゅうなん)であることを表現(ひょうげん)しており、正確(せいかく)に6つという意味(いみ)ではありません。

              +----------+
     HTTP --> |          | --> PostgreSQL
              |   Port   |
     CLI -->  |    &     | --> Redis
              |  Domain  |
   gRPC -->  |   Core   | --> Kafka
              |          |
    Test -->  |          | --> Mock DB
              +----------+

  [Driving Adapters]  [Core]  [Driven Adapters]
    (入力側)        (ビジネス)    (出力側)

5.2 ポートとアダプターの概念(がいねん)

ポート(Port): ビジネスロジックが外部(がいぶ)通信(つうしん)するためのインターフェース。

  • Driving Port(インバウンドポート): 外部(がいぶ)がアプリケーションを()()すインターフェース
  • Driven Port(アウトバウンドポート): アプリケーションが外部(がいぶ)システムを()()すインターフェース

アダプター(Adapter): ポートの具体的(ぐたいてき)実装(じっそう)

  • Driving Adapter: REST Controller、CLI、gRPC Handler
  • Driven Adapter: JPA Repository、HTTP Client、Message Publisher

5.3 全体(ぜんたい)実装(じっそう)(れい)

// ===== PORTS(インターフェース定義) =====

// Driving Port(インバウンド)
interface CreateOrderPort {
  createOrder(command: CreateOrderCommand): Promise<OrderId>
}

// Driven Port(アウトバウンド)
interface LoadOrderPort {
  findById(id: string): Promise<Order | null>
}

interface SaveOrderPort {
  save(order: Order): Promise<Order>
}

interface SendNotificationPort {
  sendOrderConfirmation(order: Order): Promise<void>
}

// ===== DOMAIN CORE(核心ビジネスロジック) =====

class Order {
  private readonly id: string
  private items: OrderItem[] = []
  private status: OrderStatus = 'PENDING'

  constructor(
    id: string,
    private readonly customerId: string
  ) {
    this.id = id
  }

  addItem(productId: string, price: Money, quantity: number): void {
    this.ensureModifiable()
    this.items.push(new OrderItem(productId, price, quantity))
  }

  confirm(): void {
    if (this.items.length === 0) {
      throw new DomainError('Cannot confirm empty order')
    }
    this.status = 'CONFIRMED'
  }

  cancel(): void {
    if (this.status === 'SHIPPED') {
      throw new DomainError('Cannot cancel shipped order')
    }
    this.status = 'CANCELLED'
  }

  private ensureModifiable(): void {
    if (this.status !== 'PENDING') {
      throw new DomainError('Order is not modifiable')
    }
  }

  get totalAmount(): Money {
    return this.items.reduce((sum, item) => sum.add(item.subtotal), Money.zero('KRW'))
  }
}

// ===== APPLICATION SERVICE(ユースケース実装) =====

class OrderService implements CreateOrderPort {
  constructor(
    private readonly loadOrder: LoadOrderPort,
    private readonly saveOrder: SaveOrderPort,
    private readonly notification: SendNotificationPort
  ) {}

  async createOrder(command: CreateOrderCommand): Promise<OrderId> {
    const order = new Order(generateId(), command.customerId)

    for (const item of command.items) {
      order.addItem(item.productId, new Money(item.price, 'KRW'), item.quantity)
    }

    order.confirm()
    const saved = await this.saveOrder.save(order)
    await this.notification.sendOrderConfirmation(saved)

    return saved.id
  }
}

// ===== ADAPTERS(具体的な実装) =====

// Driving Adapter - REST Controller
class OrderRestController {
  constructor(private createOrderPort: CreateOrderPort) {}

  async handlePost(req: Request, res: Response): Promise<void> {
    const command: CreateOrderCommand = {
      customerId: req.body.customerId,
      items: req.body.items,
    }
    const orderId = await this.createOrderPort.createOrder(command)
    res.status(201).json({ orderId })
  }
}

// Driven Adapter - Database
class PostgresOrderAdapter implements LoadOrderPort, SaveOrderPort {
  constructor(private prisma: PrismaClient) {}

  async findById(id: string): Promise<Order | null> {
    const data = await this.prisma.order.findUnique({
      where: { id },
      include: { items: true },
    })
    return data ? this.toDomain(data) : null
  }

  async save(order: Order): Promise<Order> {
    await this.prisma.order.upsert({
      where: { id: order.id },
      create: this.toPersistence(order),
      update: this.toPersistence(order),
    })
    return order
  }

  private toDomain(data: PrismaOrder): Order {
    /* マッピング */
  }
  private toPersistence(order: Order): PrismaOrderData {
    /* マッピング */
  }
}

5.4 クリーンアーキテクチャとの比較(ひかく)

観点(かんてん)クリーンアーキテクチャヘキサゴナルアーキテクチャ
提案者(ていあんしゃ)Robert C. Martin (2012)Alistair Cockburn (2005)
視覚化(しかくか)同心円(どうしんえん)六角形(ろっかくけい)
核心(かくしん)アイデア依存性(いぞんせい)内側(うちがわ)()ポートとアダプターで分離(ぶんり)
(そう)(かず)4つ3つ
強調点(きょうちょうてん)依存性(いぞんせい)ルール入出力(にゅうしゅつりょく)対称性(たいしょうせい)
本質(ほんしつ)(おな)原則(げんそく)(こと)なる表現(ひょうげん)(おな)原則(げんそく)(こと)なる表現(ひょうげん)

両方

( りょうほう )

とも**

依存性

( いぞんせい )

逆転

( ぎゃくてん )

**という

( おな )

原則

( げんそく )

( もと )

づいています。


6. DDD(Domain-Driven Design)

6.1 DDDとは

DDD(Domain-Driven Design)はEric Evansが2003年の著書(ちょしょ)紹介(しょうかい)した方法論(ほうほうろん)で、複雑(ふくざつ)なビジネスドメインをソフトウェアモデルの中心(ちゅうしん)()くアプローチです。

6.2 Strategic DDD — (おお)きな()設計(せっけい)

Ubiquitous Language(ユビキタス言語(げんご)

開発者

( かいはつしゃ )

とビジネス

専門家

( せんもんか )

( おな )

用語

( ようご )

使用

( しよう )

すること。コードのクラス

( めい )

、メソッド

( めい )

がビジネス

用語

( ようご )

一致

( いっち )

する

必要

( ひつよう )

があります。

ビジネス: 「顧客が注文を作成し、決済が承認されると注文が確定される」
コード:   customer.createOrder() -> payment.approve() -> order.confirm()

ビジネス: 「配送が開始されると注文をキャンセルできない」
コード:   order.cancel() -> if (status === 'SHIPPED') throw Error

Bounded Context(バウンデッドコンテキスト)

1つのシステム(ない)でも(おな)用語(ようご)(こと)なる意味(いみ)()つことがあります。Bounded Contextは特定(とくてい)のモデルが有効(ゆうこう)境界(きょうかい)明示(めいじ)します。

+------------------+  +------------------+  +------------------+
|  Order Context   |  | Payment Context  |  | Shipping Context |
|                  |  |                  |  |                  |
|  Order           |  |  Payment         |  |  Shipment        |
|  OrderItem       |  |  Transaction     |  |  Package         |
|  Customer (id,   |  |  Customer (id,   |  |  Customer (id,   |
|   name, email)   |  |   paymentMethod) |  |   address)       |
|                  |  |                  |  |                  |
|  「Customer」=   |  |  「Customer」=   |  |  「Customer」=   |
|  注文者情報      |  |  決済者情報      |  |  受取人情報      |
+--------+---------+  +--------+---------+  +--------+---------+

6.3 Tactical DDD — コードレベル設計(せっけい)

Entity(エンティティ)

固有

( こゆう )

識別子

( しきべつし )

( )

ち、ライフサイクルを

( つう )

じて

識別子

( しきべつし )

維持

( いじ )

されるオブジェクト。

public class Order {
    private final OrderId id;  // 固有識別子
    private OrderStatus status;
    private List<OrderItem> items;

    // ビジネスロジックがエンティティ内に存在(Rich Domain Model)
    public void addItem(Product product, int quantity) {
        validateModifiable();
        OrderItem item = new OrderItem(product, quantity);
        this.items.add(item);
        recalculateTotal();
    }

    public void confirm() {
        if (items.isEmpty()) {
            throw new OrderEmptyException("Cannot confirm empty order");
        }
        this.status = OrderStatus.CONFIRMED;
        registerEvent(new OrderConfirmedEvent(this.id));
    }
}

Value Object((あたい)オブジェクト)

識別子

( しきべつし )

がなく、

属性

( ぞくせい )

( )

等価性

( とうかせい )

判断

( はんだん )

不変

( ふへん )

(Immutable)。

public record Money(BigDecimal amount, Currency currency) {

    public Money {
        if (amount.compareTo(BigDecimal.ZERO) < 0) {
            throw new IllegalArgumentException("Amount cannot be negative");
        }
        Objects.requireNonNull(currency);
    }

    public Money add(Money other) {
        validateSameCurrency(other);
        return new Money(this.amount.add(other.amount), this.currency);
    }

    public Money multiply(int quantity) {
        return new Money(this.amount.multiply(BigDecimal.valueOf(quantity)), this.currency);
    }

    public static Money zero(Currency currency) {
        return new Money(BigDecimal.ZERO, currency);
    }
}

// 使用例
Money price = new Money(BigDecimal.valueOf(15000), Currency.JPY);
Money total = price.multiply(3);  // 45000 JPY

Aggregate(アグリゲート)

関連

( かんれん )

するエンティティと

( あたい )

オブジェクトのクラスター。Aggregate Rootを

( とお )

してのみアクセス

可能

( かのう )

// OrderがAggregate Root
// OrderItemはOrderを通してのみアクセス/修正可能
public class Order {  // Aggregate Root
    private final OrderId id;
    private final CustomerId customerId;
    private List<OrderItem> items = new ArrayList<>();  // 内部エンティティ
    private ShippingAddress address;  // 値オブジェクト
    private Money totalAmount;        // 値オブジェクト

    // 外部からOrderItemを直接生成/修正不可
    // 必ずOrderを通して操作
    public void addItem(ProductId productId, Money price, int qty) {
        OrderItem item = new OrderItem(productId, price, qty);
        this.items.add(item);
        recalculateTotal();
    }
}

Aggregate設計(せっけい)ルール:

  1. Aggregate外部(がいぶ)からはRoot Entityのみ参照(さんしょう)
  2. Aggregate(かん)参照(さんしょう)はIDのみ(直接(ちょくせつ)オブジェクト参照(さんしょう)禁止(きんし)
  3. 1つのトランザクションで1つのAggregateのみ修正(しゅうせい)
  4. Aggregateは(ちい)さく(たも)つ(1 Root + 少数(しょうすう)内部(ないぶ)オブジェクト)

Domain Event(ドメインイベント)

ドメインで発生(はっせい)した重要(じゅうよう)事象(じしょう)(あらわ)すオブジェクト。

public record OrderConfirmedEvent(
    OrderId orderId,
    CustomerId customerId,
    Money totalAmount,
    Instant occurredAt
) implements DomainEvent {}

// Aggregate Rootでイベント登録
public class Order extends AbstractAggregateRoot<Order> {

    public void confirm() {
        this.status = OrderStatus.CONFIRMED;
        registerEvent(new OrderConfirmedEvent(this.id, this.customerId, this.totalAmount));
    }
}

// イベントハンドラー
@Component
public class OrderConfirmedEventHandler {

    @EventListener
    public void handleOrderConfirmed(OrderConfirmedEvent event) {
        // 決済処理トリガー
        // 在庫差し引きトリガー
        // 確認メール送信
    }
}

6.4 Event Storming

Event Stormingは、DDDでドメインを探索(たんさく)しBounded Contextを発見(はっけん)するためのワークショップ技法(ぎほう)です。

1. Domain Events(オレンジの付箋) - 過去形で記述
   「注文が作成された」 「決済が承認された」 「商品が配送された」

2. Commands(青の付箋) - イベントを発生させる行為
   「注文作成」 「決済承認」 「配送開始」

3. Aggregates(黄色の付箋) - コマンドを処理する主体
   「Order」 「Payment」 「Shipment」

4. Bounded Contexts発見 - 関連するものをグルーピング
   Order Context | Payment Context | Shipping Context

6.5 DDD + Hexagonal = 強力(きょうりょく)()()わせ

DDDの戦術的(せんじゅつてき)パターンをヘキサゴナルアーキテクチャに配置(はいち)すると(もっと)強力(きょうりょく)()()わせになります。

src/
  order/                          # Bounded Context
    domain/                       # ヘキサゴナルのCore
      model/
        Order.java                # Aggregate Root
        OrderItem.java            # Entity
        Money.java                # Value Object
      event/
        OrderConfirmedEvent.java  # Domain Event
    application/                  # ポート定義 + Use Case
      port/
        in/
          CreateOrderUseCase.java    # Driving Port
        out/
          LoadOrderPort.java         # Driven Port
          SaveOrderPort.java
      service/
        OrderApplicationService.java # Use Case実装
    infrastructure/               # アダプター
      adapter/
        in/
          web/
            OrderController.java     # Driving Adapter
        out/
          persistence/
            JpaOrderRepository.java  # Driven Adapter
          payment/
            StripePaymentAdapter.java

6.6 DDDを使(つか)うべきでないとき

DDDは強力(きょうりょく)ですが、すべてのプロジェクトに(てき)しているわけではありません。

使(つか)うべきでない場合(ばあい):

  • シンプルなCRUDアプリケーション
  • ビジネスロジックがほとんどないデータパイプライン
  • プロトタイプやMVP
  • チームがDDDへの理解(りかい)不足(ふそく)している場合(ばあい)
  • ドメイン専門家(せんもんか)との協業(きょうぎょう)不可能(ふかのう)場合(ばあい)

7. アーキテクチャ比較(ひかく)総整理(そうせいり)

7.1 比較表(ひかくひょう)

項目(こうもく)レイヤードクリーンヘキサゴナルDDD
複雑度(ふくざつど)(ひく)(ちゅう)(ちゅう)-(たか)(たか)
学習(がくしゅう)コスト(ひく)(ちゅう)(ちゅう)(たか)
(てき)したプロジェクトシンプルなCRUD(ちゅう)程度(ていど)複雑度(ふくざつど)複雑(ふくざつ)なドメイン複雑(ふくざつ)なビジネスロジック
核心(かくしん)利点(りてん)理解(りかい)容易(ようい)フレームワーク独立(どくりつ)アダプター交換(こうかん)可能(かのう)ビジネス整合(せいごう)
テスト容易性(よういせい)普通(ふつう)(たか)(たか)非常(ひじょう)(たか)
DB交換(こうかん)容易性(よういせい)困難(こんなん)容易(ようい)非常(ひじょう)容易(ようい)非常(ひじょう)容易(ようい)

7.2 選択(せんたく)フローチャート

プロジェクト開始
  |
  +-- ビジネスロジックが単純? --YES--> レイヤードアーキテクチャ
  |
  NO
  |
  +-- 外部システム統合が多い? --YES--> ヘキサゴナルアーキテクチャ
  |
  NO
  |
  +-- ドメイン専門家との協業可能? --YES--+
  |                                       |
  NO                                      v
  |                        ビジネスロジックが非常に複雑?
  |                            |           |
  |                           YES          NO
  |                            |           |
  |                            v           v
  |                     DDD + Hexagonal  クリーンアーキテクチャ
  |
  +--> クリーンアーキテクチャ(安全なデフォルト選択)

7.3 実務(じつむ)ヒント

  1. 段階的(だんかいてき)導入(どうにゅう): 最初(さいしょ)からDDDを全体(ぜんたい)適用(てきよう)せず、(もっと)複雑(ふくざつ)なモジュールから開始(かいし)
  2. 混合(こんごう)使用(しよう): 1つのシステム(ない)でモジュールごとに(こと)なるアーキテクチャを適用(てきよう)可能(かのう)
  3. チーム能力(のうりょく)考慮(こうりょ): チームの経験(けいけん)理解度(りかいど)()ったアーキテクチャを選択(せんたく)
  4. YAGNI原則(げんそく): (いま)必要(ひつよう)ない抽象化(ちゅうしょうか)(さき)(つく)らない

8. 実践(じっせん)プロジェクト:Eコマース注文(ちゅうもん)システム

8.1 ドメインモデル設計(せっけい)

Clean/Hexagonal + DDDを適用(てきよう)したEコマース注文(ちゅうもん)システムの全体(ぜんたい)実装(じっそう)です。

Money(Value Object)

public record Money(BigDecimal amount, String currency) {

    public Money {
        Objects.requireNonNull(amount, "Amount must not be null");
        Objects.requireNonNull(currency, "Currency must not be null");
        if (amount.compareTo(BigDecimal.ZERO) < 0) {
            throw new IllegalArgumentException("Amount cannot be negative");
        }
    }

    public static Money of(long amount, String currency) {
        return new Money(BigDecimal.valueOf(amount), currency);
    }

    public static Money zero(String currency) {
        return new Money(BigDecimal.ZERO, currency);
    }

    public Money add(Money other) {
        assertSameCurrency(other);
        return new Money(this.amount.add(other.amount), this.currency);
    }

    public Money multiply(int quantity) {
        return new Money(this.amount.multiply(BigDecimal.valueOf(quantity)), this.currency);
    }

    private void assertSameCurrency(Money other) {
        if (!this.currency.equals(other.currency)) {
            throw new IllegalArgumentException(
                "Currency mismatch: " + this.currency + " vs " + other.currency
            );
        }
    }
}

Order(Aggregate Root)

public class Order {
    private final String id;
    private final String customerId;
    private final List<OrderItem> items = new ArrayList<>();
    private OrderStatus status;
    private Money totalAmount;
    private final List<DomainEvent> domainEvents = new ArrayList<>();

    public Order(String id, String customerId) {
        this.id = id;
        this.customerId = customerId;
        this.status = OrderStatus.PENDING;
        this.totalAmount = Money.zero("JPY");
    }

    public void addItem(String productId, String productName, Money price, int quantity) {
        ensureModifiable();
        this.items.add(new OrderItem(productId, productName, price, quantity));
        recalculateTotal();
    }

    public void confirm() {
        if (items.isEmpty()) {
            throw new OrderDomainException("Cannot confirm empty order");
        }
        if (status != OrderStatus.PENDING) {
            throw new OrderDomainException("Only pending orders can be confirmed");
        }
        this.status = OrderStatus.CONFIRMED;
        domainEvents.add(new OrderConfirmedEvent(this.id, this.customerId, this.totalAmount));
    }

    public void cancel() {
        if (status == OrderStatus.SHIPPED || status == OrderStatus.DELIVERED) {
            throw new OrderDomainException("Cannot cancel order in status: " + status);
        }
        this.status = OrderStatus.CANCELLED;
        domainEvents.add(new OrderCancelledEvent(this.id, this.customerId));
    }

    public void markAsShipped(String trackingNumber) {
        if (status != OrderStatus.CONFIRMED) {
            throw new OrderDomainException("Only confirmed orders can be shipped");
        }
        this.status = OrderStatus.SHIPPED;
        domainEvents.add(new OrderShippedEvent(this.id, trackingNumber));
    }

    private void ensureModifiable() {
        if (status != OrderStatus.PENDING) {
            throw new OrderDomainException("Order is not modifiable");
        }
    }

    private void recalculateTotal() {
        this.totalAmount = items.stream()
            .map(OrderItem::getSubtotal)
            .reduce(Money.zero("JPY"), Money::add);
    }
}

8.2 Application Layer(Use Cases)

// Driving Port
public interface CreateOrderUseCase {
    CreateOrderResult execute(CreateOrderCommand command);
}

// Driven Ports
public interface OrderRepository {
    Optional<Order> findById(String id);
    Order save(Order order);
}

public interface EventPublisher {
    void publish(List<DomainEvent> events);
}

// Use Case実装
@Service
@Transactional
public class CreateOrderService implements CreateOrderUseCase {

    private final OrderRepository orderRepository;
    private final EventPublisher eventPublisher;
    private final IdGenerator idGenerator;

    @Override
    public CreateOrderResult execute(CreateOrderCommand command) {
        Order order = new Order(idGenerator.generate(), command.customerId());

        for (OrderItemCommand item : command.items()) {
            order.addItem(
                item.productId(),
                item.productName(),
                Money.of(item.price(), "JPY"),
                item.quantity()
            );
        }

        order.confirm();
        Order saved = orderRepository.save(order);
        eventPublisher.publish(saved.getDomainEvents());
        saved.clearDomainEvents();

        return new CreateOrderResult(saved.getId(), saved.getTotalAmount().toString());
    }
}

8.3 Infrastructure Layer(Adapters)

// Driven Adapter - JPA Repository
@Repository
public class JpaOrderRepositoryAdapter implements OrderRepository {

    private final SpringDataOrderRepository jpaRepo;
    private final OrderPersistenceMapper mapper;

    @Override
    public Optional<Order> findById(String id) {
        return jpaRepo.findById(id).map(mapper::toDomain);
    }

    @Override
    public Order save(Order order) {
        OrderJpaEntity entity = mapper.toJpaEntity(order);
        OrderJpaEntity saved = jpaRepo.save(entity);
        return mapper.toDomain(saved);
    }
}

// Driven Adapter - Kafka Event Publisher
@Component
public class KafkaEventPublisher implements EventPublisher {

    private final KafkaTemplate<String, String> kafkaTemplate;
    private final ObjectMapper objectMapper;

    @Override
    public void publish(List<DomainEvent> events) {
        for (DomainEvent event : events) {
            String topic = resolveTopic(event);
            String payload = serialize(event);
            kafkaTemplate.send(topic, event.getAggregateId(), payload);
        }
    }

    private String resolveTopic(DomainEvent event) {
        if (event instanceof OrderConfirmedEvent) return "order.confirmed";
        if (event instanceof OrderCancelledEvent) return "order.cancelled";
        if (event instanceof OrderShippedEvent) return "order.shipped";
        return "order.unknown";
    }
}

// Driving Adapter - REST Controller
@RestController
@RequestMapping("/api/v1/orders")
public class OrderController {

    private final CreateOrderUseCase createOrderUseCase;
    private final CancelOrderUseCase cancelOrderUseCase;

    @PostMapping
    public ResponseEntity<CreateOrderResponse> createOrder(
            @Valid @RequestBody CreateOrderRequest request) {
        CreateOrderCommand command = request.toCommand();
        CreateOrderResult result = createOrderUseCase.execute(command);
        return ResponseEntity.status(201).body(CreateOrderResponse.from(result));
    }

    @DeleteMapping("/{orderId}")
    public ResponseEntity<Void> cancelOrder(@PathVariable String orderId) {
        cancelOrderUseCase.execute(orderId);
        return ResponseEntity.noContent().build();
    }
}

9. テスト戦略(せんりゃく)

9.1 ドメインロジックの単体(たんたい)テスト

ドメインロジックは**外部(がいぶ)依存性(いぞんせい)なしで純粋(じゅんすい)に**テストできます。これがクリーン/ヘキサゴナルアーキテクチャの最大(さいだい)利点(りてん)です。

class OrderTest {

    @Test
    void shouldCalculateTotalAmount() {
        Order order = new Order("order-1", "customer-1");
        order.addItem("prod-1", "Keyboard", Money.of(50000, "JPY"), 2);
        order.addItem("prod-2", "Mouse", Money.of(30000, "JPY"), 1);

        assertEquals(Money.of(130000, "JPY"), order.getTotalAmount());
    }

    @Test
    void shouldNotConfirmEmptyOrder() {
        Order order = new Order("order-1", "customer-1");

        assertThrows(OrderDomainException.class, () -> order.confirm());
    }

    @Test
    void shouldNotCancelShippedOrder() {
        Order order = createConfirmedOrder();
        order.markAsShipped("TRACK-123");

        assertThrows(OrderDomainException.class, () -> order.cancel());
    }

    @Test
    void shouldRegisterDomainEventOnConfirm() {
        Order order = new Order("order-1", "customer-1");
        order.addItem("prod-1", "Keyboard", Money.of(50000, "JPY"), 1);
        order.confirm();

        List<DomainEvent> events = order.getDomainEvents();
        assertEquals(1, events.size());
        assertInstanceOf(OrderConfirmedEvent.class, events.get(0));
    }
}

class MoneyTest {

    @Test
    void shouldAddSameCurrency() {
        Money a = Money.of(1000, "JPY");
        Money b = Money.of(2000, "JPY");

        assertEquals(Money.of(3000, "JPY"), a.add(b));
    }

    @Test
    void shouldRejectDifferentCurrency() {
        Money jpy = Money.of(1000, "JPY");
        Money usd = Money.of(1, "USD");

        assertThrows(IllegalArgumentException.class, () -> jpy.add(usd));
    }
}

9.2 Use Case統合(とうごう)テスト

class CreateOrderServiceTest {

    private CreateOrderService sut;
    private OrderRepository fakeRepository;
    private EventPublisher fakePublisher;

    @BeforeEach
    void setUp() {
        fakeRepository = new InMemoryOrderRepository();
        fakePublisher = new FakeEventPublisher();
        sut = new CreateOrderService(fakeRepository, fakePublisher, new UuidIdGenerator());
    }

    @Test
    void shouldCreateAndConfirmOrder() {
        CreateOrderCommand command = new CreateOrderCommand(
            "customer-1",
            List.of(new OrderItemCommand("prod-1", "Keyboard", 50000, 2))
        );

        CreateOrderResult result = sut.execute(command);

        assertNotNull(result.orderId());
        Order saved = fakeRepository.findById(result.orderId()).orElseThrow();
        assertEquals(OrderStatus.CONFIRMED, saved.getStatus());
    }
}

// テスト用In-Memory Repository
class InMemoryOrderRepository implements OrderRepository {
    private final Map<String, Order> store = new HashMap<>();

    @Override
    public Optional<Order> findById(String id) {
        return Optional.ofNullable(store.get(id));
    }

    @Override
    public Order save(Order order) {
        store.put(order.getId(), order);
        return order;
    }
}

9.3 ArchUnitによるアーキテクチャテスト

ArchUnitを使(つか)うと、アーキテクチャルールを自動的(じどうてき)検証(けんしょう)できます。

class ArchitectureTest {

    private final JavaClasses classes = new ClassFileImporter()
        .importPackages("com.example.shop");

    @Test
    void domainShouldNotDependOnInfrastructure() {
        noClasses()
            .that().resideInAPackage("..domain..")
            .should().dependOnClassesThat()
            .resideInAPackage("..infrastructure..")
            .check(classes);
    }

    @Test
    void domainShouldNotDependOnApplication() {
        noClasses()
            .that().resideInAPackage("..domain..")
            .should().dependOnClassesThat()
            .resideInAPackage("..application..")
            .check(classes);
    }

    @Test
    void domainShouldNotUseSpringAnnotations() {
        noClasses()
            .that().resideInAPackage("..domain..")
            .should().beAnnotatedWith("org.springframework.stereotype.Service")
            .orShould().beAnnotatedWith("org.springframework.stereotype.Component")
            .check(classes);
    }
}

9.4 テストピラミッド

          /  E2E Tests  \          <- 少なく、遅い、コスト高
         / (Cypress, etc) \
        /------------------\
       / Integration Tests  \      <- 中間
      /  (Adapter Tests)     \
     /------------------------\
    /     Unit Tests           \   <- 多く、速い、コスト低
   /  (Domain + Use Case)      \
  /-----------------------------\

クリーン/ヘキサゴナルアーキテクチャでは、ドメインロジックの単体(たんたい)テスト全体(ぜんたい)テストの大部分(だいぶぶん)()めます。


10. アンチパターンと注意点(ちゅういてん)

10.1 Over-Engineering(過剰(かじょう)エンジニアリング)

( もっと )

もよくある

間違

( まちが )

いです。シンプルなCRUDアプリにDDD + Hexagonalを

適用

( てきよう )

すると

生産性

( せいさんせい )

がかえって

低下

( ていか )

します。

// BAD: ユーザー名を変えるのに7ファイルが必要?
// Command -> CommandHandler -> UseCase -> DomainService
// -> Repository -> Adapter -> Mapper
// 単純なアップデートには過剰!

// GOOD: 複雑度に合ったアーキテクチャを選択
// 単純なCRUD = レイヤード + サービスパターンで十分

10.2 Anemic Domain Model(貧血(ひんけつ)ドメインモデル)

エンティティにgetter/setterしかなく、ビジネスロジックがサービス(そう)()らばったパターン。Martin Fowlerが名付(なづ)けたアンチパターンです。

// BAD: Anemic Domain Model
class Order {
    private String status;
    public String getStatus() { return status; }
    public void setStatus(String status) { this.status = status; }
}

class OrderService {
    void cancelOrder(Order order) {
        // ビジネスロジックがサービスに存在
        if (order.getStatus().equals("SHIPPED")) {
            throw new Exception("Cannot cancel");
        }
        order.setStatus("CANCELLED");
    }
}

// GOOD: Rich Domain Model
class Order {
    private OrderStatus status;

    public void cancel() {
        // ビジネスロジックがエンティティ内に存在
        if (this.status == OrderStatus.SHIPPED) {
            throw new OrderDomainException("Cannot cancel shipped order");
        }
        this.status = OrderStatus.CANCELLED;
    }
}

10.3 God Class((かみ)クラス)

1つのクラスが(おお)すぎる責任(せきにん)()つパターン。SRP違反(いはん)極端(きょくたん)(かたち)です。

// BAD: 1つのサービスが全てを処理
class OrderGodService {
    void createOrder() { /* ... */ }
    void cancelOrder() { /* ... */ }
    void processPayment() { /* ... */ }
    void sendNotification() { /* ... */ }
    void generateInvoice() { /* ... */ }
    void updateInventory() { /* ... */ }
    // ... 数千行のメソッド
}

// GOOD: 責任ごとに分離
class CreateOrderUseCase { /* 注文作成のみ */ }
class CancelOrderUseCase { /* 注文キャンセルのみ */ }
class PaymentService { /* 決済のみ */ }
class NotificationService { /* 通知のみ */ }

10.4 Leaky Abstraction(抽象化(ちゅうしょうか)()れ)

インフラの詳細(しょうさい)がドメイン(そう)()れるパターン。

// BAD: JPAアノテーションがドメインエンティティに存在
@Entity
@Table(name = "orders")
class Order {
    @Id @GeneratedValue
    private Long id;

    @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    private List<OrderItem> items;
    // JPAというインフラ詳細がドメインに漏出
}

// GOOD: ドメインエンティティとJPAエンティティを分離
// domain/Order.java - 純粋なドメインオブジェクト
class Order {
    private final String id;
    private List<OrderItem> items;
    // フレームワーク依存性なし
}

// infrastructure/OrderJpaEntity.java - JPA専用
@Entity @Table(name = "orders")
class OrderJpaEntity {
    @Id private String id;
    @OneToMany(cascade = CascadeType.ALL)
    private List<OrderItemJpaEntity> items;
}

10.5 Resume-Driven Architecture(履歴書(りれきしょ)駆動(くどう)アーキテクチャ)

プロジェクトの要件(ようけん)ではなく、履歴書(りれきしょ)(はな)やかにするために過剰(かじょう)技術(ぎじゅつ)導入(どうにゅう)するパターン。

「私たちのプロジェクトにはMicroservices + CQRS + Event Sourcing +
Saga Pattern + GraphQL Federationが必要だ!」

実際の要件: ユーザー5人の社内ツール

結果: 6ヶ月後もデプロイできず

原則(げんそく)問題(もんだい)()った解決策(かいけつさく)(えら)べ。


クイズ

以下

( いか )

のクイズで

理解度

( りかいど )

確認

( かくにん )

しましょう。

Q1: SOLIDの依存性逆転原則(DIP)の核心は何ですか?

A: 上位(じょうい)モジュール(ビジネスロジック)が下位(かい)モジュール(DB、外部(がいぶ)サービス)に直接(ちょくせつ)依存(いぞん)せず、両方(りょうほう)とも**抽象(ちゅうしょう)(インターフェース)**に依存(いぞん)すべきという原則(げんそく)です。これがクリーンアーキテクチャとヘキサゴナルアーキテクチャの核心的(かくしんてき)基盤(きばん)です。

Q2: クリーンアーキテクチャの依存性ルール(Dependency Rule)を説明してください。

A: すべてのソースコード依存性(いぞんせい)は**内側(うちがわ)上位(じょうい)レベル)**を()必要(ひつよう)があります。Frameworks(外側(そとがわ)) -> Adapters -> Use Cases -> Entities(内側(うちがわ))。内側(うちがわ)(えん)外側(そとがわ)(えん)について(なに)()りません。

Q3: ヘキサゴナルアーキテクチャでDriving AdapterとDriven Adapterの違いは何ですか?

A: **Driving Adapter(インバウンド)**は外部(がいぶ)がアプリケーションを()()すアダプターです(REST Controller、CLI、gRPC Handler)。**Driven Adapter(アウトバウンド)**はアプリケーションが外部(がいぶ)システムを()()すためのアダプターです(DB Repository、HTTP Client、Message Publisher)。入出力(にゅうしゅつりょく)対称性(たいしょうせい)核心(かくしん)概念(がいねん)です。

Q4: DDDのAggregate設計ルール4つを説明してください。

A:

  1. Aggregate外部(がいぶ)からはRoot Entityのみ参照(さんしょう)
  2. Aggregate(かん)参照(さんしょう)IDのみ直接(ちょくせつ)オブジェクト参照(さんしょう)禁止(きんし)
  3. 1つのトランザクションで1つのAggregateのみ修正(しゅうせい)
  4. Aggregateは**(ちい)さく(たも)つ**(1 Root + 少数(しょうすう)内部(ないぶ)オブジェクト)

これらのルールはデータの一貫性(いっかんせい)保証(ほしょう)し、拡張性(かくちょうせい)確保(かくほ)するためのものです。

Q5: Anemic Domain Modelがアンチパターンである理由は何ですか?

A: Anemic Domain Modelはエンティティにgetter/setterしかなく、ビジネスロジックがサービス(そう)()らばったパターンです。アンチパターンである理由(りゆう)は:(1)ビジネスルールが複数(ふくすう)のサービスに重複(じゅうふく)/分散(ぶんさん)して一貫性(いっかんせい)(くず)れやすく、(2)ドメインオブジェクトが(たん)なるデータコンテナに()してOOPの利点(りてん)(カプセル())を(うしな)い、(3)ビジネスルール変更(へんこう)()影響(えいきょう)範囲(はんい)把握(はあく)困難(こんなん)になります。Rich Domain Modelではビジネスロジックがエンティティ(ない)存在(そんざい)し、凝集度(ぎょうしゅうど)(たか)変更(へんこう)安全(あんぜん)です。


参考(さんこう)資料(しりょう)

書籍(しょせき)

  1. Robert C. Martin, Clean Architecture: A Craftsman's Guide to Software Structure and Design (2017)
  2. Eric Evans, Domain-Driven Design: Tackling Complexity in the Heart of Software (2003)
  3. Vaughn Vernon, Implementing Domain-Driven Design (2013)
  4. Vaughn Vernon, Domain-Driven Design Distilled (2016)
  5. Martin Fowler, Patterns of Enterprise Application Architecture (2002)
  6. Robert C. Martin, Clean Code: A Handbook of Agile Software Craftsmanship (2008)

記事(きじ)・ブログ

  1. Alistair Cockburn, Hexagonal Architecturehttps://alistair.cockburn.us/hexagonal-architecture/
  2. Robert C. Martin, The Clean Architecture (2012) — https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html
  3. Martin Fowler, AnemicDomainModelhttps://martinfowler.com/bliki/AnemicDomainModel.html
  4. Martin Fowler, BoundedContexthttps://martinfowler.com/bliki/BoundedContext.html

ツール・フレームワーク

  1. ArchUnit — https://www.archunit.org/
  2. Spring Modulith — https://spring.io/projects/spring-modulith
  3. NestJS CQRS Module — https://docs.nestjs.com/recipes/cqrs

講演(こうえん)動画(どうが)

  1. Uncle Bob, Clean Architecture and Design (NDC Conference)
  2. Vaughn Vernon, Strategic Domain-Driven Design (InfoQ)
  3. Alberto Brandolini, Event Storminghttps://www.eventstorming.com/
  4. Netflix Engineering Blog, Domain-Driven Design at Netflix