在 Go 中使用服务对象

栏目: Go · 发布时间: 5年前

内容简介:服务对象是一般而言,

服务对象是 Ruby on Rails 中一个高度可用的模式,它能够保持控制器和模型简洁干净并从两者中删除域逻辑。在我看来,服务对象是单一责任原则以及通过依赖注入分配责任的一个很好的例子。

一般而言, SOLID 及其背后的理念允许编写可测试的代码,这对于更改非常灵活。 Robert "Uncle Bob" Martin 推动了这些原则。 SOLID 原理理论在 2000 年的论文 Design Principles and Design Patterns. 中有所介绍。 Dave Cheney 有一篇很棒的关于这个原理的文章 SOLID Go Design

Robert Martin 在他的书 Clean Architecture: A Craftsman ’ s Guide to Software Structure and Design 中还提出了一个包含四个级别职责的架构:实体,用例,接口适配器,框架和驱动程序。这个体系结构引入了 用例 ,其原因与 Ruby on Rails 中的服务对象相同 - 用于封装业务逻辑。

广泛使用接口和依赖注入可以使代码独立于 UI,框架和驱动程序。此方法还提供了使用提供的 UI 和存储的模拟实现来测试业务逻辑的能力。

举个例子,让我们看看下面的代码,以及使用 SRP 和引入用例级别会有多好。

// Repository is a data access layer.
type Repository interface {
    Exists(email string) (bool, error)
    Create(*Form) (*User, error)
}

// RegistrationHandler for handling registration requests.
type RegistrationHandler struct {
    Validator *validator.Validate
    Repository
}

// ServerHTTP implements http.Handler.
func (h *RegistrationHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    var f Form
    if err := JSON.NewDecoder(r.Body).Decode(&f); err != nil {
        w.WriteHeader(http.StatusBadRequest)
        return
    }

    validations := make(map[string]string)

    err := h.Validator.Struct(f)
    if err != nil {
        if vs, ok := err.(validator.ValidationErrors); ok {
            for _, v := range vs {
                validations[v.Tag()] = fmt.Sprintf("%s is invalid", v.Tag())
            }
        }
    }

    if f.Password != f.PasswordConfirmation {
        validations["password"] = passwordMismatch
    }

    exists, err := h.Exists(f.Email)
    if err != nil {
        w.WriteHeader(http.StatusInternalServerError)
        return
    }
    if exists {
        validations["email"] = emailExists
    }

    if len(validations) > 0 {
        w.WriteHeader(http.StatusUnprocessableEntity)
        JSON.NewEncoder(w).Encode(validations)
        return
    }

    u, err := h.Create(&f)
    if err != nil {
        w.WriteHeader(http.StatusInternalServerError)
        return
    }

    JSON.NewEncoder(w).Encode(&u)
}

正如您所看到的,除了此处理程序形成传入请求的响应之外,它还包含用户注册过程的所有业务逻辑。每次上一步失败时,此代码都会写入响应并中断其他步骤。

当变化到来时,粘度症状将会上升,因为没有明显的设计可以保留,并且代码的每次更改都会成为某种黑客行为。如果需要将通知发送给注册用户以验证其电子邮件,则代码将变得更难理解和测试。

请参阅 github 上的完整代码示例。

提供的代码示例的好处是前面的开发人员为注册请求创建了集成测试,它不是遗留代码,这意味着它可以被重构。

所以让我们开始一些重构并应用用例级别。第一步是将注册过程逻辑封装到服务对象中。

// Repository is a data access layer.
type Repository interface {
    Unique(email string) error
    Create(*Form) (*User, error)
}

// Validater validation abstraction.
type Validater interface {
    Validate(*Form) error
}

// ValidationErrors holds validation errors.
type ValidationErrors map[string]string

// Error implements error interface.
func (v ValidationErrors) Error() string {
    return validationMsg
}

// Service holds data required for registration.
type Service struct {
    Validater
    Repository
}

// Registrate holds registration domain logic.
func (s *Service) Registrate(f *Form) (*User, error) {
    if err := s.Validater.Validate(f); err != nil {
        return nil, errors.Wrap(err, "validater validate")
    }

    user, err := s.Repository.Create(f)
    if err != nil {
        return nil, errors.Wrap(err, "repository create")
    }

    return user, nil
}

Registrate 方法在系统中注册用户需要两个步骤:

  1. 验证传入的表单。
  2. 将模型插入存储器。

随着服务对象的引入,以前粘性的代码变得更加明显,易于理解。如果发生变化,工程师可能会理解并保留现有设计。

// Registrater abstraction for registration service.
type Registrater interface {
    Registrate(*Form) (*User, error)
}

// RegistrationHandler for regisration requests.
type RegistrationHandler struct {
    Registrater
}

// ServerHTTP implements http.Handler.
func (h *RegistrationHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    var f Form
    if err := JSON.NewDecoder(r.Body).Decode(&f); err != nil {
        w.WriteHeader(http.StatusBadRequest)
        return
    }

    u, err := h.Registrate(&f)
    if err != nil {
        switch v := errors.Cause(err).(type) {
        case ValidationErrors:
            w.WriteHeader(http.StatusUnprocessableEntity)
            JSON.NewEncoder(w).Encode(v)
        default:
            w.WriteHeader(http.StatusInternalServerError)
        }
        return
    }

    JSON.NewEncoder(w).Encode(&u)
}

变更的 代码

在现代 Web 开发中提出了许多要求,其中之一就是可观察性。可观察性包括日志记录,度量和跟踪,这使得能够不对任务的性能和问题的来源进行猜测,而是跟踪和修复问题。

但是,要实现该目标,需要在给定代码中通过将上下文传播到服务对象来实现,以便可以跟踪传入请求,并且可以将与其相关的日志绑定到特定 TraceID

// Service holds data required for registration.
type Service struct {
    Validater
    Repository
}

// Registrate holds registration domain logic.
func (s *Service) Registrate(ctx context.Context, f *Form) (*User, error) {
    if err := s.Validater.Validate(ctx, f); err != nil {
        return nil, errors.Wrap(err, "validater validate")
    }

    user, err := s.Repository.Create(ctx, f)
    if err != nil {
        return nil, errors.Wrap(err, "repository create")
    }

    return user, nil
}

示例中使用到的 contextSameer Ajmari 的博文 Go blog 有所介绍,其中还提及了它应该在所有的传入和传出请求的路径上的传播的好处。

变更的 代码

这样我们现在可以使用装饰器模式扩展服务对象并应用日志记录,跟踪和我们需要的所有其他扩展。您可以直接编写此类装饰器或使用某些 工具 生成它们。已经有一个允许装饰接口的发生器 - gowrap 。Max Chechel-- 作者,在 GoWayFest 2.0 的演讲 "Code Generation to Survive" 中解释了为什么你可能需要这样一个工具。

示例 代码

在服务对象之上应用装饰器模式使我们能够扩展它并达到许多目标,如度量和跟踪等,并将我们的代码移动到现代微服务时代。

尽管 Go 不是通常意义上的 OOP 语言,但用它编写的代码也应该是直观的并且具有明确的结构。这些代码可以使用 SOLID 原则中包含的原则来编写,该原则带有适用于许多编程语言的通用方法集。

本文的目的是表达我对 Go 中编写的代码应该是什么样的理解,并且我希望它包含比负面代码更多的积极方面。


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Dynamic Programming

Dynamic Programming

Richard Bellman / Dover Publications / 2003-03-04 / USD 19.95

An introduction to the mathematical theory of multistage decision processes, this text takes a "functional equation" approach to the discovery of optimum policies. The text examines existence and uniq......一起来看看 《Dynamic Programming》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具