Go 设计模式|项目依赖耦合度太高?可以用适配器做下优化

大家好,这里是每周都在陪你进步的网管~!今天介绍一个在我们在开发做项目时,经常会用到的设计模式—适配器模式。

适配器模式(Adapter Pattern)又叫作变压器模式,它的功能是将一个类的接口变成客户端所期望的另一种接口,从而使原本因接口不匹配而导致无法在一起工作的两个类能够一起工作,属于结构型设计模式。

适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以在一起工作。

我们用UML类图看一下适配器模式的构成

适配器模式的结构

40e656098f261f5ce866540f30f3a1cc.png
类图-适配器模式的结构

适配器模式中的角色构成如下:

  1. 客户端(Client):首先是客户端,这里的客户端可以理解成通过适配器调用服务的代码程序,代码只需通过接口与适配器交互即可, 无需与具体的适配器类耦合。

  2. 客户端接口(Client Interface):这个接口也可被叫做适配器接口,描述了被适配的类与客户端代码协作时必须遵循的约定。

  3. 适配器 (Adapter):作为同时与客户端和服务交互的中介类:它在实现客户端接口的同时封装了服务对象。适配器接受客户端通过适配器接口发起的调用, 并将其转换为适用于被封装服务对象的调用。

  4. 服务(Service):服务通常是一些第三方功能类库或者是一些遗留系统的功能类,客户端与其不兼容,因此无法直接调用其功能,需要适配器进行转换。

通过上面的类图里各个角色类的关联我们可以看到,客户端代码只需通过接口与适配器交互即可, 无需与具体的适配器类耦合。这样, 如果有需求我们就可以向程序中添加新类型的适配器而无需修改已有适配器实现。这在我们的项目需要替换服务类的时候很有用,符合SOLID原则里的开闭原则。

我们先来看看用代码怎么实现适配器模式,稍后再给大家演示一个实践性更高的例子。

//Target 适配器接口,描述客户端和被适配服务间约定的接口
"本文使用的完整可运行源码
去公众号「网管叨bi叨」发送【设计模式】即可领取"
type Target interface {
    Request() string
}

//Adaptee 是被适配的目标接口
type Adaptee interface {
    SpecificRequest() string
}

//NewAdaptee 是被适配接口的工厂函数
func NewAdaptee() Adaptee {
    return &adapteeImpl{}
}

//AdapteeImpl 是被适配的目标类
type adapteeImpl struct{}

//SpecificRequest 是目标类的一个方法
func (*adapteeImpl) SpecificRequest() string {
    return "adaptee method"
}

//NewAdapter 是Adapter的工厂函数
func NewAdapter(adaptee Adaptee) Target {
    return &adapter{
        Adaptee: adaptee,
    }
}

//Adapter 是转换Adaptee为Target接口的适配器
type adapter struct {
    Adaptee
}

//Request 实现Target接口
func (a *adapter) Request() string {
    return a.SpecificRequest()
}

客户端代码直接通过适配器来间接使用被适配对象的功能,解决了两者不兼容的问题。

"本文使用的完整可运行源码
去公众号「网管叨bi叨」发送【设计模式】即可领取"
import "testing"

var expect = "adaptee method"

func TestAdapter(t *testing.T) {
    adaptee := NewAdaptee()
    target := NewAdapter(adaptee)
    res := target.Request()
    if res != expect {
        t.Fatalf("expect: %s, actual: %s", expect, res)
    }
}

用适配器模式引入三方依赖

为什么建议引入依赖库的时候使用适配器模式?项目使用第三方类库的时候,防止未来有更换同等功能类库的可能,一般会推荐使用适配器模式对三方类库做一层封装,这样未来需要用同等功能的服务类进行替换时,实现一个新的适配器包装服务类即可,不需要对已有的客户端代码进行更改。

使用适配器模式,在项目中接入依赖库,这样以后需要替换成其他同等功能的依赖库的时候,不会影响到项目中的通过适配器使用依赖库功能的代码。

下面举一个用适配器适配redigo库为项目提供Redis Cache 功能的例子。

首先我们定义适配器接口,未来所有 Cache 类的适配器需要实现此接口。

import (
  ...
 "github.com/gomodule/redigo/redis"
)
// Cache 定义适配器实现类要实现的接口
type Cache interface {
 Put(key string, value interface{})
 Get(key string) interface{}
 GetAll(keys []string) map[string]interface{}
}

这里为了简洁只定义了三个简单的存取Cache的方法,实际使用时我们可以把常用的Cache操作都定义成接口的方法。

定义适配器实现类, RedisCache 类型会Cache接口,同时我们为RedisCache提供一个工厂方法,在工厂方法里进行 Redis 链接池的初始化

// RedisCache 实现适配器接口
"本文使用的完整可运行源码
去公众号「网管叨bi叨」发送【设计模式】即可领取"
type RedisCache struct {
 conn *redis.Pool
}

// RedisCache的工厂方法
func NewRedisCache() Cache {
 cache := &RedisCache{
  conn: &redis.Pool{
   MaxIdle:     7,
   MaxActive:   30,
   IdleTimeout: 60 * time.Second,
   Dial: func() (redis.Conn, error) {
    conn, err := redis.Dial("tcp", "localhost:6379")
    if err != nil {
     fmt.Println(err)
     return nil, err
    }

    if _, err := conn.Do("SELECT", 0); err != nil {
     conn.Close()
     fmt.Println(err)
     return nil, err
    }

    return conn, nil
   },
  },
 }
 return cache
}

接下来为RedisCache实现 Cache 适配器接口的方法,这三个方法实现分别对应Redis的 SET、GET和MGET操作。

"本文使用的完整可运行源码
去公众号「网管叨bi叨」发送【设计模式】即可领取"
// 缓存数据
func (rc *RedisCache) Put(key string, value interface{}) {
 if _, err := rc.conn.Get().Do("SET", key, value); err != nil {
  fmt.Println(err)
 }
}

// 获取缓存中指定的Key的值
func (rc *RedisCache) Get(key string) interface{} {
 value, err := redis.String(rc.conn.Get().Do("GET", key))
 if err != nil {
  fmt.Println(err)
  return ""
 }
 return value
}

// 从缓存获取多个Key的值
func (rc *RedisCache) GetAll(keys []string) map[string]interface{} {
 intKeys := make([]interface{}, len(keys))
 for i, _ := range keys {
  intKeys[i] = keys[i]
 }

 c := rc.conn.Get()
 entries := make(map[string]interface{})
 values, err := redis.Strings(c.Do("MGET", intKeys...))
 if err != nil {
  fmt.Println(err)
  return entries
 }

 for i, k := range keys {
  entries[k] = values[i]
 }

 return entries
}

客户端在使用Cache时,是直接用Cache接口中定义的方法跟适配器交互,由适配器再去转换成对三方依赖库redigo的调用完成Redis操作。

func main() {
  var rc Cache
 rc = NewRedisCache()
 rc.Put("网管叨逼叨", "rub fish")
}

本文的完整源码,已经同步收录到我整理的电子教程里啦,可向我的公众号「网管叨bi叨」发送关键字【设计模式】领取。

65a303b0a7fc9e146c6bb474447220df.png
公众号「网管叨bi叨」发送关键字【设计模式】领取。

适配器和代理模式的区别

适配器模式和代理模式同属于结构型的设计模式,他们两个在类结构上也非常相似,都是由一个包装对象持有原对象,把客户端对包装对象的请求转发到原对象上。那么这两个模式有什么不同呢?我们怎么区分自己使用的是适配器还是代理模式?

适配器和代理模式的区别:

  1. 适配器与原对象(被适配对象)实现不同的接口,适配器的特点在于兼容,客户端通过适配器的接口完成跟自己不兼容的原对象的访问交互。

  2. 代理与原对象(被代理对象)实现相同的接口,代理模式的特点在于隔离和控制,代理直接转发原对象的返回给客户端,但是可以在调用原始对象接口的前后做一些额外的辅助工作,AOP编程的实现也是利用这个原理。

总结

适配器模式的优点是适配器类和原角色类解耦,提高程序的扩展性。在很多业务场景中符合开闭原则。不改变原有接口,却还能使用新接口的功能。不过适配器的编写过程需要结合业务场景全面考虑,同时也可能会增加系统的复杂性。

今天的文章就到这里啦,喜欢还请点个关注,每周更新最有实用性的编程知识。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值