この記事は人間が正確さを担保した上でほぼChatGPTで書いています。
TL;DR
- ソフトウェア開発において、データを正確に表現するオブジェクトを設計することは、複雑で難しい課題の一つです。
- データクラスを使用することでデータを表現する方法が一般的ですが、データクラスは低い凝集度を引き起こし、コードの保守や修正が困難になることがあります。
- そこで、データ転送オブジェクト(DTO)が登場します。本記事では、DTOの概念について探求し、データクラスとの比較、およびソフトウェア開発において効果的に使用するためのベストプラクティスを提供します。
DTOって何
データ転送オブジェクト(DTO)は、データ転送のためのオブジェクトです。フロントエンドとバックエンドなどアプリの異なる層の間でデータをやりとりする時に使います。データクラスとは異なり、DTOには保持するデータを操作するためのビジネスロジックやメソッドが期待されていません。(ここ重要)
以下は、DTOの使用方法を示すGolangの例です。
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の一般的な使用例です。つまり、異なるアプリケーションの部分間でデータを転送するためのデータのコンテナとして機能します。
データクラスはアンチパターンです
データクラスはフィールドだけを持つクラスです。オブジェクト指向デザインにおいて高い凝集度の原則に反しているため、アンチパターンとされています。高い凝集度とは、データとそのデータを操作するメソッドが密接に関連し、同じクラスに属するべきであるという原則です。しかし、データクラスはデータと操作を別々のクラスに分けてしまうため、メンテナンスや変更が困難なコードになる可能性があります。
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を使用して転送されるデータをカプセル化することができます。
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
には、ユーザーデータを転送するために必要なフィールドのみが含まれています。