什么是适配器(Adapter)模式
适配器设计模式(封装器模式) (refactoringguru.cn) 中有一个很经典的例子,我们可以通过它来了解什么是适配器。
假设我们有一个RoundHole
(圆孔)类,它有自己的radius
(半径)属性,可以通过调用fits(peg RoundPeg)
方法来判断某个RoundPeg
(圆钉)类实例是否能匹配这个RoundHole
,具体实现就是使用getRadius()
来比较两者的半径大小,得到结论。
突然之间,我们增加了一个新的类,就像产品经理在项目末期要求你增加一个新的功能一样,我们得到了SquarePeg
(方钉)类。
-
伪代码,可以忽略
kotlin// 假设你有两个接口相互兼容的类:圆孔(Round•Hole)和圆钉(Round•Peg)。 class RoundHole is constructor RoundHole(radius) { ...... } method getRadius() is // 返回孔的半径。 method fits(peg: RoundPeg) is return this.getRadius() >= peg.getRadius() class RoundPeg is constructor RoundPeg(radius) { ...... } method getRadius() is // 返回钉子的半径。 // 但还有一个不兼容的类:方钉(Square•Peg)。 class SquarePeg is constructor SquarePeg(width) { ...... } method getWidth() is // 返回方钉的宽度。
现在我们希望判断方钉是否能匹配到圆孔中,在代码中我们根据fits(RoundPeg)
的返回结果来判断是否可行,但是问题在于,fits(RoundPeg)
接受的参数是圆钉而非方钉,那么我们该如何实现呢?
我们可以想一下有几种实现方式:
- 定义一个
Peg
的接口,将fits
接收参数由RoundPeg
改为Peg
🧐
这个方法是可行的,听起来不错,但是我们实际想一下,我们将会涉及到fits
的代码改动,又由于fits
方法返回一个bool
值,假如业务中有很多依赖fits
方法的逻辑代码,修改这些逻辑将是一个非常痛苦且容易出错的任务。
在某些情况下我们不希望改动原有的代码,因为重构的成本比增加的成本更高。
更特殊的情况下,假设圆钉和圆孔都是第三方库里面的代码,我们无法直接修改它们的结构,增加一个接口就无从谈起。
- 提供一个
fitsSquarePeg(peg SquarePeg)
方法 🤔
这个方法也是可行的,但是听起来很糟糕。但我们不得不承认这是大多数人选择的方法,有时候我们的排期不科学,或者是强行插入一个需求,我们只能用这种方式实现需求。
它的好处是能快速的满足需求,坏处是你会发现新增的方法和fits
本身很像,只是换了一个对象,逻辑大部分都是相似的,这种重复会给你的代码带来坏味道(Code smell - Wikipedia)。
- 使用适配器( Adapter ) 设计模式 👌
世界上很多充电规范是不一样的,如果你在内地,去到了香港,想要给设备充电你就需要带上转接头。
你会发现这个场景很像我们遇到的问题,把方钉想象成我们原先的充电器,把圆孔想象成香港的插座,那么我们只需要一个转接头(适配器),把方钉接入到转接头中,再把转接头插上插座,那么就能达成我们的目标了。
在上述过程中,转接头就是一个适配器。
让我们回到代码中,我们可以定义一个SquarePegAdapter
(圆孔适配器),其接收一个SquarePeg
(方钉)作为属性,重写了getRadius()
来得到方钉的半径。
这里我们看下书中的伪代码:
scala
// 适配器类让你能够将方钉放入圆孔中。它会对 RoundPeg 类进行扩展,以接收适
// 配器对象作为圆钉。
class SquarePegAdapter extends RoundPeg is
// 在实际情况中,适配器中会包含一个 SquarePeg 类的实例。
private field peg: SquarePeg
constructor SquarePegAdapter(peg: SquarePeg) is
this.peg = peg
method getRadius() is
// 适配器会假扮为一个圆钉,其半径刚好能与适配器实际封装的方钉搭配起来。
return peg.getWidth() * Math.sqrt(2) / 2
如何实现适配器?
下面我们看下更抽象的实现:
Client
是我们业务逻辑代码,作为调用方。Client Interface
是我们为了完成业务逻辑存在的接口。Adapter
是我们的适配器,它持有了一个需要被适配的对象Service,重写了Client Interface
里面的method()
。Service
是作为被代理的对象。
在未使用适配器之前,我们一般只有Client Interface
和Serivce
,我们想通过某种方式让Service
和Client Interface
交互起来,而Adapter
是一种很好的实现。
但请你记住,学习设计模式最重要是学习模式背后的思想,也就是为什么这样设计,这是"形"上的概念,而不是学习该模型的形状,落入到对"形"的模仿中,得不到要领。这里不一定会有Adapter
类,Service
也不一定会是类,我们要记住的是适配
这个概念。
好处和坏处
好处:
- 单一职责原则,我们可以将接口和数据转换代码从业务逻辑中抽离,同时我们不会出现一个圆孔类有若干个
fitXXX()
方法的情况 - 开闭原则,对增加开放,对修改关闭,这能够让我们实现出易维护,可扩展的程序。
坏处:
- 代码复杂度会增加,你需要增加新的类和接口。正如前面提到的,直接修改
Service
或许会更简单。
Go 中的适配器
好了,我们初步了解了适配器模式,下面我们来看下 Go 是怎么实现适配器的。
HTTP Adapter
-
你可能会好奇这些设计的作者是谁,下面是我翻到的资料
"Russ Cox利用函数类型也可以拥有自己的方法这个特性巧妙设计出了http包的HandlerFunc类型,这样通过显式转型即可让一个普通函数成为满足http.Handler接口的类型。" ------ 《Go语言精进之路》
让我们看下HTTP包中Handler
这个接口,它定义了一个非常简单的接口用来响应HTTP请求,接口只有一个方法ServeHTTP(ResponseWriter, *Request)
,只要我们实现这个方法,我们就隐式的实现了Handler
接口。
go
// A Handler responds to an HTTP request.
// ...
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
我们实际使用的时候,可以这样做:
go
func demo(h http.Handler) {}
type MyHandler struct {
}
func (h *MyHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
//TODO implement me
panic("implement me")
}
func main() {
// init
myHandler := &MyHandler{}
demo(myHandler)
}
懒惰是程序员的美德,很多时候我们只希望提供一个ServeHTTP的具体实现,而不希望定义一个结构体。同时,我们也不希望为每个handler
方法绑定一个结构体,想象一下你有30个API接口,你需要绑定30个结构体,这是一个很恐怖的事情,我们希望能这样做:
scss
func demo(h http.Handler) {}
func handler(w http.ResponseWriter, r *http.Request) {}
func main() {
// 这行代码在编译时会报错
demo(handler)
}
但事与愿违,这段代码无法编译,报错原因是:Cannot use 'handler' (type func(w http.ResponseWriter, r *http.Request)) as the type http.Handler Type does not implement 'http.Handler' as some methods are missing: ServeHTTP(ResponseWriter, *Request)
编译器告诉我们,handler这种类型type func(w http.ResponseWriter, r *http.Request)
,它没有实现对应的ServeHTTP方法,所以无法传递给demo
这个期望接收一个http.Handler
函数。
这段有些绕,用开头的例子来说,demo
这个圆孔无法接收你提供的方钉handler
。
此时适配器就能很好的解决这个问题,再往下看,我们可以看到一个非常神奇的类型:
scss
// The HandlerFunc type is an **adapter** to allow the use of
// ordinary functions as HTTP handlers. If f is a function
// with the appropriate signature, HandlerFunc(f) is a
// Handler that calls f.
type HandlerFunc func(ResponseWriter, *Request)
Go的一大特点就是,函数是一等公民,变量也是一等公民,这意味着函数可以像变量(value)一样拥有类型,可以赋值,可以作为参数传递给函数,可以作为函数的返回值,得益于这个特性,我们定义了func(ResponseWriter, *Request)
是一种叫做HandlerFunc
的类型。
我们再往下看:
scss
// adapter...
type HandlerFunc func(ResponseWriter, *Request)
// 实现了Handler接口
// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
这里有一个非常巧妙的操作,HandlerFunc
是一种类型,在Go中,类型也可以拥有自己的方法,即使这种类型是函数。得益于这个特性,我们实现了Handler
接口,但同时我们又不做任何操作,只是使用f(w, r)
来处理逻辑。
我们再看看HandlerFunc
是怎么执行的:
scss
func demo(h http.Handler) {}
func handler(w http.ResponseWriter, r *http.Request) {}
func main() {
// 编译通过
demo(http.HandlerFunc(handler))
}
我们仅仅修改了一个地方,就是http.HandlerFunc(handler)
,就能通过编译了,而这里用到的就是适配器的思想。
http.HandlerFunc(handler)
是将我们的handler
转换为http.HandlerFunc
类型,而http.HandlerFunc
实现了Handler
接口,所以编译可以通过。那么当我们demo
需要执行ServerHTTP
的时候,就是调用f(w, r)
,也就是调用handler(w, r)
,自然就能达到我们想要的目的了。
Ent 中的 Adapter
有了上面这个例子,我们再看看ent是怎么使用设计模式的。
scss
// Querier wraps the basic Query method that is implemented
// by the different builders in this file.
Querier interface {
// Query runs the given query on the graph and returns its result.
Query(context.Context, Query) (Value, error)
}
// The QuerierFunc type is an adapter to allow the use of ordinary// function as Querier. If f is a function with the appropriate signature,
// QuerierFunc(f) is a Querier that calls f.
QuerierFunc func(context.Context, Query) (Value, error)
// Query calls f(ctx, q).func
(f QuerierFunc) Query(ctx context.Context, q Query) (Value, error) {
return f(ctx, q)
}
Querier
是一个接口,QuerierFunc
是一个适配器,它实现了Querier
这个接口。
下面看用法
go
var qr Querier = QuerierFunc(func(ctx context.Context, q Query) (Value, error) {
query, ok := q.(Q1)
if !ok {
return nil, fmt.Errorf("unexpected query type %T", q)
}
if err := selectOrGroup.sqlScan(ctx, query, v); err != nil {
return nil, err
}
if k := rv.Kind(); k == reflect.Pointer && rv.Elem().CanInterface() {
return rv.Elem().Interface(), nil
}
return v, nil
})
...
你会发现这个QuerierFunc(...)接收的匿名函数,就是一个具体的实现,我们通过将这个具体实现,封装成Querier,然后在调用的时候,只需要执行具体实现即可。
markdown
qr.Query(ctx, rootQuery)
总结
在这篇文章中,我们通过实际生活中的例子了解到适配器的思想,同时也学习到了适配器在Go中是如何使用的。
总结一下适配器什么时候可以使用:
- 你定义了一个接口,接口里面有若干个方法,通常只有一个。
- 你希望根据上下文随时实现这个接口,也就是说你可能会通过匿名函数的方式实现,这意味着你没有具体的实现接口结构,你只实现了方法。
- 那么你就可以声明一个Aadpter,把这个接口中的方法,声明为Adapter类型,然后为Adapter实现这个接口。
- 接着,在你需要实现的时候,你就可以直接传递给Adapter,交给Adapter调用。 这就是Adapter的强大和灵活之处。
当然我们学习设计模式最重要的是学习它的思想,不要落入到对模版的追求中而舍弃了对思想的理解。
关注公众号【code路漫漫】,每周更新一篇高质量文章。