Agent Skillssazardev/goca › ddd-teaching-skill

ddd-teaching-skill

GitHub

基于 Goca CLI 的 DDD 与 Clean Architecture 教学工具。通过引导式六步流程,结合 TDD 实践,教授分层架构、依赖规则及核心概念(实体、用例等),帮助用户掌握高质量代码生成与架构设计。

skills/ddd-teaching-skill/SKILL.md sazardev/goca

触发场景

用户询问如何学习 DDD 或 Clean Architecture 请求使用 Goca 生成特定组件如 entity、usecase 展示违反架构原则的代码寻求修正 要求逐步指导项目初始化与功能开发

安装

npx skills add sazardev/goca --skill ddd-teaching-skill -g -y
更多选项

不安装直接使用

npx skills use sazardev/goca@ddd-teaching-skill

指定 Agent (Claude Code)

npx skills add sazardev/goca --skill ddd-teaching-skill -a claude-code -g -y

安装 repo 全部 skill

npx skills add sazardev/goca --all -g -y

预览 repo 内 skill

npx skills add sazardev/goca --list

SKILL.md

Frontmatter
{
    "name": "ddd-teaching-skill",
    "description": "Enseña Domain-Driven Design y Clean Architecture de forma guiada usando Goca CLI. Actívar cuando el usuario pregunte \"aprender DDD\", \"como usar Goca\", \"enseñame Clean Architecture\", \"que es domain\", \"como generar entity\/usecase\/repository\/handler\", o pida practicar DDD con Goca. Inactívar al completar el flujo o si el usuario cambia a tareas no relacionadas con aprendizaje."
}

DDD Teaching — Aprendizaje Guiado con Goca

Filosofía

DDD + Clean Architecture resuelven el problema de código que muere lentamente: lógica de negocio mezclada con frameworks, tests imposibles, cambios que cascada. La solución es la Dependency Rule — las dependencias apuntan hacia adentro.

Goca materializa esta arquitectura generando código en 4 capas:

Handler (adapter)  →  UseCase (app logic)  →  Repository (persistence)  →  Domain (pure)
     ↓                       ↓                        ↓                        ↓
   HTTP/gRPC           business logic              SQL/Redis               entities

Cada capa solo conoce la que está inmediatamente dentro de ella, y siempre a través de interfaces.

Activación

Invocar este skill automáticamente cuando el usuario:

  • Pregunte conceptos de DDD o Clean Architecture
  • Quiera aprender a usar Goca paso a paso
  • Pida "generar un feature completo" sin entender qué genera
  • Muestre código con violaciones arquitectónicas (handler con GORM, usecase con http, etc.)

Mapa Conceptual ↔ Comandos Goca

Concepto DDD Comando Goca Archivo generado Propósito
Entidad goca entity internal/domain/product.go Pureza del dominio, invariantes
Value Object goca entity --validation domain/product.go — Validate() Validación encapsulada
Repository interface goca repository --interface-only repository/interfaces.go Contrato de persistencia
Repository impl goca repository -d postgres repository/postgres_product.go GORM encapsulado
DTO / Application Service goca usecase usecase/dto.go, service.go Separación capas
UseCase interface goca usecase usecase/product_usecase.go Contrato para handlers
Handler / Adapter goca handler -t http handler/http/product_handler.go Delivery
DI Container goca di di/container.go Wiring

Flujo Guiado — 6 Pasos (con TDD)

Cada paso: Concepto DDDComando GocaCódigo generadoTest

Paso 1: Init — Scaffold del Proyecto

Concepto: Separación en capas limpias. El directorio internal/ es la frontera — nada fuera de internal/ puede importar lo de adentro, pero lo de adentro sí puede importar pkg/.

Comando:

goca init ecommerce --module github.com/myapp/ecommerce --database postgres

Estructura generada:

internal/
  domain/       ← Puro: sin imports externos, solo lógica de negocio
  usecase/      ← Solo importa domain + repository interfaces
  repository/   ← Implementa interfaces; conoce GORM, no el usecase
  handler/      ← Conoce interfaces de usecase, nunca repository directo

Análisis: Cada carpeta corresponde exactamente a un círculo de Clean Architecture. La regla de dependencia se aplica a nivel de import: handlerusecaserepositorydomain. Prohibido saltar capas.

Test:

cd ecommerce && go build ./... && go vet ./...
# Debe pasar sin errores: es el esqueleto vacío pero correcto

Paso 2: Entity — Domain Puro

Concepto: Una entidad DDD tiene identidad (ID) e invariantes — reglas que siempre deben cumplirse. Los Value Objects son inmutables y se comparan por valor. Ambos viven en domain/ sin dependencias externas.

Comando:

goca entity Product --fields "name:string,price:float64,category:string" --validation --business-rules

Código generado (extraído de cmd/templates.go y cmd/template_components.go):

// internal/domain/product.go
package domain

type Product struct {
    ID       int     `json:"id" gorm:"primaryKey"`
    Name     string  `json:"name" gorm:"type:varchar(255);not null"`
    Price    float64 `json:"price" gorm:"type:decimal(10,2);not null"`
    Category string  `json:"category" gorm:"type:varchar(100)"`
}

func (p *Product) Validate() error {
    if p.Name == "" {
        return errors.New("product name is required")
    }
    if p.Price <= 0 {
        return errors.New("product price must be positive")
    }
    return nil
}

func (p *Product) IsExpensive() bool {
    return p.Price > 1000
}
// internal/domain/errors.go
package domain

import "errors"

var (
    ErrInvalidProductName  = errors.New("product name is required")
    ErrInvalidProductPrice = errors.New("product price must be positive")
)

Análisis:

  • Struct Product: sin lógica externa. Los tags de campo son metadata de serialización, no comportamiento.
  • Validate(): invariante expresado como método del dominio. Sin dependencias externas. Sin ORM. Sin HTTP.
  • IsExpensive(): regla de negocio co-localizada con los datos. Cambia la regla? Cambias este método, no un service remoto.
  • Errores como sentinel values (var Err...), no strings mágicos. El consumidor puede hacer errors.Is(err, domain.ErrInvalidProductName).

Test (TDD primero):

// internal/domain/product_test.go
func TestProduct_Validate(t *testing.T) {
    tests := []struct {
        name    string
        product Product
        wantErr bool
    }{
        {"valid product", Product{Name: "Laptop", Price: 999.99, Category: "Electronics"}, false},
        {"empty name", Product{Price: 999.99}, true},
        {"zero price", Product{Name: "Laptop", Price: 0}, true},
        {"negative price", Product{Name: "Laptop", Price: -1}, true},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            err := tt.product.Validate()
            if (err != nil) != tt.wantErr {
                t.Errorf("Validate() error = %v, wantErr = %v", err, tt.wantErr)
            }
        })
    }
}

func TestProduct_IsExpensive(t *testing.T) {
    cheap := Product{Name: "Notebook", Price: 10}
    expensive := Product{Name: "MacBook", Price: 2500}

    if cheap.IsExpensive() {
        t.Error("expected cheap product to not be expensive")
    }
    if !expensive.IsExpensive() {
        t.Error("expected expensive product to be expensive")
    }
}

Paso 3: UseCase — Lógica de Aplicación

Concepto: El Application Service orquesta la lógica de negocio. No contiene reglas de dominio (esas van en la entidad). Usa DTOs para desacoplar el mundo exterior del dominio. Depende de interfaces de repositorio, no de implementaciones concretas.

Comando:

goca usecase ProductService --entity Product --operations "create,read,update,delete,list"

Código generado (extraído de cmd/template_components.go + cmd/templates.go):

// internal/usecase/product_usecase.go
package usecase

import (
    "github.com/myapp/ecommerce/internal/domain"
    "github.com/myapp/ecommerce/internal/repository"
)

type ProductUseCase interface {
    CreateProduct(input CreateProductInput) (*CreateProductOutput, error)
    GetProductByID(id int) (*ProductOutput, error)
    UpdateProduct(id int, input UpdateProductInput) error
    DeleteProduct(id int) error
    ListProducts() (*ListProductsOutput, error)
}

type productService struct {
    repo repository.ProductRepository
}

func NewProductService(repo repository.ProductRepository) ProductUseCase {
    return &productService{repo: repo}
}

func (s *productService) CreateProduct(input CreateProductInput) (*CreateProductOutput, error) {
    product := domain.Product{
        Name:     input.Name,
        Price:    input.Price,
        Category: input.Category,
    }

    if err := product.Validate(); err != nil {
        return nil, err
    }

    if err := s.repo.Save(&product); err != nil {
        return nil, err
    }

    return &CreateProductOutput{
        Product: &product,
        Message: "Product created successfully",
    }, nil
}
// internal/usecase/dto.go
type CreateProductInput struct {
    Name     string  `json:"name"`
    Price    float64 `json:"price"`
    Category string  `json:"category"`
}

type CreateProductOutput struct {
    Product *domain.Product `json:"product"`
    Message string          `json:"message"`
}

type UpdateProductInput struct {
    Name     *string  `json:"name,omitempty"`
    Price    *float64 `json:"price,omitempty"`
    Category *string  `json:"category,omitempty"`
}

type ProductOutput struct {
    Product *domain.Product `json:"product"`
}

type ListProductsOutput struct {
    Products []domain.Product `json:"products"`
    Total    int              `json:"total"`
}
// internal/usecase/interfaces.go
package usecase

import "github.com/myapp/ecommerce/internal/domain"

type ProductRepository interface {
    Save(product *domain.Product) error
    FindByID(id int) (*domain.Product, error)
    Update(product *domain.Product) error
    Delete(id int) error
    FindAll() ([]domain.Product, error)
}

Análisis:

  • ProductUseCase es interfaz pública. El handler programa contra esta interfaz, no contra el struct concreto.
  • productService es privado (minúscula). Solo se exporta el constructor NewProductService(repo). Nadie puede acoplar al tipo concreto.
  • Constructor injection: el repo se inyecta. El service no crea su propio repo, no llama a sql.Open(), no sabe si es Postgres o Mock.
  • CreateProductInput vs domain.Product: el input puede tener validaciones distintas al domain. El Update usa punteros (*string) para distinguir "no enviado" de "enviado vacío".
  • La interfaz ProductRepository en usecase/interfaces.go es la misma que en repository/interfaces.go. El usecase la necesita para su firma.

Test con Mock (TDD — RED antes de implementar):

// internal/usecase/product_service_test.go
func TestCreateProduct_Success(t *testing.T) {
    mockRepo := new(MockProductRepository)
    mockRepo.On("Save", mock.AnythingOfType("*domain.Product")).Return(nil)

    svc := NewProductService(mockRepo)
    input := CreateProductInput{Name: "Laptop", Price: 999.99, Category: "Electronics"}

    output, err := svc.CreateProduct(input)

    assert.NoError(t, err)
    assert.NotNil(t, output.Product)
    assert.Equal(t, "Laptop", output.Product.Name)
    assert.Equal(t, "Product created successfully", output.Message)
    mockRepo.AssertExpectations(t)
}

func TestCreateProduct_InvalidData(t *testing.T) {
    mockRepo := new(MockProductRepository)

    svc := NewProductService(mockRepo)
    input := CreateProductInput{Name: "", Price: 0}

    _, err := svc.CreateProduct(input)

    assert.Error(t, err)
    mockRepo.AssertNotCalled(t, "Save")
}

Paso 4: Repository — Persistencia

Concepto: El Repository Pattern abstrae el almacenamiento detrás de una interfaz. El dominio y el usecase conocen la interfaz, no la implementación. GORM, SQL, Redis, MongoDB — todo queda encapsulado detrás de esta interfaz.

Comando:

goca repository Product --database postgres --transactions

Código generado (extraído de cmd/templates.go + cmd/repository.go):

// internal/repository/interfaces.go
package repository

import "github.com/myapp/ecommerce/internal/domain"

type ProductRepository interface {
    Save(product *domain.Product) error
    FindByID(id int) (*domain.Product, error)
    Update(product *domain.Product) error
    Delete(id int) error
    FindAll() ([]domain.Product, error)
}
// internal/repository/postgres_product_repository.go
package repository

import (
    "gorm.io/gorm"
    "github.com/myapp/ecommerce/internal/domain"
)

type postgresProductRepository struct {
    db *gorm.DB
}

func NewPostgresProductRepository(db *gorm.DB) ProductRepository {
    return &postgresProductRepository{db: db}
}

func (r *postgresProductRepository) Save(product *domain.Product) error {
    return r.db.Create(product).Error
}

func (r *postgresProductRepository) FindByID(id int) (*domain.Product, error) {
    var product domain.Product
    if err := r.db.First(&product, id).Error; err != nil {
        return nil, err
    }
    return &product, nil
}

func (r *postgresProductRepository) Update(product *domain.Product) error {
    return r.db.Save(product).Error
}

func (r *postgresProductRepository) Delete(id int) error {
    return r.db.Delete(&domain.Product{}, id).Error
}

func (r *postgresProductRepository) FindAll() ([]domain.Product, error) {
    var products []domain.Product
    if err := r.db.Find(&products).Error; err != nil {
        return nil, err
    }
    return products, nil
}

Análisis:

  • Interfaz en repository/interfaces.go define el contrato. El usecase la consume. El handler ni la conoce.
  • postgresProductRepository es privado — nadie fuera del package la instancia excepto el DI container.
  • El constructor NewPostgresProductRepository(db *gorm.DB) ProductRepository devuelve la interfaz, no el struct.
  • GORM está completamente encapsulado en este package. Si migras a MongoDB, cambias este archivo, no tocas domain ni usecase.
  • --transactions agrega SaveWithTx(tx *gorm.DB, ...) para Unit of Work.

Paso 5: Handler — Delivery Adapter

Concepto: El Adapter convierte requests externos (HTTP, gRPC, CLI) en llamadas a usecase. No contiene lógica de negocio. Solo serialización, routing, delegación.

Comando:

goca handler Product --type http --validation

Código generado (extraído de cmd/templates.go):

// internal/handler/http/product_handler.go
package http

import (
    "encoding/json"
    "net/http"
    "strconv"

    "github.com/gorilla/mux"
    "github.com/myapp/ecommerce/internal/usecase"
)

type ProductHandler struct {
    usecase usecase.ProductUseCase
}

func NewProductHandler(uc usecase.ProductUseCase) *ProductHandler {
    return &ProductHandler{usecase: uc}
}

func (h *ProductHandler) CreateProduct(w http.ResponseWriter, r *http.Request) {
    var input usecase.CreateProductInput

    if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
        http.Error(w, "Invalid request body", http.StatusBadRequest)
        return
    }

    output, err := h.usecase.CreateProduct(input)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(output)
}

func (h *ProductHandler) GetProduct(w http.ResponseWriter, r *http.Request) {
    vars := mux.Vars(r)
    id, err := strconv.Atoi(vars["id"])
    if err != nil {
        http.Error(w, "Invalid product ID", http.StatusBadRequest)
        return
    }

    output, err := h.usecase.GetProductByID(id)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(output)
}
// internal/handler/http/routes.go
package http

import (
    "github.com/gorilla/mux"
    "github.com/myapp/ecommerce/internal/usecase"
)

func SetupProductRoutes(router *mux.Router, uc usecase.ProductUseCase) {
    handler := NewProductHandler(uc)

    router.HandleFunc("/products", handler.CreateProduct).Methods("POST")
    router.HandleFunc("/products/{id}", handler.GetProduct).Methods("GET")
    router.HandleFunc("/products/{id}", handler.UpdateProduct).Methods("PUT")
    router.HandleFunc("/products/{id}", handler.DeleteProduct).Methods("DELETE")
    router.HandleFunc("/products", handler.ListProducts).Methods("GET")
}

Análisis:

  • Handler importa usecase solo. No importa repository, no importa gorm. ✅
  • ProductHandler recibe usecase.ProductUseCase (interfaz), no el service concreto.
  • No hay lógica de negocio aquí. Solo: parse request → call usecase → serialize response.
  • Si cambias HTTP → gRPC, el handler cambia pero usecase y domain no se tocan.
  • Routes se inyecta el usecase y construye el handler — el router no necesita saber cómo construirlo.

Test con httptest:

func TestCreateProductHandler(t *testing.T) {
    mockUC := new(MockProductUseCase)
    mockUC.On("CreateProduct", mock.Anything).Return(&usecase.CreateProductOutput{
        Product: &domain.Product{Name: "Laptop", Price: 999.99},
        Message: "Product created successfully",
    }, nil)

    handler := NewProductHandler(mockUC)
    body := `{"name":"Laptop","price":999.99,"category":"Electronics"}`
    req := httptest.NewRequest(http.MethodPost, "/products", strings.NewReader(body))
    req.Header.Set("Content-Type", "application/json")
    w := httptest.NewRecorder()

    handler.CreateProduct(w, req)

    assert.Equal(t, http.StatusCreated, w.Code)
    assert.Contains(t, w.Body.String(), "Laptop")
    mockUC.AssertExpectations(t)
}

Paso 6: DI — Wiring Completo

Concepto: El Composition Root es el único lugar donde se instancian implementaciones concretas. Construye el grafo de objetos completo. Todas las demás capas reciben sus dependencias ya construidas.

Comando:

goca di --features "Product" --database postgres

Código generado (extraído de cmd/di.go):

// internal/di/container.go
package di

import (
    "gorm.io/gorm"

    "github.com/myapp/ecommerce/internal/repository"
    "github.com/myapp/ecommerce/internal/usecase"
    "github.com/myapp/ecommerce/internal/handler/http"
)

type Container struct {
    db *gorm.DB

    productRepo    repository.ProductRepository
    productUC      usecase.ProductUseCase
    productHandler *http.ProductHandler
}

func NewContainer(db *gorm.DB) *Container {
    c := &Container{db: db}
    c.setupRepositories()
    c.setupUseCases()
    c.setupHandlers()
    return c
}

func (c *Container) setupRepositories() {
    c.productRepo = repository.NewPostgresProductRepository(c.db)
}

func (c *Container) setupUseCases() {
    c.productUC = usecase.NewProductService(c.productRepo)
}

func (c *Container) setupHandlers() {
    c.productHandler = http.NewProductHandler(c.productUC)
}

func (c *Container) ProductHandler() *http.ProductHandler {
    return c.productHandler
}

Análisis:

  • NewContainer(db) recibe la conexión a BD. El container construye TODO el grafo.
  • Único lugar donde se llama a NewPostgresProductRepository, NewProductService, NewProductHandler.
  • Si quieres cambiar Postgres → MySQL, cambias UNA línea en setupRepositories.
  • Los getters (ProductHandler()) exponen solo lo que main.go necesita.

Feature Completo (atajo)

goca feature Product --fields "name:string,price:float64,category:string" --database postgres --validation --handlers http

Este comando ejecuta Pasos 2-6 en una sola operación. Equivale a:

1. goca entity Product --fields "name:string,price:float64,category:string" --validation --business-rules
2. goca usecase ProductService --entity Product --operations "create,read,update,delete,list"
3. goca repository Product --database postgres
4. goca handler Product --type http --validation
5. goca messages Product --all
6. goca di --features "Product" --database postgres

Además integra el handler en main.go y registra migraciones automáticas.


Anti-Patterns (qué NO hacer)

Handler importando repository directamente

type ProductHandler struct {
    repo *gorm.DB  // MAL: handler conoce la BD y GORM!
}

✅ Handler solo conoce usecase.ProductUseCase

UseCase con GORM o SQL

type productService struct {
    db *gorm.DB  // MAL: lógica de negocio conoce GORM
}

✅ UseCase recibe repository.ProductRepository (interfaz abstracta)

Entidad anémica (solo getters/setters, sin comportamiento)

type Product struct {
    Name  string  // MAL: sin Validate(), sin reglas de negocio
    Price float64 // solo es una struct de datos, no una entidad DDD
}

✅ Domain tiene métodos: Validate(), IsExpensive(), etc.

DTOs expuestos desde handler — usa usecase.CreateProductInput, no crees DTOs en handler.

init() y variables globales — usa constructor injection siempre.


Límites del Skill

Este skill enseña DDD + Clean Architecture a través de Goca. Para otros aspectos, delegar:

Necesitas Recurso
Benchmarks, fuzzing, test coverage avanzado golang-testing skill
Table-driven tests, helpers, golden files golang-testing skill
sync.Pool, zero allocation, performance golang-performance skill
Interface design, functional options, patterns golang-patterns skill
Validar dependencias entre capas archivo por archivo ArchitectGuard agent mode en .github/AGENTS.md
Verificar que templates generan código compilable CodegenAuditor agent mode en .github/AGENTS.md
Guía completa de comandos Goca GUIDE.md

Cuando el usuario cambie a una tarea no relacionada con aprendizaje DDD (ej: "arregla este bug", "agrega esta feature"), desactivar este skill y continuar como agente normal.

版本历史

  • 8aaeb51 当前 2026-07-11 16:58

同 Skill 集合

.agents/skills/caveman-commit/SKILL.md
.agents/skills/caveman/SKILL.md
.agents/skills/golang-patterns/SKILL.md
.agents/skills/golang-testing/SKILL.md

元信息

文件数
0
版本
8aaeb51
Hash
02797f0c
收录时间
2026-07-11 16:58

首页 - Wiki
Copyright © 2011-2026 iteam. Current version is 2.155.2. UTC+08:00, 2026-07-13 22:43
浙ICP备14020137号-1 $访客地图$