拥有可模拟的 Golang web 应用/ API 结构 -(依赖注入)

huangapple go评论173阅读模式
英文:

Having a mockable Golang web app / api structure - (Dependency Injection)

问题

你好!以下是你要翻译的内容:

你在Go中构建应用程序的最佳方式是什么,以便仍然可以模拟依赖项并进行测试?

在我的应用程序中,我试图拥有一个内部的API(或服务)层,它公开功能。然后,在其之上有一个薄薄的HTTP处理程序层,用于与该服务层进行交互。这样,当我们开始进行RPC通信时,可以重用相同的服务层。
每个关注点都封装在自己的Service结构中,它们都实现相同的接口以实现可互换性。

问题是当不同的服务想要相互通信时。如果注入依赖项,可能会出现循环依赖。服务A使用服务B,而服务B调用服务A。
例如,对应用程序服务的请求将返回请求的应用程序的所有用户。还有一个对用户服务的请求,返回与用户相关联的所有应用程序。

我的想法:
1 - 使用依赖注入容器,并在初始化时将其传递给每个服务。
2 - 使用工厂并传递它,这与第一种方法没有太大区别。

什么是最好的Go方法?

这里是文件的示例结构+示例代码:

/**
.
├── dal
│  ├── application.go
│  └── user.go
├── main.go
├── model
│  ├── application.go
│  └── user.go
├── service
│  ├── application.go
│  └── user.go
└── vendor
*/


package model

type Model interface{} // 通用的模型接口,所有其他模型都将继承自此接口
type UserModel struct{
    UserID int
    Apps []AppModel
} 
type AppModel struct{
    AppID   int
    Users []UserModel
}



// DAL
package dal

import (
    "model"
)

type DAL interface{
    GetByID( id int) model.Model 
    GetAll(filters map[string]string) []model.Model
}

type AppDal struct{}
func (dal AppDal)GetByID(id int) model.Model {}
func (dal AppDal)GetAll(filters map[string]string) []model.Model {}

type UserDal struct{}
func (dal UserDal)GetByID(id int) model.Model {}
func (dal UserDal)GetAll(filters map[string]string) []model.Model {



// Services
package service

import (
    "model"
    "dal"
)

type Service interface{
    GetByID (id int) model.Model 
    GetAll (filters map[string]string) []model.Model 
}

type AppService struct{
    dal dal.DAL
}
func (s AppService) GetByID(id int) model.Model{
    apps := s.dal.GetByID(id)
    // 问题:如何在这里注入userService
    users := userService.GetAll(map[string]string{"ApplicationID": string(id)})
    model.AppModel{Users: users}
    return 
}

func (s AppService) GetAll (filters map[string]string) []model.Model{}

func NewAppService(dal dal.DAL) {
    return AppService{dal:dal}
}


type UserService struct{
    dal dal.DAL
}
func (s UserService) GetByID(id int) model.Model{
    users := s.dal.GetByID(id)
    // 问题:如何在这里注入appService
    apps := appService.GetAll(map[string]string{"UserID": string(id)})
    model.UserModel{Apps: apps}
    return 
}

func (s UserService) GetAll (filters map[string]string) []model.Model{}

func NewUserService(dal dal.DAL) {
    return UserService{dal:dal}
}


// Main

package main

var appDal = AppDal{}
var userDal = UserDal{}

var appService = NewAppService(appDal)
var userService = NewUserService(userDal)

// 我应该将所有服务放在一个DI容器中,并将DIC传递给每个服务。这似乎不太对。

// 伪代码:
http.OnGet("/applications/:id", appService.GetByID)
http.OnGet("/users/:id", userService.GetByID)

希望对你有所帮助!

英文:

What is your best way to structure an app in Go so you can still mock the dependencies and test?

In my app I'm trying to have an internal API ( or service) layer which exposes the functionalities. Then I have a thin layer of HTTP handlers on top to interact with this service layer. This way I can reuse the same Service layer when we start having RPC communications.
Each concern is encapsulated in it's own Service struct, which they all implement the same interface for interchangeability.

The problem is when different services want to talk to one another. If I inject the dependency then I might end up with circular dependencies. Service A uses Service B and Service B calls Service A.
As an example a request to Application Service would return all the Users for the requested App. Also a request to User service returns all the Applications the user is associated with.

My thoughts:
1 - Using a Dependency Injection Container and pass it to each Service at initialization.

2 - Using a factory and pass that around which is not much different.

What would be the best Go approach?

Here is a sample structure of the files + sample code:

/**
.
├── dal
│  ├── application.go
│  └── user.go
├── main.go
├── model
│  ├── application.go
│  └── user.go
├── service
│  ├── application.go
│  └── user.go
└── vendor
*/
package model
type Model interface{} // Generic Model interface so all other Models will inherit from
type UserModel struct{
UserID int
Apps []AppModel
} 
type AppModel struct{
AppID   int
Users []UserModel
}
// DAL
package dal
import (
"model"
)
type DAL interface{
GetByID( id int) model.Model 
GetAll(filters map[string]string) []model.Model
}
type AppDal struct{}
func (dal AppDal)GetByID(id int) model.Model {}
func (dal AppDal)GetAll(filters map[string]string) []model.Model {}
type UserDal struct{}
func (dal UserDal)GetByID(id int) model.Model {}
func (dal UserDal)GetAll(filters map[string]string) []model.Model {}
// Services
package service
import (
"model"
"dal"
)
type Service interface{
GetByID (id int) model.Model 
GetAll (filters map[string]string) []model.Model 
}
type AppService struct{
dal dal.DAL
}
func (s AppService) GetByID(id int) model.Model{
apps := s.dal.GetByID(id)
// Question: How do you inject the userService here
users := userService.GetAll(map[string]string{"ApplicationID": string(id)})
model.AppModel{Users: users}
return 
}
func (s AppService) GetAll (filters map[string]string) []model.Model{}
func NewAppService(dal dal.DAL) {
return AppService{dal:dal}
}
type UserService struct{
dal dal.DAL
}
func (s UserService) GetByID(id int) model.Model{
users := s.dal.GetByID(id)
// Question: How do you inject the appservice here
apps := appService.GetAll(map[string]string{"UserID": string(id)})
model.UserModel{Apps: apps}
return 
}
func (s UserService) GetAll (filters map[string]string) []model.Model{}
func NewUserService(dal dal.DAL) {
return UserService{dal:dal}
}
// Main
package main
var appDal = AppDal{}
var userDal = UserDal{}
var appService = NewAppService (appDal)
var userService = NewUserService (userDal)
// Should I put all services in a DI Container and pass the DIC to each service. That does not seem right.
// psuedo code here:
http.OnGet("/applications/:id", appService.GetByID)
http.OnGet("/users/:id", userService.GetByID)

答案1

得分: 1

请看一下Ben Johnson关于“为增长构建应用程序”的演讲。你的应用程序存在循环依赖,因为你按照人类的逻辑方式来构建应用程序,而不是按照机器的逻辑方式。与其按功能组织代码,不如将领域结构组织在根目录的“main”包中,并为依赖包创建包。你还必须使用接口来避免这种类型的循环依赖。这在演讲中有更详细的解释Structuring applications for growth

特别是在你的应用程序中,如果你想实现依赖注入,你会有严重的实现错误。你应该完全避免在函数中改变包或全局对象。Go不是一种函数式编程语言,但是试图避免副作用问题通常是一种良好的编程方法。例如,UserService:

type UserService struct{
_dal dal.DAL
}
func (s UserService) GetByID(id int) model.Model{
users := s.dal.GetByID(id)
// 问题:你如何在这里注入appservice
apps := appService.GetAll(map[string]string{"UserID": string(id)})
model.UserModel{Apps: apps}
return 
}

GetById需要一个实现DAL接口的appService。你在字段中有一个未使用的_dal(至少看起来是这样)。在函数签名func (s *UserService)中省略*时,你也没有传递对该方法的引用。通常情况下,使用包对象来访问它是不好的。相反,我会以类似这样的方式编写代码:

type DAL interface{
GetByID( id int) (Model, error)
GetAll(filters map[string]string) ([]Model, error)
}
type UserService struct{
InjectedAppService DAL
}
func (s *UserService) GetByID(id int) (Model, error) {
// 问题:你如何在这里注入appservice
apps, err := s.InjectedAppService.GetAll(map[string]string{"UserID": string(id)})
if err != nil {
return nil, err
}
model := UserModel{Apps: apps}
return model 
}
func NewUserService(appService DAL) {
return UserService{appService:dal}
}

实际上,通过阅读代码,我不确定你想做什么,但是现在,GetByID使用了一个接口并返回了一个接口,这是完美的。这使你可以通过实现这些接口来创建模拟对象,并轻松测试这个函数。如果它不在函数外部改变任何东西,它更容易进行测试。

对我来说,看起来你不久前从Java或C++转到Go,并且仍然在思考典型的JEE应用程序。别担心,每个人都会遇到这个问题(包括我),并且需要一些时间才能习惯在Go中有效地工作。

英文:

Take a look at the talk of Ben Johnson about "Structuring applications for growth". You have circular dependencies because you are structuring your app in a human logical way instead that in a machine logical way. Instead or organizing your code in functional groups, it's better to group domain structs in the root "main" package and create packages for dependent packages. You must work with interfaces to avoid this type of circular dependencies too. This is explained with greater detail in the talk Structuring applications for growth

Specifically in your app, you are having serious implementation mistakes if you want to achieve dependency injection. You should completely avoid mutate package or global objects in your functions. Go is not a functional programming but to try to avoid side effects problems is, in general, a good approach for programming. For example the UserService

type UserService struct{
_dal dal.DAL
}
func (s UserService) GetByID(id int) model.Model{
users := s.dal.GetByID(id)
// Question: How do you inject the appservice here
apps := appService.GetAll(map[string]string{"UserID": string(id)})
model.UserModel{Apps: apps}
return 
}

GetById needs an appService which implements the DAL interface. You have a _dal in the fields that you aren't using (at least that is what it seems). You are also not passing a reference to this method when omitting the * on the func signature func (s *UserService). Instead of injecting this dependency explicitly you are using a package object to access to it, which in general is bad. Instead, I would write more or less in this way:

type DAL interface{
GetByID( id int) (Model, error)
GetAll(filters map[string]string) ([]Model, error)
}
type UserService struct{
InjectedAppService DAL
}
func (s *UserService) GetByID(id int) (Model, error) {
// Question: How do you inject the appservice here
apps, err := s.InjectedAppService.GetAll(map[string]string{"UserID": string(id)})
if err != nil {
return nil, err
}
model := UserModel{Apps: apps}
return model 
}
func NewUserService(appService DAL) {
return UserService{appService:dal}
}

In fact, I'm not sure by reading the code what you want to do but, now, GetByID uses an interface and returns an interface, which is perfect. This allow you to create mock objects by implementing those interfaces and test this function easily. If it doesn't mutate anything outside of the function, it's more easily testable.

For me, it seems you have done the jump from Java or C++ to Go not long ago and you are still thinking in typical JEE applications. Don't worry, it happened to everyone (me too) and it takes some time to get used to work effectively in Go.

huangapple
  • 本文由 发表于 2016年8月19日 11:48:36
  • 转载请务必保留本文链接:https://go.coder-hub.com/39030803.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定