douao3063 2016-08-19 03:48
浏览 14

具有可模拟的Golang Web应用程序/ API结构-(依赖注入)

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条回答 默认 最新

  • dongshengyin0147 2016-08-19 06:43
    关注

    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.

    评论

报告相同问题?

悬赏问题

  • ¥15 2020长安杯与连接网探
  • ¥15 关于#matlab#的问题:在模糊控制器中选出线路信息,在simulink中根据线路信息生成速度时间目标曲线(初速度为20m/s,15秒后减为0的速度时间图像)我想问线路信息是什么
  • ¥15 banner广告展示设置多少时间不怎么会消耗用户价值
  • ¥16 mybatis的代理对象无法通过@Autowired装填
  • ¥15 可见光定位matlab仿真
  • ¥15 arduino 四自由度机械臂
  • ¥15 wordpress 产品图片 GIF 没法显示
  • ¥15 求三国群英传pl国战时间的修改方法
  • ¥15 matlab代码代写,需写出详细代码,代价私
  • ¥15 ROS系统搭建请教(跨境电商用途)