基于Go-Kit的Golang整洁架构实践( 二 )

  • DecodeRequestFunc将HTTP请求转换为Request Model,并且
  • EncodeResponseFunc格式化Response Model并将其编码到HTTP响应中 。
  • 返回的*server实现http.Server(有ServeHTTP方法) 。
传输层使用这个函数来创建http.Server,解码器和编码器在传输中定义 , 端点在运行时初始化 。
简短示例:(基于发货示例[6])
简易服务我们将描述一个具有两个API的简单服务,用于从数据层创建和读取文章,传输层是HTTP,数据层只是一个内存映射 。可以在这里找到Github源代码[7] 。
注意文件结构:
- inmem- articlerepo.go- publishing- transport.go- endpoint.go- service.go- formatter.go- article- article.go我们看看如何表示整洁架构的不同层 。
  • article —— 这是实体层,不包含BL、数据层或传输层的知识 。
  • inmem —— 这是数据层 。
  • transport —— 这是传输层 。
  • endpoint+service —— 组成了边界+交互器 。
从服务开始:
import ("context""fmt""math/rand""github.com/OrenRosen/gokit-example/article")type ArticlesRepository interface {GetArticle(ctx context.Context, id string) (article.Article, error)InsertArticle(ctx context.Context, thing article.Article) error}type service struct {repo ArticlesRepository}func NewService(repo ArticlesRepository) *service {return &service{repo: repo,}}func (s *service) GetArticle(ctx context.Context, id string) (article.Article, error) {return s.repo.GetArticle(ctx, id)}func (s *service) CreateArticle(ctx context.Context, artcle article.Article) (id string, err error) {artcle.ID = generateID()if err := s.repo.InsertArticle(ctx, artcle); err != nil {return "", fmt.Errorf("publishing.CreateArticle: %w", err)}return artcle.ID, nil}func generateID() string {// code emitted}服务对交付和数据层一无所知,它不从外层(HTTP、inmem…)导入任何东西 。BL就在这里,你可能会说这里没有真正的BL , 这里的服务可能是冗余的,但需要记住这只是一个简单示例 。
实体package articletype Article struct {IDstringTitle stringTextstring}实体只是一个DTO,如果有业务策略或行为,可以添加到这里 。
端点endpoint.go定义了服务接口:
type Service interface {GetArticle(ctx context.Context, id string) (article.Article, error)CreateArticle(ctx context.Context, thing article.Article) (id string, err error)}然后为每个用例(RPC)定义一个端点 。例如,对于获取文章::
type GetArticleRequestModel struct {ID string}type GetArticleResponseModel struct {Article article.Article}func MakeEndpointGetArticle(s Service) endpoint.Endpoint {return func(ctx context.Context, request interface{}) (response interface{}, err error) {req, ok := request.(GetArticleRequestModel)if !ok {return nil, fmt.Errorf("MakeEndpointGetArticle failed cast request")}a, err := s.GetArticle(ctx, req.ID)if err != nil {return nil, fmt.Errorf("MakeEndpointGetArticle: %w", err)}return GetArticleResponseModel{Article: a,}, nil}}注意如何定义RequestModel和ResponseModel,这是RPC的输入/输出 。其思想是,可以看到所需数据(输入)和返回数据(输出),甚至无需读取端点本身的实现,因此我认为端点代表单个RPC 。服务具有实际触发BL的方法,但是端点是RPC的应用定义 。理论上,一个端点可以触发多个BL方法 。
传输transport.go注册HTTP路由:
type Router interface {Handle(method, path string, handler http.Handler)}func RegisterRoutes(router *httprouter.Router, s Service) {getArticleHandler := kithttp.NewServer(MakeEndpointGetArticle(s),decodeGetArticleRequest,encodeGetArticleResponse,)createArticleHandler := kithttp.NewServer(MakeEndpointCreateArticle(s),decodeCreateArticleRequest,encodeCreateArticleResponse,)router.Handler(http.MethodGet, "/articles/:id", getArticleHandler)router.Handler(http.MethodPost, "/articles", createArticleHandler)}传输层通过MakeEndpoint函数在运行时创建端点 , 并提供用于反序列化请求的解码器和用于格式化和编码响应的编码器 。
例如:
func decodeGetArticleRequest(ctx context.Context, r *http.Request) (request interface{}, err error) {params := httprouter.ParamsFromContext(ctx)return GetArticleRequestModel{ID: params.ByName("id"),}, nil}func encodeGetArticleResponse(ctx context.Context, w http.ResponseWriter, response interface{}) error {res, ok := response.(GetArticleResponseModel)if !ok {return fmt.Errorf("encodeGetArticleResponse failed cast response")}formatted := formatGetArticleResponse(res)w.Header().Set("Content-Type", "Application/json")return json.NewEncoder(w).Encode(formatted)}func formatGetArticleResponse(res GetArticleResponseModel) map[string]interface{} {return map[string]interface{}{"data": map[string]interface{}{"article": map[string]interface{}{"id":res.Article.ID,"title": res.Article.Title,"text":res.Article.Text,},},}}


推荐阅读