Golang中如何从数据库模型重构领域模型的常用方法

Golang中如何从数据库模型重构领域模型的常用方法 给定一个简化的待办事项应用的领域模型

type Todo struct {
    title          string
    isMarkedAsDone bool
    modifiedAt     time.Time
}

func NewTodo(title string) (*Todo, error) {
    if title == "" {
        return nil, errors.New("title is empty")
    }

    todo := &Todo{
        title:          title,
        isMarkedAsDone: false,
        modifiedAt:     time.Now(),
    }

    return todo, nil
}

func (t *Todo) GetTitle() string {
    return t.title
}

func (t *Todo) IsMarkedAsDone() bool {
    return t.isMarkedAsDone
}

// 其他getter方法...

func (t *Todo) Rename(newTitle string) error {
    if t.isMarkedAsDone {
        return errors.New("todo is already marked as done")
    }

    if newTitle == "" {
        return errors.New("new title is empty")
    }

    t.title = newTitle
    t.modifiedAt = time.Now()

    return nil
}

func (t *Todo) MarkAsDone() error {
    if t.isMarkedAsDone {
        return errors.New("todo is already marked as done")
    }

    t.isMarkedAsDone = true
    t.modifiedAt = time.Now()

    return nil
}

// 其他setter方法...

将这个待办事项保存到存储中不是问题,因为我可以通过getter方法访问字段。但是,当我从存储中查询标记为完成的待办事项时,我无法从返回的数据中重建领域对象。

构造函数不接受 isMarkedAsDone = true 参数(以及 isMarkedAsDone 字段),如果我尝试创建一个新的领域待办事项并对其调用 MarkAsDone 函数,我会错误地覆盖 modifiedAt 字段。

解决这个问题的常见方法是什么?

  • 将所有内容设为公开?(这对我来说感觉不对,使用者可能会使领域对象处于无效状态)
  • 更改整个构造函数以接受所有字段并验证它们,这样使用者就必须从外部提供所有字段?
  • 保持现状,但在同一个包中提供一个 reconstruct 函数,该函数接受所有字段(并进行验证)来创建领域模型,并且由于它在同一个包中,因此具有对私有字段的写入权限?

更多关于Golang中如何从数据库模型重构领域模型的常用方法的实战教程也可以访问 https://www.itying.com/category-94-b0.html

7 回复

是的,它不需要为每个字段都创建getter/setter方法和选项函数。

更多关于Golang中如何从数据库模型重构领域模型的常用方法的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


确实如此,但在 Todo 上调用像 RenameMarkAsDone 这样的函数时,你仍然会更新 modifiedAt 字段,对吧?

如果使用者随后再次对其调用 WithModifiedAt 会怎样?我个人认为他们不应该能够这样做……

感谢您的回复。但您会如何处理 modifiedAt 字段呢?使用一个 WithModifiedAt 函数吗?

通常我们绝不会设置这样的函数,因为这个字段会在每次修改时自动设置。所以,如果采用选项模式,您也可以设置一个大型函数来验证每个参数,不是吗?

保持一切不变,但在同一个包中提供一个 reconstruct 函数,该函数接受所有字段(+ 验证),创建一个领域模型,并且由于它位于同一个包中,因此具有对私有字段的写入权限?

我认为这是最佳选择。

将所有字段设为公共字段违反了封装原则,而在构造函数中接受所有字段可能导致其被误用。

我注意到这个问题有点久了,但我还是想对代码提供一些反馈。请将此视为一个友好的建议,旨在使您的代码更易于理解。

似乎很多人以这种风格编码,我很好奇这是为什么。思考表情 也许这被视为一种聪明的编码方式,但有时简单性可能更有效,并且对每个人来说都更容易理解。

我想提供一些建议,或许也能帮助其他看到这个帖子的人。

与其专注于一种常见的方法,为什么不追求一种好的方法呢? 领域模型可能不是必需的,而且在这种情况下你也不需要构造函数。你可以直接初始化应用结构体。

我的建议如下:

与其直接在您的 Todo 上使用函数,不如考虑在您的数据库连接器上使用函数。直接使用您的 db.Models,而不是重新定义它们。(假设您正在使用 SQL 来持久化存储待办事项。)

以下是一些伪代码来说明:

type App struct {
    Queries *db.Queries
}

func (app *App) getTodo(todoId uuid.UUID) (todo db.Todo, err error) {
    todo, err := app.Queries.GetTodo(context.TODO(), db.GetTodoParams{ID: todoId})
    // 错误处理
    return todo, err
}

func (app *App) createTodo(name string) (err error) {
    _, err := app.Queries.CreateTodo(context.TODO(), db.CreateTodoParams{Name: name})
    // 错误处理
    return err 
}

尽量避免强制使用面向对象编程。既然您在处理待办事项列表,这是一个很好的练习机会。通过实现两种方法,您将能够看出哪一种效果更好。通常,更简单的解决方案被证明更有效,而且现实生活中的任务本身就会带来足够的复杂性。

Go 语言是简单的。

希望这对您有所帮助。祝您周末愉快!笑脸太阳表情

在Go语言中处理这类问题的惯用法是使用选项模式。请查阅

Go Options Pattern

Go Options Pattern

Go中的灵活构造函数

阅读时间:3分钟


The Option Pattern in Golang - Damavis Blog

Golang中的选项模式 - Damavis博客

Golang是由Google开发的一种编译型编程语言。在这篇文章中,我们将通过一个示例来了解如何实现选项模式。


在某些情况下,我也会应用构建器模式。

Builder in Go / Design Patterns

Go中的构建器模式 / 设计模式

Go中的构建器模式。包含详细注释和解释的完整Go代码示例。构建器是一种创建型设计模式,它允许逐步构建复杂对象。


Understanding the Builder Pattern in Go

理解Go中的构建器模式

构建器模式是一种创建型设计模式,它提供了一种…

在Golang中从数据库模型重构领域模型,通常采用工厂函数或重构函数的方式。以下是几种常见方法:

方法1:使用重构工厂函数

// 在同一包内创建重构函数
func ReconstructTodo(id, title string, isMarkedAsDone bool, modifiedAt time.Time) (*Todo, error) {
    if title == "" {
        return nil, errors.New("title is empty")
    }
    
    if modifiedAt.IsZero() {
        modifiedAt = time.Now()
    }
    
    return &Todo{
        title:          title,
        isMarkedAsDone: isMarkedAsDone,
        modifiedAt:     modifiedAt,
    }, nil
}

// 在repository中使用
type TodoRepository struct {
    db *sql.DB
}

func (r *TodoRepository) FindByStatus(isDone bool) ([]*Todo, error) {
    rows, err := r.db.Query("SELECT id, title, is_marked_as_done, modified_at FROM todos WHERE is_marked_as_done = ?", isDone)
    if err != nil {
        return nil, err
    }
    defer rows.Close()
    
    var todos []*Todo
    for rows.Next() {
        var id, title string
        var isMarkedAsDone bool
        var modifiedAt time.Time
        
        err := rows.Scan(&id, &title, &isMarkedAsDone, &modifiedAt)
        if err != nil {
            return nil, err
        }
        
        todo, err := ReconstructTodo(id, title, isMarkedAsDone, modifiedAt)
        if err != nil {
            return nil, err
        }
        
        todos = append(todos, todo)
    }
    
    return todos, nil
}

方法2:使用内部包结构

// todo.go
package todo

type Todo struct {
    title          string
    isMarkedAsDone bool
    modifiedAt     time.Time
}

// 公开的重构函数
func FromPersistence(title string, isMarkedAsDone bool, modifiedAt time.Time) (*Todo, error) {
    if title == "" {
        return nil, errors.New("title is empty")
    }
    
    return &Todo{
        title:          title,
        isMarkedAsDone: isMarkedAsDone,
        modifiedAt:     modifiedAt,
    }, nil
}

// repository.go (在internal或infra包中)
package repository

import (
    "yourproject/domain/todo"
)

type TodoRepository struct {
    // ...
}

func (r *TodoRepository) FindCompleted() ([]*todo.Todo, error) {
    // 查询数据库
    // ...
    
    // 使用公开的工厂函数重构
    todoItem, err := todo.FromPersistence(
        dbTitle,
        dbIsMarkedAsDone,
        dbModifiedAt,
    )
    
    return []*todo.Todo{todoItem}, nil
}

方法3:使用接口和实现分离

// domain/todo.go
package domain

type Todo interface {
    GetTitle() string
    IsMarkedAsDone() bool
    GetModifiedAt() time.Time
    Rename(newTitle string) error
    MarkAsDone() error
}

// domain/factory.go
package domain

func NewTodo(title string) (Todo, error) {
    return newTodo(title)
}

func ReconstructTodo(title string, isMarkedAsDone bool, modifiedAt time.Time) (Todo, error) {
    return reconstructTodo(title, isMarkedAsDone, modifiedAt)
}

// internal/todo.go
package internal

type todo struct {
    title          string
    isMarkedAsDone bool
    modifiedAt     time.Time
}

func newTodo(title string) (*todo, error) {
    if title == "" {
        return nil, errors.New("title is empty")
    }
    
    return &todo{
        title:          title,
        isMarkedAsDone: false,
        modifiedAt:     time.Now(),
    }, nil
}

func reconstructTodo(title string, isMarkedAsDone bool, modifiedAt time.Time) (*todo, error) {
    if title == "" {
        return nil, errors.New("title is empty")
    }
    
    if modifiedAt.IsZero() {
        modifiedAt = time.Now()
    }
    
    return &todo{
        title:          title,
        isMarkedAsDone: isMarkedAsDone,
        modifiedAt:     modifiedAt,
    }, nil
}

func (t *todo) GetTitle() string {
    return t.title
}

func (t *todo) IsMarkedAsDone() bool {
    return t.isMarkedAsDone
}

func (t *todo) GetModifiedAt() time.Time {
    return t.modifiedAt
}

func (t *todo) Rename(newTitle string) error {
    if t.isMarkedAsDone {
        return errors.New("todo is already marked as done")
    }
    
    if newTitle == "" {
        return errors.New("new title is empty")
    }
    
    t.title = newTitle
    t.modifiedAt = time.Now()
    
    return nil
}

func (t *todo) MarkAsDone() error {
    if t.isMarkedAsDone {
        return errors.New("todo is already marked as done")
    }
    
    t.isMarkedAsDone = true
    t.modifiedAt = time.Now()
    
    return nil
}

方法4:使用包私有重构方法

// todo.go
package todo

type Todo struct {
    title          string
    isMarkedAsDone bool
    modifiedAt     time.Time
}

// 公开的构造函数
func NewTodo(title string) (*Todo, error) {
    return newTodo(title, false, time.Now())
}

// 包私有的重构函数
func newTodo(title string, isMarkedAsDone bool, modifiedAt time.Time) (*Todo, error) {
    if title == "" {
        return nil, errors.New("title is empty")
    }
    
    if modifiedAt.IsZero() {
        modifiedAt = time.Now()
    }
    
    return &Todo{
        title:          title,
        isMarkedAsDone: isMarkedAsDone,
        modifiedAt:     modifiedAt,
    }, nil
}

// 在同一包内的repository中使用
type Repository struct {
    // ...
}

func (r *Repository) loadFromDB(title string, isDone bool, modifiedAt time.Time) (*Todo, error) {
    return newTodo(title, isDone, modifiedAt)
}

最常用的方法是保持领域模型的封装性,在同一包内提供专门的重构函数。这样既能保持领域模型的完整性,又能支持从持久层重建对象的需求。重构函数应该包含必要的验证逻辑,确保重建的对象符合领域规则。

回到顶部