Value Object

2 件の記事

Contents

Actively use Value Objects for variables representing important domain concepts. (重要なドメイン概念を表す変数には、Value Objectを積極的に使用する)

解説

プリミティブ型(文字列、数値など)だけでドメイン概念を表現すると、型安全性が失われ、無効な値の混入リスクが高まります。Value Objectを使用することで、ドメインルールをカプセル化し、型システムの力でバグを防止できます。重要なドメイン概念(メールアドレス、金額、ユーザーIDなど)をValue Objectとして定義することで、コードの表現力と安全性が向上します。

具体例

// 悪い例(プリミティブ型のみ使用)
type User struct {
    ID    string
    Email string
}

func CreateUser(id, email string) (*User, error) {
    // バリデーションが散在
    if id == "" || email == "" {
        return nil, errors.New("invalid input")
    }
    return &User{ID: id, Email: email}, nil
}

func SendEmail(email string) error {
    // emailの妥当性チェックが毎回必要
    if !strings.Contains(email, "@") {
        return errors.New("invalid email")
    }
    return emailClient.Send(email, "Hello")
}

// 良い例(Value Object使用)
type UserID struct {
    value string
}

func NewUserID(id string) (UserID, error) {
    if id == "" {
        return UserID{}, errors.New("user ID cannot be empty")
    }
    return UserID{value: id}, nil
}

func (u UserID) String() string {
    return u.value
}

type Email struct {
    value string
}

func NewEmail(email string) (Email, error) {
    if !strings.Contains(email, "@") {
        return Email{}, errors.New("invalid email format")
    }
    return Email{value: email}, nil
}

func (e Email) String() string {
    return e.value
}

type User struct {
    ID    UserID
    Email Email
}

func CreateUser(id UserID, email Email) *User {
    // Value Objectは既に検証済みなので追加チェック不要
    return &User{ID: id, Email: email}
}

func SendEmail(email Email) error {
    // Emailは既に妥当性が保証されている
    return emailClient.Send(email.String(), "Hello")
}

参考リンク

Contents

Use Value Objects for both arguments and return values when possible. (可能な限り、引数と戻り値の両方にValue Objectを使用する)

解説

関数のシグネチャでValue Objectを使用することで、型安全性が向上し、無効な値の受け渡しを防げます。プリミティブ型では、引数の順序を間違えたり、無効な値を渡したりするミスが発生しやすくなります。Value Objectを使用することで、コンパイル時に型チェックが働き、ドメインルール違反を早期に検出できます。これにより、実行時エラーではなくコンパイルエラーとして問題を発見できます。

具体例

// 悪い例(プリミティブ型の引数)
func TransferMoney(fromAccount, toAccount string, amount float64) error {
    // 引数の順序ミスが起きやすい
    // 負の金額が渡される可能性がある
    if amount <= 0 {
        return errors.New("invalid amount")
    }
    // ...
}

func main() {
    // 引数の順序を間違える可能性
    TransferMoney("ACC002", "ACC001", 100.0)
    // 負の値を渡せてしまう
    TransferMoney("ACC001", "ACC002", -50.0)
}

// 良い例(Value Objectの引数と戻り値)
type AccountID struct {
    value string
}

func NewAccountID(id string) (AccountID, error) {
    if id == "" || !strings.HasPrefix(id, "ACC") {
        return AccountID{}, errors.New("invalid account ID")
    }
    return AccountID{value: id}, nil
}

type Amount struct {
    value float64
}

func NewAmount(amount float64) (Amount, error) {
    if amount <= 0 {
        return Amount{}, errors.New("amount must be positive")
    }
    return Amount{value: amount}, nil
}

func (a Amount) Value() float64 {
    return a.value
}

type TransferResult struct {
    TransactionID string
    CompletedAt   time.Time
}

func TransferMoney(from, to AccountID, amount Amount) (TransferResult, error) {
    // Value Objectは既に検証済み
    // 型が異なるため引数の順序ミスも防げる
    result := TransferResult{
        TransactionID: generateID(),
        CompletedAt:   time.Now(),
    }
    return result, nil
}

func main() {
    from, _ := NewAccountID("ACC001")
    to, _ := NewAccountID("ACC002")
    amount, _ := NewAmount(100.0)

    // 型安全性により、引数の順序ミスはコンパイルエラーになる
    result, err := TransferMoney(from, to, amount)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Transfer completed: %s\n", result.TransactionID)
}

参考リンク