オブジェクト指向のDTOを理解する

この記事は人間が正確さを担保した上でほぼChatGPTで書いています。

# TL;DR

- ソフトウェア開発において、データを正確に表現するオブジェクトを設計することは、複雑で難しい課題の一つです。
- データクラスを使用することでデータを表現する方法が一般的ですが、データクラスは低い凝集度を引き起こし、コードの保守や修正が困難になることがあります。
- そこで、データ転送オブジェクト（DTO）が登場します。本記事では、DTOの概念について探求し、データクラスとの比較、およびソフトウェア開発において効果的に使用するためのベストプラクティスを提供します。


# DTOって何

データ転送オブジェクト（DTO）は、データ転送のためのオブジェクトです。フロントエンドとバックエンドなどアプリの異なる層の間でデータをやりとりする時に使います。データクラスとは異なり、DTOには保持するデータを操作するためのビジネスロジックやメソッドが***期待されていません。***(ここ重要)

以下は、DTOの使用方法を示すGolangの例です。

```Go
type PersonDTO struct {
    FirstName string `json:"firstName"`
    LastName  string `json:"lastName"`
    Age       int    `json:"age"`
}

func main() {
    person := PersonDTO{
        FirstName: "John",
        LastName:  "Doe",
        Age:       30,
    }
    jsonData, err := json.Marshal(person)
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Println(string(jsonData))
}
```

この例では、FirstName、LastName、Age を持つ PersonDTO 構造体を定義しています。ただし、PersonDTO 構造体にはメソッドは含まれていません。

代わりに、 json パッケージを使用して、 PersonDTO オブジェクトをJSON形式に変換し、別のレイヤーやシステムに転送できます。これは、DTOの一般的な使用例です。つまり、異なるアプリケーションの部分間でデータを転送するためのデータのコンテナとして機能します。

# データクラスはアンチパターンです

データクラスはフィールドだけを持つクラスです。オブジェクト指向デザインにおいて高い凝集度の原則に反しているため、アンチパターンとされています。高い凝集度とは、データとそのデータを操作するメソッドが密接に関連し、同じクラスに属するべきであるという原則です。しかし、データクラスはデータと操作を別々のクラスに分けてしまうため、メンテナンスや変更が困難なコードになる可能性があります。

```Golang
type Person struct {
    FirstName string `json:"firstName"`
    LastName  string `json:"lastName"`
    Age       int    `json:"age"`
}

type Order struct {
    OrderID     int
    Customer    Person
    ProductID   int
    Quantity    int
    OrderDate   time.Time
}

func (o *Order) SendEmailNotification() error {
    // Send email notification to the customer
    name := o.Customer.FirstName + o.Customer.LastName
    body := fmt.Sprintf("Hi %s, your order with ID %d has been shipped!", name, o.OrderID)
    return email.Send(o.Customer.Email, "Order Shipped", body)
}
```

`Person` 構造体を用いて`SendEmailNotification`　の中で顧客のフルネームを計算しています。この程度の規模ではまだ問題になりませんが、`Person` のデータを離れた場所で操作しているので低凝集なコードになっています。今後別の場所で顧客のフルネームが欲しくなった場合同じコードがコピーされていくことになるので容易に修正漏れやバグの原因になってしまいます。

# DTOとデータクラスはどう違うのか

データクラスはデータを保持し、関連するメソッドを別クラスで実装します。それに対しDTOは単に転送されるデータを表すためだけに使用されます(***メソッドが期待されない***)。このためDTOはビジネスロジックを含まず、操作も行わないため、低凝集になる可能性は低くなります。

ここでDTOクラスの例と、特定のシナリオでの使い方を紹介します。例えば、マイクロサービス・アーキテクチャで、異なるサービスがHTTPリクエストを通じて互いに通信するとします。サービス間でデータを転送する場合、DTOを使用して転送されるデータをカプセル化することができます。

```Golang
type UserDTO struct {
    ID       int    `json:"id"`
    Name     string `json:"name"`
    Email    string `json:"email"`
    Password string `json:"password"`
}

// In a user service, we can use the following code to retrieve user data and transfer it to another service
func (u *UserService) GetUserByID(id int) (*UserDTO, error) {
    user, err := u.userRepo.GetUserByID(id)
    if err != nil {
        return nil, err
    }

    // Map user data to UserDTO
    userDTO := &UserDTO{
        ID:       user.ID,
        Name:     user.Name,
        Email:    user.Email,
        Password: "", // never send passwords in DTOs
    }

    return userDTO, nil
}

// In a different service, we can use the following code to receive user data and convert it back to User object
func (o *OrderService) CreateOrder(userDTO *UserDTO) error {
    // Map UserDTO data to User object
    user := &User{
        ID:    userDTO.ID,
        Name:  userDTO.Name,
        Email: userDTO.Email,
    }

    // create order with user object
    err := o.orderRepo.CreateOrder(user)
    if err != nil {
        return err
    }

    return nil
}
```

この例では、マイクロサービスアーキテクチャの `UserService` と `OrderService` の間でユーザーデータを転送するために、`UserDTO` を使用します。 `UserDTO` には、ユーザーデータを転送するために必要なフィールドのみが含まれています。