Go言語でドメイン駆動設計(DDD)を実現する考え方!初心者でもわかる設計手法完全ガイド
生徒
「先生、ドメイン駆動設計っていう言葉を聞いたんですが、すごく難しそうで...。Go言語でも使えるんですか?」
先生
「ドメイン駆動設計は、略してDDDと呼ばれる設計手法です。ビジネスのルールや概念を中心にプログラムを設計する考え方で、Go言語でも十分に実現できますよ。」
生徒
「ビジネスのルールを中心にって、どういうことですか?普通のプログラミングと何が違うんですか?」
先生
「それでは、ドメイン駆動設計の基本的な考え方から、Go言語でどのように実装するのか、詳しく見ていきましょう!」
1. ドメイン駆動設計(DDD)とは何か?
ドメイン駆動設計(Domain-Driven Design、略してDDD)とは、エリック・エヴァンスという人が提唱した、ソフトウェア設計の手法です。ドメインとは、アプリケーションが扱う業務領域や問題領域のことを指します。
例えば、ECサイトを作る場合、商品の販売、在庫管理、配送、決済などがドメインになります。銀行のシステムなら、口座管理、入出金、振込などがドメインです。DDDでは、これらのビジネスルールや業務知識を、プログラムの中心に据えて設計します。
従来のプログラミングでは、データベースやWebフレームワークといった技術的な部分から設計を始めることが多くありました。しかし、DDDでは逆に、ビジネスのルールや概念を最初に考え、それを表現するためのコードを書きます。
例えるなら、家を建てるときに、まず住む人の生活スタイルや必要な部屋を考えてから設計するのと似ています。いきなり壁やドアの素材を決めるのではなく、どんな暮らしをしたいかを最初に考えるのです。
2. DDDの主要な概念
ドメイン駆動設計には、いくつかの重要な概念があります。これらを理解することで、DDDの全体像が見えてきます。
エンティティ
一意の識別子を持つオブジェクトです。例えば、ユーザーや注文は、それぞれIDで識別されます。同じ名前のユーザーがいても、IDが違えば別のユーザーです。エンティティは、ライフサイクルを持ち、状態が変化します。
値オブジェクト
識別子を持たず、属性の値だけで判断されるオブジェクトです。例えば、金額や住所などです。1000円という金額は、どの1000円も同じ価値を持ちます。値オブジェクトは、作成後に変更されない(イミュータブル)特性を持ちます。
集約
関連するエンティティや値オブジェクトをまとめたものです。集約には必ずルートとなるエンティティがあり、外部からは必ずこのルートを通してアクセスします。例えば、注文という集約には、注文明細や配送先情報が含まれます。
リポジトリ
集約を保存したり取得したりするための仕組みです。データベースとのやり取りを隠蔽し、ドメインモデルとデータベースを分離します。
ドメインサービス
エンティティや値オブジェクトに属さない、ドメインのロジックを実装する場所です。例えば、送料の計算や在庫の引当処理などです。
3. エンティティの実装例
それでは、実際のGo言語のコードで、DDDの概念を実装してみましょう。まずはエンティティから見ていきます。ECサイトの注文を例にします。
package domain
import (
"errors"
"time"
)
type OrderID int
type OrderStatus string
const (
OrderStatusPending OrderStatus = "pending"
OrderStatusConfirmed OrderStatus = "confirmed"
OrderStatusShipped OrderStatus = "shipped"
OrderStatusDelivered OrderStatus = "delivered"
)
type Order struct {
id OrderID
customerID int
items []OrderItem
totalPrice Money
status OrderStatus
createdAt time.Time
}
func NewOrder(customerID int, items []OrderItem) (*Order, error) {
if customerID <= 0 {
return nil, errors.New("顧客IDが無効です")
}
if len(items) == 0 {
return nil, errors.New("注文商品が空です")
}
order := &Order{
customerID: customerID,
items: items,
status: OrderStatusPending,
createdAt: time.Now(),
}
order.calculateTotal()
return order, nil
}
func (o *Order) Confirm() error {
if o.status != OrderStatusPending {
return errors.New("確定できない状態です")
}
o.status = OrderStatusConfirmed
return nil
}
func (o *Order) calculateTotal() {
total := 0
for _, item := range o.items {
total += item.Price * item.Quantity
}
o.totalPrice = Money{Amount: total, Currency: "JPY"}
}
このコードでは、Order構造体がエンティティです。IDを持ち、状態(ステータス)が変化します。フィールドを小文字にすることで、外部から直接変更できないようにしています。状態の変更は、Confirmメソッドなどのビジネスルールをチェックするメソッドを通してのみ行います。これにより、不正な状態変更を防ぎます。
4. 値オブジェクトの実装例
次に、値オブジェクトを実装します。値オブジェクトは、識別子を持たず、値そのものが重要なオブジェクトです。金額を表すMoneyを実装してみましょう。
package domain
import "errors"
type Money struct {
Amount int
Currency string
}
func NewMoney(amount int, currency string) (Money, error) {
if amount < 0 {
return Money{}, errors.New("金額は0以上である必要があります")
}
if currency == "" {
return Money{}, errors.New("通貨が指定されていません")
}
return Money{Amount: amount, Currency: currency}, nil
}
func (m Money) Add(other Money) (Money, error) {
if m.Currency != other.Currency {
return Money{}, errors.New("異なる通貨を加算できません")
}
return Money{
Amount: m.Amount + other.Amount,
Currency: m.Currency,
}, nil
}
func (m Money) Equals(other Money) bool {
return m.Amount == other.Amount && m.Currency == other.Currency
}
func (m Money) IsGreaterThan(other Money) bool {
if m.Currency != other.Currency {
return false
}
return m.Amount > other.Amount
}
値オブジェクトの特徴は、変更不可能(イミュータブル)であることです。Addメソッドは、既存のMoneyを変更するのではなく、新しいMoneyを作成して返します。また、Equalsメソッドで、値による等価性判定を実装しています。これにより、1000円という金額は、どの1000円も同じ価値であるというビジネスルールを表現できます。
5. リポジトリの実装例
リポジトリは、集約の永続化を担当します。DDDでは、リポジトリはインターフェースとして定義し、具体的な実装は別の層に配置します。
package domain
type OrderRepository interface {
Save(order *Order) error
FindByID(id OrderID) (*Order, error)
FindByCustomerID(customerID int) ([]*Order, error)
Delete(id OrderID) error
}
このインターフェースをドメイン層に配置することで、ドメインモデルがデータベースの実装に依存しないようにします。実際のデータベースアクセスは、インフラストラクチャ層で実装します。
package infrastructure
import (
"database/sql"
"myapp/domain"
)
type MySQLOrderRepository struct {
db *sql.DB
}
func NewMySQLOrderRepository(db *sql.DB) domain.OrderRepository {
return &MySQLOrderRepository{db: db}
}
func (r *MySQLOrderRepository) Save(order *domain.Order) error {
query := `INSERT INTO orders (customer_id, total_price, status, created_at)
VALUES (?, ?, ?, ?)`
result, err := r.db.Exec(query,
order.CustomerID(),
order.TotalPrice().Amount,
order.Status(),
order.CreatedAt(),
)
if err != nil {
return err
}
id, err := result.LastInsertId()
if err != nil {
return err
}
order.SetID(domain.OrderID(id))
return nil
}
func (r *MySQLOrderRepository) FindByID(id domain.OrderID) (*domain.Order, error) {
query := `SELECT customer_id, total_price, status, created_at
FROM orders WHERE id = ?`
var customerID int
var totalPrice int
var status string
var createdAt time.Time
err := r.db.QueryRow(query, id).Scan(
&customerID,
&totalPrice,
&status,
&createdAt,
)
if err != nil {
return nil, err
}
return domain.ReconstructOrder(
id,
customerID,
totalPrice,
domain.OrderStatus(status),
createdAt,
)
}
このように、リポジトリの実装をドメイン層から分離することで、データベースをMySQLからPostgreSQLに変更しても、ドメインモデルには影響を与えません。DDDの重要な原則の一つである、ドメインモデルの独立性が保たれます。
6. ドメインサービスの実装例
ドメインサービスは、複数のエンティティや値オブジェクトにまたがるビジネスロジックを実装します。例えば、在庫の確認と引当処理を実装してみましょう。
package domain
type StockService struct {
stockRepo StockRepository
}
func NewStockService(stockRepo StockRepository) *StockService {
return &StockService{stockRepo: stockRepo}
}
func (s *StockService) AllocateStock(order *Order) error {
for _, item := range order.Items() {
stock, err := s.stockRepo.FindByProductID(item.ProductID)
if err != nil {
return err
}
if stock.Quantity < item.Quantity {
return errors.New("在庫が不足しています")
}
if err := stock.Allocate(item.Quantity); err != nil {
return err
}
if err := s.stockRepo.Save(stock); err != nil {
return err
}
}
return nil
}
func (s *StockService) CalculateShippingFee(order *Order, destination Address) (Money, error) {
totalWeight := 0
for _, item := range order.Items() {
totalWeight += item.Weight * item.Quantity
}
distance := s.calculateDistance(order.WarehouseAddress(), destination)
baseFee := 500
weightFee := totalWeight * 10
distanceFee := distance * 5
total := baseFee + weightFee + distanceFee
return NewMoney(total, "JPY")
}
ドメインサービスは、特定のエンティティに属さないロジックを実装します。在庫の引当処理は、注文と在庫の両方に関わるため、どちらのエンティティにも属しません。こうした処理は、ドメインサービスとして独立させることで、コードの責任を明確にできます。
7. DDDのレイヤー構成
ドメイン駆動設計では、アプリケーションを複数の層に分けて構成します。Go言語でDDDを実装する際の、推奨されるディレクトリ構成を見てみましょう。
myapp/
├── domain/
│ ├── model/
│ │ ├── order.go
│ │ ├── product.go
│ │ └── customer.go
│ ├── repository/
│ │ ├── order_repository.go
│ │ └── product_repository.go
│ └── service/
│ ├── stock_service.go
│ └── pricing_service.go
├── application/
│ └── usecase/
│ ├── create_order.go
│ └── confirm_order.go
├── infrastructure/
│ ├── persistence/
│ │ ├── mysql_order_repository.go
│ │ └── mysql_product_repository.go
│ └── web/
│ └── handler/
│ └── order_handler.go
└── go.mod
ドメイン層には、ビジネスルールの中核となるモデルとリポジトリのインターフェースを配置します。アプリケーション層には、ユースケースを実装します。インフラストラクチャ層には、データベースやWebなどの技術的な実装を配置します。この構成により、ビジネスロジックと技術的な詳細が分離され、変更に強いシステムが構築できます。
8. ユビキタス言語の重要性
DDDでは、ユビキタス言語という概念が非常に重要です。ユビキタス言語とは、開発者とビジネス側の人が共通して使う言葉のことです。
共通言語を使う
コード内の変数名、関数名、クラス名は、ビジネス側の人が使う言葉と同じにします。例えば、注文を表すならOrder、在庫ならStockというように、誰が見ても分かる言葉を使います。
ドキュメントとコードの一致
仕様書に書かれている用語と、コードで使われている用語が同じであることが重要です。これにより、仕様書とコードの乖離を防ぎ、コミュニケーションが円滑になります。
会話で使う言葉
開発者同士、また開発者とビジネス側の人が会話するときも、同じ言葉を使います。プログラマーだけが分かる専門用語ではなく、誰もが理解できる業務用語を使うことで、認識の齟齬を減らせます。
9. Go言語でDDDを実践するメリット
Go言語は、ドメイン駆動設計を実践するのに適した言語です。その理由をいくつか見ていきましょう。
シンプルな構文
Go言語のシンプルな構文は、ビジネスロジックを明確に表現できます。複雑な継承よりも、構造体の組み合わせとインターフェースを使うGo言語の設計思想は、DDDの考え方とよく合います。
インターフェースの活用
Go言語の小さなインターフェースは、DDDのリポジトリやドメインサービスの定義に最適です。実装を隠蔽し、依存関係を逆転させるDDDの原則を、自然に実現できます。
パフォーマンス
Go言語の高速性は、複雑なビジネスロジックを持つシステムでも、優れたパフォーマンスを発揮します。並行処理も扱いやすく、大規模なシステム構築に適しています。
10. DDDを学ぶ際の注意点
ドメイン駆動設計は強力な手法ですが、すべてのプロジェクトに適しているわけではありません。初心者が押さえておくべきポイントを見ていきましょう。
小規模プロジェクトには過剰
DDDは、複雑なビジネスルールを持つ中規模から大規模のプロジェクトに適しています。シンプルなCRUDアプリケーションでは、かえって開発が複雑になる可能性があります。
ビジネス知識が必要
DDDを実践するには、対象となる業務への深い理解が必要です。ビジネス側の人と密にコミュニケーションを取り、業務の本質を理解することが重要です。
段階的に導入する
最初から完璧なDDDを目指さず、まずはエンティティと値オブジェクトから始めましょう。プロジェクトの成長に合わせて、徐々にDDDの概念を取り入れていくのが現実的です。
ドメイン駆動設計は、ビジネスロジックを中心に据え、技術的な詳細から独立させることで、変更に強く保守しやすいシステムを構築する手法です。Go言語のシンプルさと、DDDの明確な設計原則は相性が良く、実践的なシステム開発に役立ちます。最初は概念が難しく感じるかもしれませんが、実際のプロジェクトで少しずつ適用していくことで、その価値を実感できるでしょう。
【超入門】ゼロから始めるGo言語プログラミング:最速で「動くアプリ」を作るマンツーマン指導
「プログラミングの仕組み」が根本からわかる。Go言語でバックエンド開発の第一歩を。
本講座を受講することで、単なる文法の暗記ではなく、「プログラムがコンピュータの中でどう動いているか」という本質的な理解につながります。シンプルながら強力なGo言語(Golang)を通じて、現代のバックエンドエンジニアに求められる基礎体力を最短距離で身につけます。
具体的な開発内容と環境
【つくるもの】
ターミナル(黒い画面)上で動作する「対話型計算プログラム」や、データを整理して表示する「ミニ・ツール」をゼロから作成します。自分の書いたコードが形になる感動を体験してください。
【開発環境】
プロの現場でシェアNo.1のVisual Studio Code (VS Code)を使用します。インストールから日本語化、Go言語用の拡張機能設定まで、現場基準の環境を一緒に構築します。
この60分で得られる3つの理解
「なぜ動くのか」という設定の仕組みを理解し、今後の独学で詰まらない土台を作ります。
データの種類やメモリの概念など、他言語にも通じるプログラミングの本質を学びます。
ただ動くだけでなく、誰が見ても分かりやすい「綺麗なコード」を書くための考え方を伝授します。
※本講座は、将来的にバックエンドエンジニアやクラウドインフラに興味がある未経験者のためのエントリー講座です。マンツーマン形式により、あなたの理解度に合わせて進行します。
初めてのGo言語を一緒に学びましょう!