コードに意味を与える技術

データ表現オブジェクトが語るコードの意図 - DTO, VO, Entityの役割と使い分け

Tags: DTO, VO, Entity, データ設計, 可読性, 設計原則

ソフトウェア開発において、データの構造や表現方法はコードの意図を伝える上で非常に重要です。特に、アプリケーションの異なる層間でのデータの受け渡しや、ビジネス上の概念をコードで表現する際には、どのようなデータ表現オブジェクトを選択するかが、コードの可読性、保守性、そして設計思想を大きく左右します。

本稿では、データ転送オブジェクト(DTO: Data Transfer Object)、値オブジェクト(VO: Value Object)、エンティティ(Entity)といった、代表的なデータ表現オブジェクトに焦点を当て、それぞれがどのような意図を持ち、どのように使い分けることでコードの意図を明確に伝えられるのかを解説します。

データ表現オブジェクトの役割と意図

アプリケーションは通常、複数の層に分割されます。例えば、プレゼンテーション層、アプリケーション層、ドメイン層、インフラストラクチャ層などです。これらの層の間でデータをやり取りする際や、各層内でデータを表現する際に、どのようなオブジェクトを使用するかがコードの「意図」を明確にします。

これらのオブジェクトを適切に使い分けることで、「このデータは層をまたいで転送されるためのものだ」「このデータは単なる文字列ではなく、特定の意味を持つ不変な値だ」「これはビジネス上の識別可能な実体を表しており、状態を持つ」といった開発者の意図をコードで表現できます。

DTOで伝える「転送されるデータ」の意図

DTOは、主にプレゼンテーション層とアプリケーション層の間、またはアプリケーション層とインフラストラクチャ層(例えば、外部APIとの連携)の間でデータをやり取りする際に使用されます。DTOを使用する意図は、以下の点を明確にすることです。

Before: DTOを使わない場合

例えば、データベースから取得したユーザー情報をAPIレスポンスとして返す際に、DB Entityをそのまま使用するコードです。

// DB Entity (仮定)
public class UserEntity {
    private Long id;
    private String username;
    private String passwordHash; // パスワードハッシュ
    private String email;
    private boolean isActive;
    // ... getter/setter, その他のフィールド
}

// APIコントローラー (仮定)
@RestController
@RequestMapping("/users")
public class UserController {
    @Autowired
    private UserService userService;

    @GetMapping("/{id}")
    public UserEntity getUser(@PathVariable Long id) {
        // DB Entityをそのまま返す
        return userService.findUserById(id);
    }
}

この例では、プレゼンテーション層(コントローラー)がインフラストラクチャ層(DB Entity)に直接依存しています。また、APIレスポンスにpasswordHashのような公開すべきでない情報が含まれてしまう可能性があります。これは、「API経由でユーザーの公開情報を提供したい」という意図がコードから読み取りにくく、セキュリティリスクも伴います。

After: DTOを使用する場合

APIレスポンス専用のDTOを定義し、Service層やController層でEntityからDTOへの変換を行います。

// APIレスポンス用DTO
public class UserResponseDto {
    private Long id;
    private String username;
    private String email;
    private boolean isActive;

    // コンストラクタ、getter/setter
    public UserResponseDto(Long id, String username, String email, boolean isActive) {
        this.id = id;
        this.username = username;
        this.email = email;
        this.isActive = isActive;
    }
    // ... getter/setter
}

// APIコントローラー (仮定)
@RestController
@RequestMapping("/users")
public class UserController {
    @Autowired
    private UserService userService;

    @GetMapping("/{id}")
    public UserResponseDto getUser(@PathVariable Long id) {
        UserEntity userEntity = userService.findUserById(id);
        // EntityからDTOへ変換して返す
        return new UserResponseDto(userEntity.getId(), userEntity.getUsername(), userEntity.getEmail(), userEntity.isActive());
    }
}

Afterのコードからは、「このAPIはユーザーの公開可能な情報のみを返すことを意図している」ということが、UserResponseDtoというデータ構造自体から明確に伝わります。また、プレゼンテーション層とインフラストラクチャ層の結合度が下がり、それぞれの変更が互いに影響しにくくなります。

VOで伝える「意味のある値」の意図

VOは、特定の意味を持つ値を表現し、その値に関連する振る舞いをカプセル化するために使用されます。VOを使用する意図は、以下の点を明確にすることです。

Before: プリミティブ型で済ませる場合

金額を単なるintdoubleで表現するコードです。

// 注文クラス (仮定)
public class Order {
    private int amount; // 金額 (円)

    public void processPayment(int paymentAmount) {
        if (paymentAmount < amount) {
            // エラー処理
            throw new IllegalArgumentException("支払金額が不足しています");
        }
        // 支払い処理...
    }
    // ... getter/setter, その他のメソッド
}

// 使用箇所 (仮定)
Order order = new Order();
order.setAmount(1000); // 単位や通貨が不明瞭
order.processPayment(900); // バリデーションがメソッド内に散在

このコードでは、amountが「金額」であること、そしてその単位が「円」であることはコメントや変数名でしか伝わりません。また、金額に関するバリデーション(負の金額は許されないなど)や振る舞い(税込み価格の計算など)は、それを使用する各箇所に散らばりがちです。これは、「この数値が何を表すのか」「どのような制約があるのか」という意図が不明瞭になる典型的な例です。

After: VOを使用する場合

金額を表現するMoneyというVOを定義します。

// 金額VO
public class Money {
    private final int amount; // 金額本体
    private final String currency; // 通貨

    public Money(int amount, String currency) {
        if (amount < 0) {
            throw new IllegalArgumentException("金額は負であってはなりません");
        }
        if (currency == null || currency.isEmpty()) {
            throw new IllegalArgumentException("通貨を指定してください");
        }
        this.amount = amount;
        this.currency = currency;
    }

    public int getAmount() { return amount; }
    public String getCurrency() { return currency; }

    // VOとしての同一性判定 (金額と通貨が同じなら同一)
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Money money = (Money) o;
        return amount == money.amount && Objects.equals(currency, money.currency);
    }

    @Override
    public int hashCode() {
        return Objects.hash(amount, currency);
    }

    // VOに関連する振る舞いをカプセル化
    public Money add(Money other) {
        if (!this.currency.equals(other.currency)) {
            throw new IllegalArgumentException("通貨が異なります");
        }
        return new Money(this.amount + other.amount, this.currency);
    }
}

// 注文クラス (仮定)
public class Order {
    private Money totalAmount; // 金額 (VOを使用)

    public Order(Money totalAmount) {
        if (totalAmount == null) {
             throw new IllegalArgumentException("合計金額を指定してください");
        }
        this.totalAmount = totalAmount;
    }

    public void processPayment(Money paymentAmount) {
        // VO自身が持つ情報や振る舞いを利用
        if (paymentAmount.getAmount() < totalAmount.getAmount() || !paymentAmount.getCurrency().equals(totalAmount.getCurrency())) {
             throw new IllegalArgumentException("支払金額が不足しているか、通貨が異なります");
        }
        // 支払い処理...
    }
    // ... getter/setter, その他のメソッド
}

// 使用箇所 (仮定)
Money price = new Money(1000, "JPY"); // 金額が「1000円」であることが明確
Order order = new Order(price);
Money payment = new Money(900, "JPY");
order.processPayment(payment); // VOによって意図が明確になり、関連チェックも容易に

Afterのコードでは、MoneyというVOを使用することで、「この値は単なる数値ではなく、通貨を持つ金額という概念である」という意図が強く伝わります。不変性が保証され、金額に関するバリデーションや計算といった振る舞いがMoneyクラス内にカプセル化されるため、コードの再利用性や保守性が向上します。

Entityで伝える「識別可能な実体」の意図

Entityは、一意な識別子を持ち、そのライフサイクルの中で状態が変化する可能性のあるオブジェクトです。Entityを使用する意図は、ビジネス上の「識別可能な何か」をコードで表現し、関連するドメインロジックを保持することにあります。

DB EntityがORMによってデータベースのテーブルとマッピングされることが多いですが、ドメイン層におけるEntityは、必ずしもDB構造と1対1である必要はありません。ドメインの意図を正確に反映することが重要です。

DTO, VO, Entityは、それぞれ異なる役割と意図を持っています。これらを適切に使い分けることで、コードは単なる手続きの羅列ではなく、「このデータはなぜこの形なのか」「このオブジェクトは何を表すのか」「この値にどのような制約や意味があるのか」といった、開発者の意図や設計思想を明確に語り始めます。

よくあるアンチパターンとその改善

まとめ

DTO、VO、Entityといったデータ表現オブジェクトは、それぞれが明確な目的と役割を持っています。

これらのオブジェクトを適切に設計し、使い分けることは、コードの意図を明確にし、可読性、保守性、そしてチーム開発における共通理解の促進に大きく貢献します。単にデータを保持する箱としてではなく、「このデータはどのような目的で、どのような意味を持っているのか」という意図をコードで表現するために、データ表現オブジェクトの活用を意識してみてください。これにより、コードレビューでの指摘が減り、他者の書いたコードもよりスムーズに理解できるようになるはずです。