Go-DDDマーケットプレイスアプリケーション 商品管理機能の実装手順¶
1. 概要¶
この文書では、Go-DDDマーケットプレイスアプリケーションにおける商品管理機能の実装手順を説明します。実装はドメイン駆動設計(DDD)の原則に従い、レイヤードアーキテクチャを使用します。
1.1 商品のライフサイクル¶
以下は商品エンティティのライフサイクルを表すステート図です:
2. 実装ステップ¶
以下は商品管理機能の実装ステップの概要を表すフロー図です:
2.1 ドメインレイヤーの実装¶
2.1.1 商品エンティティの作成¶
internal/domain/entities/product.goに商品エンティティを定義します。
package entities
import (
"time"
"errors"
)
type Product struct {
Id string
Name string
Description string
Price float64
SellerId string
CreatedAt time.Time
UpdatedAt time.Time
}
func (p *Product) Validate() error {
if p.Name == "" {
return errors.New("product name cannot be empty")
}
if p.Price <= 0 {
return errors.New("product price must be greater than zero")
}
if p.SellerId == "" {
return errors.New("product must have a seller")
}
return nil
}
2.1.2 バリデーション済み商品エンティティの作成¶
internal/domain/entities/validated_product.goにバリデーション済み商品エンティティを定義します。
package entities
type ValidatedProduct struct {
Product Product
}
func NewValidatedProduct(product Product) (*ValidatedProduct, error) {
if err := product.Validate(); err != nil {
return nil, err
}
return &ValidatedProduct{Product: product}, nil
}
2.1.3 商品リポジトリインターフェースの定義¶
internal/domain/repositories/product_repository.goに商品リポジトリインターフェースを定義します。
package repositories
import (
"github.com/sklinkert/go-ddd/internal/domain/entities"
)
type ProductRepository interface {
Save(product entities.Product) error
FindAll() ([]entities.Product, error)
ById(id string) (entities.Product, error)
Update(product entities.Product) error
}
2.2 アプリケーションレイヤーの実装¶
2.2.1 商品コマンドの作成¶
internal/application/command/product_commands.goに商品コマンドを定義します。
package command
type CreateProductCommand struct {
Name string
Description string
Price float64
SellerId string
}
type UpdateProductCommand struct {
Id string
Name string
Description string
Price float64
}
2.2.2 商品クエリの作成¶
internal/application/query/product_queries.goに商品クエリを定義します。
package query
type FindAllProductsQuery struct {}
type FindProductByIdQuery struct {
Id string
}
2.2.3 商品サービスの実装¶
internal/application/services/product_service.goに商品サービスを実装します。
package services
import (
"errors"
"time"
"github.com/google/uuid"
"github.com/sklinkert/go-ddd/internal/application/command"
"github.com/sklinkert/go-ddd/internal/domain/entities"
"github.com/sklinkert/go-ddd/internal/domain/repositories"
)
type ProductService struct {
productRepo repositories.ProductRepository
sellerRepo repositories.SellerRepository
}
func NewProductService(productRepo repositories.ProductRepository, sellerRepo repositories.SellerRepository) *ProductService {
return &ProductService{
productRepo: productRepo,
sellerRepo: sellerRepo,
}
}
func (s *ProductService) Create(cmd command.CreateProductCommand) (entities.Product, error) {
// 出品者の存在確認
_, err := s.sellerRepo.ById(cmd.SellerId)
if err != nil {
return entities.Product{}, errors.New("seller not found")
}
// 商品エンティティの作成
now := time.Now()
product := entities.Product{
Id: uuid.New().String(),
Name: cmd.Name,
Description: cmd.Description,
Price: cmd.Price,
SellerId: cmd.SellerId,
CreatedAt: now,
UpdatedAt: now,
}
// バリデーション
if _, err := entities.NewValidatedProduct(product); err != nil {
return entities.Product{}, err
}
// 保存
if err := s.productRepo.Save(product); err != nil {
return entities.Product{}, err
}
return product, nil
}
func (s *ProductService) FindAll() ([]entities.Product, error) {
return s.productRepo.FindAll()
}
func (s *ProductService) ById(id string) (entities.Product, error) {
return s.productRepo.ById(id)
}
func (s *ProductService) Update(cmd command.UpdateProductCommand) (entities.Product, error) {
// 既存の商品を取得
product, err := s.productRepo.ById(cmd.Id)
if err != nil {
return entities.Product{}, err
}
// 商品を更新
product.Name = cmd.Name
product.Description = cmd.Description
product.Price = cmd.Price
product.UpdatedAt = time.Now()
// バリデーション
if _, err := entities.NewValidatedProduct(product); err != nil {
return entities.Product{}, err
}
// 保存
if err := s.productRepo.Update(product); err != nil {
return entities.Product{}, err
}
return product, nil
}
2.3 インフラストラクチャレイヤーの実装¶
2.3.1 商品リポジトリの実装(PostgreSQL)¶
internal/infrastructure/db/postgres/product_repository.goに商品リポジトリの実装を作成します。
package postgres
import (
"errors"
"gorm.io/gorm"
"github.com/sklinkert/go-ddd/internal/domain/entities"
)
type GormProductRepository struct {
db *gorm.DB
}
func NewGormProductRepository(db *gorm.DB) *GormProductRepository {
return &GormProductRepository{db: db}
}
func (r *GormProductRepository) Save(product entities.Product) error {
dbProduct := toDBProduct(product)
result := r.db.Create(&dbProduct)
return result.Error
}
func (r *GormProductRepository) FindAll() ([]entities.Product, error) {
var dbProducts []DBProduct
result := r.db.Find(&dbProducts)
if result.Error != nil {
return nil, result.Error
}
products := make([]entities.Product, len(dbProducts))
for i, dbProduct := range dbProducts {
products[i] = toDomainProduct(dbProduct)
}
return products, nil
}
func (r *GormProductRepository) ById(id string) (entities.Product, error) {
var dbProduct DBProduct
result := r.db.First(&dbProduct, "id = ?", id)
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return entities.Product{}, errors.New("product not found")
}
return entities.Product{}, result.Error
}
return toDomainProduct(dbProduct), nil
}
func (r *GormProductRepository) Update(product entities.Product) error {
dbProduct := toDBProduct(product)
result := r.db.Save(&dbProduct)
return result.Error
}
2.3.2 商品マッパーの実装¶
internal/infrastructure/db/postgres/product_mapper.goに商品マッパーを実装します。
package postgres
import (
"time"
"github.com/sklinkert/go-ddd/internal/domain/entities"
)
type DBProduct struct {
ID string `gorm:"primaryKey"`
Name string
Description string
Price float64
SellerID string
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time `gorm:"index"`
}
func (DBProduct) TableName() string {
return "products"
}
func toDBProduct(product entities.Product) DBProduct {
return DBProduct{
ID: product.Id,
Name: product.Name,
Description: product.Description,
Price: product.Price,
SellerID: product.SellerId,
CreatedAt: product.CreatedAt,
UpdatedAt: product.UpdatedAt,
}
}
func toDomainProduct(dbProduct DBProduct) entities.Product {
return entities.Product{
Id: dbProduct.ID,
Name: dbProduct.Name,
Description: dbProduct.Description,
Price: dbProduct.Price,
SellerId: dbProduct.SellerID,
CreatedAt: dbProduct.CreatedAt,
UpdatedAt: dbProduct.UpdatedAt,
}
}
2.4 インターフェースレイヤーの実装¶
2.4.1 商品コントローラーの実装¶
internal/interface/api/rest/product_controller.goに商品コントローラーを実装します。
package rest
import (
"net/http"
"time"
"github.com/labstack/echo/v4"
"github.com/sklinkert/go-ddd/internal/application/command"
"github.com/sklinkert/go-ddd/internal/application/services"
)
type ProductController struct {
productService *services.ProductService
}
func NewProductController(e *echo.Echo, productService *services.ProductService) {
controller := &ProductController{
productService: productService,
}
api := e.Group("/api/v1")
api.POST("/products", controller.Create)
api.GET("/products", controller.FindAll)
api.GET("/products/:id", controller.ById)
api.PUT("/products/:id", controller.Update)
}
type CreateProductRequest struct {
Name string `json:"name"`
Description string `json:"description"`
Price float64 `json:"price"`
SellerId string `json:"sellerId"`
}
type ProductResponse struct {
Id string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Price float64 `json:"price"`
SellerId string `json:"sellerId"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
}
func (ctrl *ProductController) Create(c echo.Context) error {
var req CreateProductRequest
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
}
cmd := command.CreateProductCommand{
Name: req.Name,
Description: req.Description,
Price: req.Price,
SellerId: req.SellerId,
}
product, err := ctrl.productService.Create(cmd)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
}
resp := ProductResponse{
Id: product.Id,
Name: product.Name,
Description: product.Description,
Price: product.Price,
SellerId: product.SellerId,
CreatedAt: product.CreatedAt.Format(time.RFC3339),
UpdatedAt: product.UpdatedAt.Format(time.RFC3339),
}
return c.JSON(http.StatusCreated, resp)
}
func (ctrl *ProductController) FindAll(c echo.Context) error {
products, err := ctrl.productService.FindAll()
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
var resp []ProductResponse
for _, product := range products {
resp = append(resp, ProductResponse{
Id: product.Id,
Name: product.Name,
Description: product.Description,
Price: product.Price,
SellerId: product.SellerId,
CreatedAt: product.CreatedAt.Format(time.RFC3339),
UpdatedAt: product.UpdatedAt.Format(time.RFC3339),
})
}
return c.JSON(http.StatusOK, resp)
}
func (ctrl *ProductController) ById(c echo.Context) error {
id := c.Param("id")
product, err := ctrl.productService.ById(id)
if err != nil {
return c.JSON(http.StatusNotFound, map[string]string{"error": err.Error()})
}
resp := ProductResponse{
Id: product.Id,
Name: product.Name,
Description: product.Description,
Price: product.Price,
SellerId: product.SellerId,
CreatedAt: product.CreatedAt.Format(time.RFC3339),
UpdatedAt: product.UpdatedAt.Format(time.RFC3339),
}
return c.JSON(http.StatusOK, resp)
}
func (ctrl *ProductController) Update(c echo.Context) error {
id := c.Param("id")
var req CreateProductRequest
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
}
cmd := command.UpdateProductCommand{
Id: id,
Name: req.Name,
Description: req.Description,
Price: req.Price,
}
product, err := ctrl.productService.Update(cmd)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
}
resp := ProductResponse{
Id: product.Id,
Name: product.Name,
Description: product.Description,
Price: product.Price,
SellerId: product.SellerId,
CreatedAt: product.CreatedAt.Format(time.RFC3339),
UpdatedAt: product.UpdatedAt.Format(time.RFC3339),
}
return c.JSON(http.StatusOK, resp)
}
2.5 アプリケーションの起動設定¶
cmd/marketplace/main.goにアプリケーションの起動設定を実装します。
package main
import (
_ "github.com/jinzhu/gorm/dialects/postgres"
"github.com/labstack/echo/v4"
"github.com/sklinkert/go-ddd/internal/application/services"
postgres2 "github.com/sklinkert/go-ddd/internal/infrastructure/db/postgres"
"github.com/sklinkert/go-ddd/internal/interface/api/rest"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"log"
)
func main() {
dsn := "host=localhost user=root password=password dbname=mydb port=9920 sslmode=disable TimeZone=Asia/Shanghai"
port := ":9090"
gormDB, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
log.Fatalf("Failed to connect to database: %v", err)
}
// データベースマイグレーション
gormDB.AutoMigrate(&postgres2.DBProduct{}, &postgres2.DBSeller{})
// リポジトリの初期化
productRepo := postgres2.NewGormProductRepository(gormDB)
sellerRepo := postgres2.NewGormSellerRepository(gormDB)
// サービスの初期化
productService := services.NewProductService(productRepo, sellerRepo)
sellerService := services.NewSellerService(sellerRepo)
// Echoサーバーの初期化
e := echo.New()
// コントローラーの初期化
rest.NewProductController(e, productService)
rest.NewSellerController(e, sellerService)
// サーバーの起動
if err := e.Start(port); err != nil {
log.Fatalf("Failed to start server: %v", err)
}
}
3. テスト¶
各レイヤーのコンポーネントに対してユニットテストを作成することが重要です。以下は商品エンティティのテスト例です。
package entities
import (
"testing"
"time"
)
func TestProduct_Validate(t *testing.T) {
tests := []struct {
name string
product Product
wantErr bool
}{
{
name: "valid product",
product: Product{
Id: "1",
Name: "Test Product",
Description: "Test Description",
Price: 100.0,
SellerId: "1",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
},
wantErr: false,
},
{
name: "empty name",
product: Product{
Id: "1",
Name: "",
Description: "Test Description",
Price: 100.0,
SellerId: "1",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
},
wantErr: true,
},
{
name: "zero price",
product: Product{
Id: "1",
Name: "Test Product",
Description: "Test Description",
Price: 0.0,
SellerId: "1",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
},
wantErr: true,
},
{
name: "empty seller id",
product: Product{
Id: "1",
Name: "Test Product",
Description: "Test Description",
Price: 100.0,
SellerId: "",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := tt.product.Validate(); (err != nil) != tt.wantErr {
t.Errorf("Product.Validate() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
4. まとめ¶
この実装手順では、Go-DDDマーケットプレイスアプリケーションの商品管理機能を、ドメイン駆動設計の原則に従って実装する方法を説明しました。各レイヤー(ドメイン、アプリケーション、インフラストラクチャ、インターフェース)の役割と実装方法を示し、クリーンなアーキテクチャを実現しています。
この実装パターンを使用することで、ビジネスロジックを明確に分離し、テスト可能で保守性の高いコードを作成することができます。また、新しい機能の追加や既存機能の変更が容易になります。