Skip to content

Go-DDDマーケットプレイスアプリケーション 出品者管理機能の実装手順

1. 概要

この文書では、Go-DDDマーケットプレイスアプリケーションにおける出品者管理機能の実装手順を説明します。実装はドメイン駆動設計(DDD)の原則に従い、レイヤードアーキテクチャを使用します。

1.1 出品者のライフサイクル

以下は出品者エンティティのライフサイクルを表すステート図です:

uml diagram

2. 実装ステップ

以下は出品者管理機能の実装ステップの概要を表すフロー図です:

uml diagram

2.1 ドメインレイヤーの実装

2.1.1 出品者エンティティの作成

  1. internal/domain/entities/seller.go に出品者エンティティを定義します。
package entities

import (
    "time"
    "errors"
    "regexp"
)

type Seller struct {
    Id        string
    Name      string
    Email     string
    CreatedAt time.Time
    UpdatedAt time.Time
    DeletedAt *time.Time
}

func (s *Seller) Validate() error {
    if s.Name == "" {
        return errors.New("seller name cannot be empty")
    }

    if s.Email == "" {
        return errors.New("seller email cannot be empty")
    }

    // メールアドレスの簡易バリデーション
    emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`)
    if !emailRegex.MatchString(s.Email) {
        return errors.New("invalid email format")
    }

    return nil
}

func (s *Seller) UpdateName(name string) error {
    if name == "" {
        return errors.New("seller name cannot be empty")
    }
    s.Name = name
    s.UpdatedAt = time.Now()
    return nil
}

func (s *Seller) UpdateEmail(email string) error {
    if email == "" {
        return errors.New("seller email cannot be empty")
    }

    // メールアドレスの簡易バリデーション
    emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`)
    if !emailRegex.MatchString(email) {
        return errors.New("invalid email format")
    }

    s.Email = email
    s.UpdatedAt = time.Now()
    return nil
}

func (s *Seller) SoftDelete() {
    now := time.Now()
    s.DeletedAt = &now
}

func (s *Seller) IsDeleted() bool {
    return s.DeletedAt != nil
}

2.1.2 バリデーション済み出品者エンティティの作成

  1. internal/domain/entities/validated_seller.go にバリデーション済み出品者エンティティを定義します。
package entities

type ValidatedSeller struct {
    Seller Seller
}

func NewValidatedSeller(seller Seller) (*ValidatedSeller, error) {
    if err := seller.Validate(); err != nil {
        return nil, err
    }
    return &ValidatedSeller{Seller: seller}, nil
}

2.1.3 出品者リポジトリインターフェースの定義

  1. internal/domain/repositories/seller_repository.go に出品者リポジトリインターフェースを定義します。
package repositories

import (
    "github.com/sklinkert/go-ddd/internal/domain/entities"
)

type SellerRepository interface {
    Save(seller entities.Seller) error
    FindAll() ([]entities.Seller, error)
    ById(id string) (entities.Seller, error)
    Update(seller entities.Seller) error
    Delete(id string) error
}

2.2 アプリケーションレイヤーの実装

2.2.1 出品者コマンドの作成

  1. internal/application/command/seller_commands.go に出品者コマンドを定義します。
package command

type CreateSellerCommand struct {
    Name  string
    Email string
}

type UpdateSellerNameCommand struct {
    Id   string
    Name string
}

type UpdateSellerEmailCommand struct {
    Id    string
    Email string
}

type DeleteSellerCommand struct {
    Id string
}

2.2.2 出品者クエリの作成

  1. internal/application/query/seller_queries.go に出品者クエリを定義します。
package query

type FindAllSellersQuery struct {}

type FindSellerByIdQuery struct {
    Id string
}

2.2.3 出品者サービスの実装

  1. internal/application/services/seller_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 SellerService struct {
    sellerRepo repositories.SellerRepository
}

func NewSellerService(sellerRepo repositories.SellerRepository) *SellerService {
    return &SellerService{
        sellerRepo: sellerRepo,
    }
}

func (s *SellerService) Create(cmd command.CreateSellerCommand) (entities.Seller, error) {
    // 出品者エンティティの作成
    now := time.Now()
    seller := entities.Seller{
        Id:        uuid.New().String(),
        Name:      cmd.Name,
        Email:     cmd.Email,
        CreatedAt: now,
        UpdatedAt: now,
    }

    // バリデーション
    if _, err := entities.NewValidatedSeller(seller); err != nil {
        return entities.Seller{}, err
    }

    // 保存
    if err := s.sellerRepo.Save(seller); err != nil {
        return entities.Seller{}, err
    }

    return seller, nil
}

func (s *SellerService) FindAll() ([]entities.Seller, error) {
    return s.sellerRepo.FindAll()
}

func (s *SellerService) ById(id string) (entities.Seller, error) {
    return s.sellerRepo.ById(id)
}

func (s *SellerService) UpdateName(cmd command.UpdateSellerNameCommand) (entities.Seller, error) {
    // 既存の出品者を取得
    seller, err := s.sellerRepo.ById(cmd.Id)
    if err != nil {
        return entities.Seller{}, err
    }

    // 名前を更新
    if err := seller.UpdateName(cmd.Name); err != nil {
        return entities.Seller{}, err
    }

    // バリデーション
    if _, err := entities.NewValidatedSeller(seller); err != nil {
        return entities.Seller{}, err
    }

    // 保存
    if err := s.sellerRepo.Update(seller); err != nil {
        return entities.Seller{}, err
    }

    return seller, nil
}

func (s *SellerService) UpdateEmail(cmd command.UpdateSellerEmailCommand) (entities.Seller, error) {
    // 既存の出品者を取得
    seller, err := s.sellerRepo.ById(cmd.Id)
    if err != nil {
        return entities.Seller{}, err
    }

    // メールアドレスを更新
    if err := seller.UpdateEmail(cmd.Email); err != nil {
        return entities.Seller{}, err
    }

    // バリデーション
    if _, err := entities.NewValidatedSeller(seller); err != nil {
        return entities.Seller{}, err
    }

    // 保存
    if err := s.sellerRepo.Update(seller); err != nil {
        return entities.Seller{}, err
    }

    return seller, nil
}

func (s *SellerService) Delete(cmd command.DeleteSellerCommand) error {
    // 既存の出品者を取得して存在確認
    _, err := s.sellerRepo.ById(cmd.Id)
    if err != nil {
        return errors.New("seller not found")
    }

    // 削除
    return s.sellerRepo.Delete(cmd.Id)
}

2.3 インフラストラクチャレイヤーの実装

2.3.1 出品者リポジトリの実装(PostgreSQL)

  1. internal/infrastructure/db/postgres/seller_repository.go に出品者リポジトリの実装を作成します。
package postgres

import (
    "errors"
    "gorm.io/gorm"
    "github.com/sklinkert/go-ddd/internal/domain/entities"
)

type GormSellerRepository struct {
    db *gorm.DB
}

func NewGormSellerRepository(db *gorm.DB) *GormSellerRepository {
    return &GormSellerRepository{db: db}
}

func (r *GormSellerRepository) Save(seller entities.Seller) error {
    dbSeller := toDBSeller(seller)
    result := r.db.Create(&dbSeller)
    return result.Error
}

func (r *GormSellerRepository) FindAll() ([]entities.Seller, error) {
    var dbSellers []DBSeller
    result := r.db.Find(&dbSellers)
    if result.Error != nil {
        return nil, result.Error
    }

    sellers := make([]entities.Seller, len(dbSellers))
    for i, dbSeller := range dbSellers {
        sellers[i] = toDomainSeller(dbSeller)
    }
    return sellers, nil
}

func (r *GormSellerRepository) ById(id string) (entities.Seller, error) {
    var dbSeller DBSeller
    result := r.db.First(&dbSeller, "id = ?", id)
    if result.Error != nil {
        if errors.Is(result.Error, gorm.ErrRecordNotFound) {
            return entities.Seller{}, errors.New("seller not found")
        }
        return entities.Seller{}, result.Error
    }
    return toDomainSeller(dbSeller), nil
}

func (r *GormSellerRepository) Update(seller entities.Seller) error {
    dbSeller := toDBSeller(seller)
    result := r.db.Save(&dbSeller)
    return result.Error
}

func (r *GormSellerRepository) Delete(id string) error {
    // ソフトデリート
    result := r.db.Delete(&DBSeller{}, "id = ?", id)
    return result.Error
}

2.3.2 出品者マッパーの実装

  1. internal/infrastructure/db/postgres/seller_mapper.go に出品者マッパーを実装します。
package postgres

import (
    "time"
    "github.com/sklinkert/go-ddd/internal/domain/entities"
)

type DBSeller struct {
    ID        string `gorm:"primaryKey"`
    Name      string
    Email     string
    CreatedAt time.Time
    UpdatedAt time.Time
    DeletedAt *time.Time `gorm:"index"`
}

func (DBSeller) TableName() string {
    return "sellers"
}

func toDBSeller(seller entities.Seller) DBSeller {
    return DBSeller{
        ID:        seller.Id,
        Name:      seller.Name,
        Email:     seller.Email,
        CreatedAt: seller.CreatedAt,
        UpdatedAt: seller.UpdatedAt,
        DeletedAt: seller.DeletedAt,
    }
}

func toDomainSeller(dbSeller DBSeller) entities.Seller {
    return entities.Seller{
        Id:        dbSeller.ID,
        Name:      dbSeller.Name,
        Email:     dbSeller.Email,
        CreatedAt: dbSeller.CreatedAt,
        UpdatedAt: dbSeller.UpdatedAt,
        DeletedAt: dbSeller.DeletedAt,
    }
}

2.4 インターフェースレイヤーの実装

2.4.1 出品者コントローラーの実装

  1. internal/interface/api/rest/seller_controller.go に出品者コントローラーを実装します。

```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 SellerController struct { sellerService *services.SellerService }

func NewSellerController(e echo.Echo, sellerService services.SellerService) { controller := &SellerController{ sellerService: sellerService, }

api := e.Group("/api/v1")
api.POST("/sellers", controller.Create)
api.GET("/sellers", controller.FindAll)
api.GET("/sellers/:id", controller.ById)
api.PUT("/sellers/:id/name", controller.UpdateName)
api.PUT("/sellers/:id/email", controller.UpdateEmail)
api.DELETE("/sellers/:id", controller.Delete)

}

type CreateSellerRequest struct { Name string json:"name" Email string json:"email" }

type UpdateSellerNameRequest struct { Name string json:"name" }

type UpdateSellerEmailRequest struct { Email string json:"email" }

type SellerResponse struct { Id string json:"id" Name string json:"name" Email string json:"email" CreatedAt string json:"createdAt" UpdatedAt string json:"updatedAt" DeletedAt *string json:"deletedAt,omitempty" }

func (ctrl *SellerController) Create(c echo.Context) error { var req CreateSellerRequest if err := c.Bind(&req); err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()}) }

cmd := command.CreateSellerCommand{
    Name:  req.Name,
    Email: req.Email,
}

seller, err := ctrl.sellerService.Create(cmd)
if err != nil {
    return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
}

resp := toSellerResponse(seller)
return c.JSON(http.StatusCreated, resp)

}

func (ctrl *SellerController) FindAll(c echo.Context) error { sellers, err := ctrl.sellerService.FindAll() if err != nil { return c.JSON(http