手把手带你写一个 Web 框架
叶剑峰
腾讯高级工程师,前滴滴技术专家
22731 人已学习
新⼈⾸单¥59
登录后,你可以任选4讲全文学习
课程目录
已完结/共 42 讲
特别放送 (1讲)
手把手带你写一个 Web 框架
15
15
1.0x
00:00/00:00
登录|注册

11|面向接口编程(下):一切皆服务,服务基于协议

lock: sync.RWMutex
instances: map[string]interface{}
providers: map[string]ServiceProvider
MakeNew(key string, params []interface{}) (interface{}, error)
MustMake(key string) interface{}
Make(key string) (interface{}, error)
IsBind(key string) bool
Bind(provider ServiceProvider) error
Context 允许 Bind 方法的好处和不好的地方
为什么不将 Bind 系列方法也在 Context 中实现?
通过服务提供方创建服务
创建服务提供方 DemoServiceProvider
实现 DemoService 的具体服务实例
设计服务提供方 DemoServiceProvider
创建服务接口文件 contract.go
在 Engine 初始化 Context 的时候,将服务容器传递进入 Context
将服务容器存放在 Engine 中
实现 MakeNew 方法
实现 Make 方法
实现 Bind 方法
HadeContainer 结构
按照面向接口编程,设计服务容器的接口设计
思考题
如何创建一个服务提供方
容器和框架的结合
具体实现
服务容器的实现
面向接口编程

该思维导图由 AI 生成,仅供参考

你好,我是轩脉刃。
之前对比面向过程 / 面向对象,讲解了抽象程度更高的面向接口编程理念,先定义接口再梳理如何通过接口协议交互,最后实现具体模块。
根据这一理念,我们框架的核心设计就是:框架主体作为一个服务容器,其他各个服务模块都作为服务提供者,在服务容器中注册自己的服务凭证和服务接口,通过服务凭证来获取具体的服务实例。这样,功能的具体实现交给了各个服务模块,我们只需要规范服务提供者也就是服务容器中的接口协议。
在上节课也已经完成了服务提供方接口的实现。所以今天就接着实现框架的主体逻辑。

服务容器的实现

首先是服务容器,先看它需要具有什么能力。同样的,按照面向接口编程,我们不考虑具体实现,先思考服务容器的接口设计。
将服务容器实现在 framework/container.go 文件中。
正如之前讨论的,一个服务容器主要的功能是:为服务提供注册绑定、提供获取服务实例,所以服务容器至少有两个方法:注册方法 Bind、获取实例方法 Make。
对于注册的方法,直接将一个服务提供者注册到容器中,参数是之前定义的服务提供者,返回值则是 error 是否注册成功。
// Bind 绑定一个服务提供者,如果关键字凭证已经存在,会进行替换操作,不返回 error
Bind(provider ServiceProvider) error
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

本文介绍了面向接口编程的理念,通过定义接口和协议来实现模块间的交互。作者提出了将框架主体作为服务容器,其他模块作为服务提供者的核心设计思想,并详细讲解了服务容器的接口设计和具体实现,以及HadeContainer数据结构的设计和实现。文章还介绍了如何将服务容器融合进入框架中,并展示了如何创建一个服务提供方和通过服务提供方创建服务的方法。总体而言,本文深入浅出,适合技术人员快速了解面向接口编程的概念和实践方法。 在本文中,作者详细介绍了面向接口编程的核心理念,即将框架主体作为服务容器,其他模块作为服务提供者,通过定义接口和协议来实现模块间的交互。文章深入探讨了服务容器的接口设计和具体实现,以及HadeContainer数据结构的设计和实现,展示了如何将服务容器融合进入框架中,并演示了创建服务提供方和通过服务提供方创建服务的方法。这种设计的拓展性非常好,保证了服务协议不变的情况下,不用担心具体的某个服务实现进行了变化。整体而言,本文对面向接口编程的概念和实践方法进行了深入浅出的阐述,对于技术人员来说具有很高的参考价值。

仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《手把手带你写一个 Web 框架》
新⼈⾸单¥59
立即购买
登录 后留言

全部留言(17)

  • 最新
  • 精选
  • 芒果少侠
    老师我提个并发场景下的潜在bug:如果serviceProvider是延迟加载的话,多个请求同时调用Make的时候,这时候是读锁,所以在后面新建出来实例后,回写map的时候,可能会出现并发写的情况。https://github.com/gohade/coredemo/blob/geekbang/11/framework/container.go#L133 我这边有一个解决思路,即避免并发写map的异常,又可以保证后续并发调用Make的读性能。不知道老师觉得怎么样: func (w *MyContainer) Make(key string) (interface{}, error) { w.lock.RLock() // 简单使用读锁会有并发写map的可能 // defer w.lock.RUnlock() if ins := w.instances[key]; ins != nil { w.lock.RUnlock() return ins, nil } w.lock.RUnlock() w.lock.Lock() defer w.lock.Unlock() // 双重检查,在获得写锁之后,可能有别的协程已经创建完成,可以直接返回,避免再创建 if ins := w.instances[key]; ins != nil { return ins, nil } sp := w.providers[key] if sp == nil { return nil, fmt.Errorf("provider %s not bound", key) } params := sp.Params(w) ins, err := w.newInstance(sp, params) if err != nil { return nil, err } w.instances[key] = ins return ins, nil }

    作者回复: 是的,这里有问题,单纯用读锁是不行的,应该用你这种办法,读锁加写锁的逻辑,感谢指正,我修改下

    2021-10-12
    13
  • 宙斯
    总结: 1 根据容器定义『为服务提供注册绑定、提供获取服务实例』,定义接口Bind,Make方法,并扩展IsBind和MustMake,MakeNew接口规约。 2 定义container结构体,并实现上面的5个规约,特别是Bind和Make实现。 3 融入框架中,并希望在框架中通过context使用,通过在Engine和Context中维护container,并在container中定义Make获取服务的方法。 4 创建服务规约(contract),然后实现服务提供者(provider)和服务具体业务逻辑(server)。 并且需要定义服务结构体,且该服务需要提供NewInstance类型方法,在NewInstance类型方法里是服务和服务提供者真正产生关联的地方(初始化时服务只和服务提供者关联),在main中注册是服务提供者,在controller里通过context调用Make去获取服务。 问题:我们将服务容器的 Make 系列的方法在 Context 中实现了,为什么不把 Bind 系列方法也在 Context 中实现呢? Context 允许 Bind 方法有什么好处和什么不好的地方呢? 回答: 不放Bind到context原因:正如文中提到Bind是服务提供者注册,对服务提供者来说只需要全局注册一次即可,至于里面的服务是实例化一次还是实例多次和服务提供者注册一次没关系,Context是基于请求的它是负责获取服务,服务实例可能是使用单例,也可能是每次请求获得一个新实例,因此Bind通常不由Contxet控制,每次Bind都会覆盖以前相同的Provider。 若Context 允许 Bind 方法好处坏处? 好处:每个请求都会有独立的服务提供者(但没什么用)。 坏处:有并发写入问题,需要加锁,性能降低。

    作者回复: 是的,这里没有必要引入并发问题

    2021-10-24
    3
  • Bind方法我觉得是对于整个项目的生命周期而言的,在整个容器中存一份,比如 连接池对象,和适合单例的对象,将Bind放在context中就相当于面向的是请求级别,实话说没太大用,还让container有了并发的风险 还得做好锁导致性能也下降

    作者回复: 是的,主要是考虑到有并发风险

    2021-12-07
    1
  • Geek_6dc1bc
    容器绑定到ctx中会不会有这个文章中说的:https://cloud.tencent.com/developer/news/462918 ,复用context出现的问题?我看了下gin里面也是用的context pool的机制来保证一个高性能

    作者回复: 不会的,我理解你发的文章中说的是context的复用问题。a携程拿context的时候,context还被b使用着。而我们这个框架底层实际上是net/http。context是每个请求各自一个context。首先这个context不会有问题。 其次我们将容器绑定在context中,只需要控制住容器的原子性,就是在初始化的时候才修改,其他时候都只有获取其中的服务。就不会出现服用问题

    2022-08-02归属地:北京
  • 心平气和
    这个算是微服务架构吗

    作者回复: 微服务更多是从架构层面来进行分割的,哪些业务划分为一个小的服务。 从框架层面,如果内部各个服务依赖的是http协议,这个框架也算是微服务。

    2022-07-13归属地:北京
    2
  • 刚刚重构了一手代码,按照自己的思路写了一些并发的粒度控制 https://github.com/zzm996-zzm/arms/blob/master/framework/container.go 之后会在这个仓库补全单元测试,刚刚发现想测试很多情况 但是手动测试太费劲

    作者回复: 赞

    2021-12-07
    2
  • 宙斯
    你好 在make时的文稿里,容器中还未实例化,第一次实例化,inst, err := hade.newInstance(sp, nil) 这里没有带参数params,这是为什么没有params呢?

    作者回复: 在微信群已经回答了,可以进入下一层看下,没有params就是用provider的params了做参数

    2021-10-24
    2
  • void
    1. HadeContainer 为什么要嵌套 Container?注释说“强制要求 HadeContainer 实现 Container 接口”,但是嵌套做不到强制实现啊。下边代码可以正常运行(不调用接口方法的情况下): type TTT interface { GetName() string } type MyTT struct { TTT name string } func main() { t := MyTT{ TTT: nil, name: "name", } fmt.Println(t) // t.GetName() // 调用方法时才会panic } 如果希望MyTT必须实现TTT的话,使用 “var _ Container = (*HadeContainer)(nil)” 可以在编译时保证。 2. Bind方法78行,为什么要用errors.New,而不是直接返回err? 3. make方法加了读锁,调用的 findServiceProvider 又加了读锁,属于重复加锁。但是后边写map没有加写锁。这个问题别的同学已经提到并给出了改正代码
    2021-11-15
    2
    8
  • fursen
    什么时候是最开心的时候; 就是代码跑起来的时候
    2022-02-01
    2
  • 有点spring的感觉,控制反转,把功能都交给容器进行管理,真的比引入一个包直接使用好么?
    2022-06-07
    1
    1
收起评论
显示
设置
留言
17
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部