go工程化实践(2):在实际应用开发中设计并落地DDD领域驱动思想

249 阅读5分钟

前言:本篇文章主要内容来自《ddd with golang》,《How TO ImPle DDD In Go》欢迎大家多多支持正版

DDD的一些思考

image.png

  • 什么是DDD
    • domain 对项目的映射
    • DDD不能偏离它本身目的最重要的手段是对要开发的项目进行实际而且详尽的观察以及思考

一个酒馆的ddd设计场景

1.假设你要设计一个酒馆的系统

- 有哪些要素是你觉得要组成酒馆的业务的?
- 只有顾客吗?
- 只有员工吗?
- 只有支付吗?
- 只有供货商吗?

2.我们可以在上面的考虑中初步划分了根域(酒馆),子域名(顾客等);接下来我们就能开展我们的编程了

(1)初始化项目

mkdir ddd-go
go mod init github.com/percybolmer/ddd-go

(2)划定我们的目录

image.png

  • domain用来存放什么内容? 用来存放子域
  • entity用来存放什么内容? 用来存放实体,即有唯一标识符的实体对象。我们规划了两个entity:Person,Item。
  • 在此基础上,我们思考如果一个类似Const并且没有唯一标识符的实体,应该怎么命名呢? 我们称之为V-O Value Objects

image.png

image.png

// Package entities holds all the entities that are shared across all subdomains
package entity

import (
	"github.com/google/uuid"
)

// Person is a entity that represents a person in all Domains
type Person struct {
	// ID is the identifier of the Entity, the ID is shared for all sub domains
	ID uuid.UUID
	// Name is the name of the person
	Name string
	// Age is the age of the person
	Age int
}
package entity

import "github.com/google/uuid"

// Item represents a Item for all sub domains
type Item struct {
	ID          uuid.UUID 
	Name        string    
	Description string    
}

(3)aggregate

image.png

  • 在上面创建entity的基础上,假如我们要创建交易记录,而且缺少id仅仅只是起到记录作用,那么我们就可以创建值对象,即value-object
package valueobject

import (
	"time"
)

// Transaction is a payment between two parties
type Transaction struct {
	// all values lowercase since they are immutable
	amount    int
	from      uuid.UUID
	to        uuid.UUID
	createdAt time.Time
}
  • 如果我们要把上面的entity以及value-object聚集成集合aggregate,我们首先要明白,我们这个集合的根entity是什么呢,是上面的person实体。
mkdir aggregate
cd aggregate
touch customer.go
// Package aggregates holds aggregates that combines many entities into a full object
package aggregate

import (
	"github.com/percybolmer/ddd-go/entity"
	"github.com/percybolmer/ddd-go/valueobject"
)

// Customer is a aggregate that combines all entities needed to represent a customer
type Customer struct {
	// person is the root entity of a customer
	// which means the person.ID is the main identifier for this aggregate
	person *entity.Person 
	// a customer can hold many products
	products []*entity.Item 
	// a customer can perform many transactions
	transactions []valueobject.Transaction 
}
  • 我们可以观察到,在上面的Customer中,对于entity是指针对象,对于value-object是值对象,是什么原因呢?是因为entity是可以修改的,value-object一旦创建就不能修改了
  • 同时,我们没有定义对应的tag,是我们的实体以及集合只是起到声明的作用,并不会对外部的json以及其他应用进行耦合,我们只负责我们自身的目的。
  • 第三点我们的aggregate中字段都是小写开头,是因为我们需要指定唯一的出口,即以person.ID作为唯一外部出口

(4)工厂模式提供aggregate的实例对象

image.png

  • 工厂模式的使用

如果我们要使用到上面的aggregate,我们需要使用工厂模式作为提供方来提供实例。

// Package aggregate holds aggregates that combines many entities into a full object
package aggregate

import (
	"errors"

	"github.com/google/uuid"
	"github.com/percybolmer/ddd-go/entity"
	"github.com/percybolmer/ddd-go/valueobject"
)

var (
	// ErrInvalidPerson is returned when the person is not valid in the NewCustome factory
	ErrInvalidPerson = errors.New("a customer has to have an valid person")
)

// Customer is a aggregate that combines all entities needed to represent a customer
type Customer struct {
	// person is the root entity of a customer
	// which means the person.ID is the main identifier for this aggregate
	person *entity.Person
	// a customer can hold many products
	products []*entity.Item
	// a customer can perform many transactions
	transactions []valueobject.Transaction
}

// NewCustomer is a factory to create a new Customer aggregate
// It will validate that the name is not empty
func NewCustomer(name string) (Customer, error) {
	// Validate that the Name is not empty
	if name == "" {
		return Customer{}, ErrInvalidPerson
	}

	// Create a new person and generate ID
	person := &entity.Person{
		Name: name,
		ID:   uuid.New(),
	}
	// Create a customer object and initialize all the values to avoid nil pointer exceptions
	return Customer{
		person:       person,
		products:     make([]*entity.Item, 0),
		transactions: make([]valueobject.Transaction, 0),
	}, nil
}

(5)repository的提供aggregate的数据存储层

image.png

  • 为什么在实际数据存储以及业务数据之间抽象数据存储层
    • 方便无缝切换存储方式
    • 方便mock测试
// Package Customer holds all the domain logic for the customer domain.
package customer

import (
	"github.com/google/uuid"
	"github.com/percybolmer/ddd-go/aggregate"
)
var (
	// ErrCustomerNotFound is returned when a customer is not found.
	ErrCustomerNotFound = errors.New("the customer was not found in the repository")
	// ErrFailedToAddCustomer is returned when the customer could not be added to the repository.
	ErrFailedToAddCustomer = errors.New("failed to add the customer to the repository")
	// ErrUpdateCustomer is returned when the customer could not be updated in the repository.
	ErrUpdateCustomer = errors.New("failed to update the customer in the repository")
)
// CustomerRepository is a interface that defines the rules around what a customer repository
// Has to be able to perform
type CustomerRepository interface {
	Get(uuid.UUID) (aggregate.Customer, error)
	Add(aggregate.Customer) error
	Update(aggregate.Customer) error
}
  • 接下来假如我们需要实际在mysql中进行数据存储
mkdir storage
touch storage/customer.model.go
package customer

import (
    "errors"
    "github.com/google/uuid"
    "github.com/percybolmer/ddd-go/aggregate"
    "gorm.io/gorm"
)

type CustomerModel struct {
    db *gorm.DB
}

func NewCustomerModel(db *gorm.DB) *CustomerModel {
    return &CustomerModel{
        db: db,
    }
}

func (cm *CustomerModel) Get(id uuid.UUID) (aggregate.Customer, error) {
    var customer aggregate.Customer
    result := cm.db.First(&customer, "id = ?", id)
    if errors.Is(result.Error, gorm.ErrRecordNotFound) {
        return aggregate.Customer{}, ErrCustomerNotFound
    }
    return customer, result.Error
}

func (cm *CustomerModel) Add(customer aggregate.Customer) error {
    result := cm.db.Create(&customer)
    if result.Error != nil {
        return ErrFailedToAddCustomer
    }
    return nil
}

func (cm *CustomerModel) Update(customer aggregate.Customer) error {
    result := cm.db.Save(&customer)
    if result.Error != nil {
        return ErrUpdateCustomer
    }
    return nil
}

(6)service在业务层的具体实现

image.png

  • 假如我们有一个order-service,那么我们就需要用到CustomerRepository和ProductRepository
mkdir service/order
cd service/order
touch logic.go && touch handler.go
// logic.go
// OrderConfiguration is an alias for a function that will take in a pointer to an OrderService and modify it
type OrderConfiguration func(os *OrderService) error

// OrderService is a implementation of the OrderService
type OrderService struct {
	customers customer.CustomerRepository
}

// NewOrderService takes a variable amount of OrderConfiguration functions and returns a new OrderService
// Each OrderConfiguration will be called in the order they are passed in
func NewOrderService(cfgs ...OrderConfiguration) (*OrderService, error) {
	// Create the orderservice
	os := &OrderService{}
	// Apply all Configurations passed in
	for _, cfg := range cfgs {
		// Pass the service into the configuration function
		err := cfg(os)
		if err != nil {
			return nil, err
		}
	}
	return os, nil
}

// WithCustomerRepository applies a given customer repository to the OrderService
func WithCustomerRepository(cr customer.CustomerRepository) OrderConfiguration {
	// return a function that matches the OrderConfiguration alias,
	// You need to return this so that the parent function can take in all the needed parameters
	return func(os *OrderService) error {
		os.customers = cr
		return nil
	}
}

// WithModelCustomerRepository applies a model customer repository to the OrderService
func WithModelCustomerRepository() OrderConfiguration {
	// Create the model repo, if we needed parameters, such as connection strings they could be inputted here
	cr := model.New()
	return WithCustomerRepository(cr)
}

// handler.go
func (o *OrderService) CreateOrder(customerID uuid.UUID, productIDs []uuid.UUID) error {
	// Get the customer
	c, err := o.customers.Get(customerID)
	if err != nil {
		return err
	}

	// Get each Product, Ouchie, We need a ProductRepository

	return nil
}