前言
看到题目,有人可能会纳闷,其实起因是当我在网络上搜寻无关 golang 依赖注入
、wire
这些关键词的时候,有一些评论是上面这样的:
-
有人认为 依赖注入 不应该呈现在 golang 中,是毒药
-
而也有人认为 依赖注入 是十分好的设计思路,是依赖治理的解药
在通过不少我的项目的磨砺,笔者也终于对依赖注入有了新的意识,但这几个月始终在折腾和纠结,到底要不要写本文。网络上早就曾经有了各种探讨有对于 Golang 是否须要 依赖注入
的呼声。明天,我还是感觉将它换一个角度,作为一个集体的小总结和感悟将它记录下来。
故,本文倡议举荐给下述人群:
- 还在纠结 golang 要不要应用依赖注入的人
- 对 依赖注入 了解还有所纳闷的人
为了简述,下文有以下简称
依赖注入 简称为:DI
面向对象 简称为:OOP
个人观点
我不喜爱浪费时间,也防止题目党嫌疑,所以为了节约工夫,对于曾经晓得 DI 概念的敌人,间接先给出我本人的观点:
- 如果你当初做的我的项目不大,或是集体我的项目,并且还在尝试阶段。齐全的面向过程编程,在 go 中是可行的。
- 但如果你的我的项目比拟大,又是多人合作,我真心倡议你应用 DI,OOP 是有它存在的意义的。
- 如果你没接触过 DI,那么你肯定要尝试去了解它一次,至多给他一次机会,不要自觉听取网络上的声音,实际是测验真谛的唯一标准。
或者听下来,我这个观点如同有点矛盾,如果你违心,请服从我一个过来人,上面几个方面去论述一下。
为什么我说是过来人呢?因为我一开始也是应用的 java 做了很久,spring 也是 YYDS,而后转而到 golang 并且一开始也没有应用依赖注入,而后缓缓在学习过程中有了转变,心愿从这个门路能给你一些思路
Golang 齐全面向过程编程可行吗?
可行!十分明确的通知你。
我之前有幸参加过前公司一个比拟大型的开发我的项目,因为那个时候刚接触 golang,过后大家都还比拟生疏,还在摸索的阶段,过后就是齐全应用了面向过程的形式去编程。我的项目自身蕴含前端和后盾,有 WEB,也有业务,也有各种 SDK 和接口。或者你还对面向过程的形式不太了解,我举几个具体代码例子🌰:
service.GetUserByID(userID)
dao.GetUserByID(userID)
db.Engine.Find()
你是否有相熟的感觉呢?如果有,那可能你和咱们过后差不多。过后所有的函数都是间接应用包名调用的,不须要初始化,也不须要 new 对象,function 内容 就是 过程。
也有几个不言而喻的特色:
- 初始化 全副都在 main 外面实现,包含日志、数据库、缓存等等 …
- 办法的调用都是间接
包名 + 办法名
- 相干依赖中间件的调用 (数据库引擎、缓存实例等) 全副都走的是 全局变量,一次初始化,轻易哪里都能用
PS:其实,当初这个我的项目还有一个 1.0 的版本,在 1.0 的版本中尽管没有应用 DI,然而过后是 OOP 的思维在做的,咱们过后的开发也统一感觉麻烦,所以没有采纳。
整个我的项目当初都还在失常运行,除了 bug 没有问题。
开发实现的感触
- 疾速
- 好了解
- 无扩大
整个我的项目从头至尾咱们就没有定义过几个 interface 去实现,并且咱们当初感觉良好,甚至多拿了点奖金,哈哈。没有意识到任何问题。直到我一直的做我的项目,换了公司才发现,原来挖坑了。
面向过程开发过后的想法
那时,我对依赖注入的想法能够和某些当初的同学是截然不同的,那时我看到 DI 这个货色就是恶感,没有任何去理解的欲望,过后的想法就是上面这样:
- DI == java 的 Spring (过后我看过 spring 的源码,讨厌八股文的同时,也对它有了讨厌)
- 既然我都用 Go 了为啥还要像 Spring 那样非要 New 对象呢?
- Go 为什么还有公司会出 DI 的工具?还会出 Spring 那样相似的框架?
没错,总之那时的我打心底里是不承受 DI 的。我也还停留在 golang NB;less is more
的口嗨当中。
那用了依赖注入之后呢?
直到前两年,我参加了一个新的我的项目之后,才慢慢的明确,为什么会须要 OOP,为什么会须要 DI。以至于之后的各种我的项目都有着 DI 的身影。
新的我的项目
过后因为这个新的我的项目没有 KPI,所以技术选型能够比拟激进,于是咱们将很多新个性和形式使用到了这个我的项目外面,其中就蕴含了 wire
。没错,过后咱们只是想理解到底 wire
做了什么,为什么 google 会开发它,咱们才去应用的。
其实做我的项目的时候有些中央比拟苦楚,一方面咱们须要去理解 wire
的工作形式,一方面因为依赖很多常常会呈现一些依赖的问题须要调整依赖关系节约了很多工夫。最初,我第一次有了一些对 DI 的意识。
为什么须要 OOP
理由 1: 调用办法前保障初始化
从实践上来说,如果你单单只是通过 包名 + 办法名
调用办法,那么势必带来的问题就是,你无奈保障以后办法内所应用的依赖是肯定曾经被初始化实现的。
以数据库操作举例:
- 如果是面向过程,你无奈保障调用
dao
办法的时候,数据库连贯曾经被初始化实现 - 如果是面向对象,当你调用这个对象的办法前,你肯定会
New
这个对象,这个对象的相干依赖肯定会被传递进去,并且曾经被初始化好,所以你能够保障数据库连贯曾经被初始化实现了。
当然你会说,我早就在 main 函数(或者初始化函数)中初始化过数据库连贯了,我一开始也是这样想的,然而起初我发现,你只能说从人为的角度保障了先初始化数据库再应用,而从代码的角度,我其实能够在任意中央调用这个办法。
理由 2: 缩小全局变量
之前面向过程的时候简直全部都是全局变量,数据库 ORM 的引擎是全局变量,配置文件的实体构造也是,过多的全局变量会导致的问题和下面一样,在应用时,你从代码层面无奈保障使用者是在初始化之后进行应用的。
那么也就是意味着,应用可能会导致空指针,也就是没有初始化好,就曾经在应用了。尽管你一样能够说人为的将所有初始化放在 main 中实现。
理由 3: 形象接口,随便切换实现
当你面向过程的时候,你调用某个办法,那就是某个办法,当你想要扭转实现的时候,你只能手动切换别的办法。比方从:dao.GetUserFromDB
改为 dao.GetUserFromCache
然而当你应用 OOP 的时候,你能够将原来的依赖改为依赖接口,并创建对象来实现这个接口。当你须要批改实现的时候,下层无需做任何改变,只须要批改实现的对象就能够了。
为什么须要 DI
那么问题来了,OOP 的确有好的中央,然而这与咱们探讨的 DI 有什么关系,DI 到底解决了什么问题呢?
既然有了 OOP 就有了对象,有了对象就须要 new,就须要治理对象之间的依赖关系。那么 DI 就是为了解决你不须要 new 对象,不须要手动治理依赖关系的问题。
DI:Dependency Injection 依赖注入。如果你是第一次见到这个概念,或者还对这个概念比拟生疏。
我也是从 java 过去的,在 java 中 spring 框架中就有这个概念,过后我在学习 java 的时候就有所理解,但其实当我在 golang 中实际了之后有了更粗浅的意识。
如图,我轻易画了一下可能存在的广泛依赖关系,那么就会遇到上面几个问题:
先有鸡能力有蛋
首先,如果咱们须要调用一个对象的办法,那么第一步须要 new 这个对象。比方,咱们须要应用 userRepo
的 Get 办法,首先咱们须要有 userRepo
对象才能够。这就是面向对象的第一个问题,先有鸡能力有蛋。
先有母鸡能力有小鸡
而后,当咱们的对象依赖于其余对象的时候,咱们须要先初始化其余对象,而后将其余对象传递进去能力进行以后对象的初始化。比方,userRepo
对象须要先初始化数据库 Engine
,那么咱们就须要先 new Engine
对象,而后能力 new userRepo
对象。这就是面向对象的第二个问题,先有母鸡能力有小鸡。
鸡的亲戚关系难治理
最初,因为对象很多,依赖会越来越简单,如果咱们手动去治理这些依赖,那么就会十分麻烦,并且依赖的先后顺序很难被理分明,特地是当新的依赖被增加的时候。这也就是第三个问题,鸡的亲戚关系难治理。
为了解决这些问题,于是依赖注入就呈现了。有了它,最大的特点就是,你不须要 new,也不须要被动去治理依赖关系。
一开始,我接触 Java Spring 的时候经常会听到一句话,有了 spring 你就不必 new 对象了,其实刚学习的时候集体齐全不了解,齐全是一种被动承受,他人写
@Autowired
,我也这么写
应用 wire 实现 DI
在 golang 中实现 DI 最常见的两个库一个是 dig 一个是 wire。实现思路上,dig
应用的是反射,而 wire
应用的是代码生成。反射必定会有性能损失,而 wire
在我应用的过程中还是挺不错,所以这里用 wire
来讲述具体应用状况。
base code
首先,咱们定义一些构造来模仿咱们常常做的 web 我的项目的初始化过程。
type DB struct { }
type Cache struct { }
type UserRepo struct {
DB *DB
Cache *Cache
}
type UserService struct {UserRepo *UserRepo}
type App struct {UserService *UserService}
func (app *App) Start() {fmt.Println("server starting")
}
func NewDB() (*DB, func(), error) {db := &DB{}
cleanup := func() {fmt.Println("close db connection")
}
return db, cleanup, nil
}
func NewCache() *Cache {return &Cache{}
}
func NewUserRepo(db *DB, cache *Cache) *UserRepo {return &UserRepo{DB: db, Cache: cache}
}
func NewUserService(userRepo *UserRepo) *UserService {return &UserService{UserRepo: userRepo}
}
func NewApp(userService *UserService) *App {return &App{UserService: userService}
}
不应用 wire
如果不应用 wire,咱们能够通过手动 new 的形式初始化
func main() {db, cleanup, err := NewDB()
if err != nil {panic(err)
}
defer cleanup()
cache := NewCache()
userRepo := NewUserRepo(db, cache)
userService := NewUserService(userRepo)
app := NewApp(userService)
app.Start()}
应用 DI
须要应用 wire 的话,首先须要创立一个 wire.go
的文件用于生成代码,申明了最初始的入参和最终的产出物
//go:build wireinject
// +build wireinject
package main
import ("github.com/google/wire")
// InitializeApplication
func InitializeApplication() (*App, func(), error) {panic(wire.Build(NewDB, NewCache, NewUserRepo, NewUserService, NewApp))
}
而后咱们只须要在应用的中央调用对应的初始化办法取得产物即可,不须要关怀其中的依赖关系。
func main() {app, cleanup, err := InitializeApplication()
if err != nil {panic(err)
}
defer cleanup()
app.Start()}
最初应用 wire .
命令就能够生成对应的依赖关系和初始化过程
// Code generated by Wire. DO NOT EDIT.
//go:generate go run github.com/google/wire/cmd/wire//go:build !wireinject
// +build !wireinject
package main
// Injectors from wire.go:
// InitializeApplication
func InitializeApplication() (*App, func(), error) {db, cleanup, err := NewDB()
if err != nil {return nil, nil, err}
cache := NewCache()
userRepo := NewUserRepo(db, cache)
userService := NewUserService(userRepo)
app := NewApp(userService)
return app, func() {cleanup()
}, nil
}
其实,咱们能够看到生成的代码和咱们手动写初始化的代码简直截然不同。到这里你可能会感觉,那么我本人写不是也能够吗?没错,在我的项目小的时候简直看不出来劣势,然而当我的项目大了,有许许多多资源的时候初始化就会变得非常复杂。
并且,如果你须要做优雅敞开的时候,你须要程序将依赖一层层的进行:
比方你是先初始化数据库,再初始化缓存,最初启动 http 服务;那么绝对应的进行的时候,你应该先进行 http 服务,再敞开缓存,最初敞开数据库连贯。 如果你先敞开数据库连贯,http 服务仍旧存在,拜访就会出错。
而 wire 在每个 new 办法中反对三个参数,对象,cleanup
,error,其中第二个参数 cleanup
就会在敞开的时候依照依赖的倒序顺次进行敞开。
所以 wire 做的事件就是依据你 new 办法的入参和出参,辨认了他们之间的依赖关系,生成了对应的初始化代码。
我的项目体现
最初当咱们应用了依赖注入之后,体现在我的项目中的应用状况具体表现:
- 咱们再也没有关怀过对象依赖关系初始化程序和 new
- 因为咱们依赖的是接口,实现的切换简直是无痛的,在下层也感知不到数据的起源变动
- 全局变量说拜拜,再也没有呈现说用某个货色空指针,” 哦,不对还没有初始化 ” 的难堪
比照
那么问题来了,就如题目所说的,到底 DI 是解药还是毒药?在网络上搜寻 golang 依赖注入,或者搜 wire,许许多多的人会在上面评论,golang 不须要 DI,把 DI 认为是毒药。golang 就应该简略。DI 齐全是徒增代码简单,并且还多了概念须要让人了解。
其实,我在一开始写 java 的时候就问过这个问题,为什么 java 外面不将所有的办法都申明成 static 这样都不须要 new 间接调用就能够了。
然而当我磨砺了很多我的项目之后,我就有了更加粗浅的了解,为什么之前的人会想要这样去设计,所以我感觉这个问题能够从两个方向上来看:
为什么我之前的我的项目齐全面向过程没有问题
- 所有依赖在一开始就实现了初始化,并且依赖只有配置文件、数据库和缓存
- 我的项目自身性能简直没有二次周期的迭代,性能十分间接,已有性能没有调整,只有新性能的增加
用了 DI 带来了什么收益
- 缩小了全局变量
- 理分明了初始化的依赖关系,并且从代码层面保障你应用时,相干依赖曾经初始化结束
- 能够依赖接口,按需切换实现
论断
回过头来看我一开始说的观点其实就不矛盾了,就拿我本人举例来说,如果是一些小我的项目,并且很多时候 go 并不是做 web 开发,更多的是做工具那么 DI 有时候并不一定须要。
然而对于一些大我的项目来说,我感觉为了当前的思考,还是别挖坑了,无论是从打消全局变量还是扩展性来说,DI 或者说 OOP 都是十分有必要的。
而后,有两点十分重要:
- DI 能够从代码层面间接限度你,依赖必须要初始化能力应用,我十分认可这点
code is law
代码框架即约定能够从很多时候防止问题。一个好的框架,应该从代码层面间接限度掉很多问题的产生,而不应该依赖于程序员的口头约定或者文档约定。 - DI 能够解耦实现,这点也很要害,我晓得并非所有的我的项目都会有革新实现的需要,然而依赖倒置这样的设计模式往往能给当前的扩大提供更好的反对,并且这样的设计思路能够使用在很多代码的其余设计上。
当然,也有两点值得揭示:
- 应用 DI 并非肯定绑定一个工具,并不是肯定要有 wire 或者 dig,如果你的依赖只有一两个手动治理也并非不可,失常的 OOP 也能够写的很优雅。
- 也不是所有全局变量都要一棒子打死,你用
sync.Once
写个 单例模式,也是十分不错的设计。
最初,我感觉,如果你素来没有用过 DI 或者没有了解过它的思维,那么请你用一次,至多明确它的设计思路,或者在别的设计方向上能够给你启发。任何语言都不应该有刻板印象,java 的 spring 并非在 golang 看来一无是处。
咱们编码是为了实现性能,不必管网络的评论或者是他人的说法,实际最重要,只有你用了之后感觉难受感觉爽,就是你认为的能够。
其实我感觉就如同 DDD 相似,很多人感觉 DDD 是银弹,很多人认为 DDD 就是繁琐堆砌了一堆概念。而真正能评估的是深刻应用过他们的人们。
其余参考
当然,兼听则明,偏信则暗
,我在写本文之前,我也曾陷入自我狐疑,特地去采访了一些大厂、中厂的同学,失去的答复是这样的:” 很多做业务的同学都应用了,做基架的有的没用 ”。当然也有局部做业务的同学并没有应用的状况,所以这也是为什么我敢说面向过程也没问题,大可释怀。当然也仅供参考~
本文参加了思否技术征文,欢送正在浏览的你也退出。