【设计模式】适配器,已有功能扩展?你猜对了

适配器模式属于结构型设计模式

GoF定义:将一个类的接口转换成客户希望的另外一个接口。Adapter模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。

场景

工厂模式介绍中,对接了七牛云存储。

在当时只要求对接七牛,不需要对接其他的云存储,但经过一段时间后,可能会需要对接新的云存储,比如对接阿里云。

适配器模式,在这个时候非常适合了。

实现

定义统一接口

go 复制代码
// IStorage 接口,定义统一的存储操作
type IStorage interface {
	GetUploadManager() IStorage
	UploadLocalFile(filePath string) (string, error)
	UploadBinary(fileReader io.Reader, fileName string) (string, error)
}

// IStorageFactory 工厂接口
type IStorageFactory interface {
	Build() IStorage
}

七牛云存储

在原有的代码上,稍微调整一点

1.第一处
func (q *QiNiu) GetUploadManager() *QiNiu

修改为
func (q *QiNiu) GetUploadManager() IStorage

返回参数的类型调整,使用 IStorage 进行约束

2.第二处
func (f *QiNiuFactory) Build() *QiNiu

修改为
func (f *QiNiuFactory) Build() IStorage

返回参数的类型调整,使用 IStorage 进行约束

3.第三处
func NewQiNiuFactory() *QiNiuFactory

修改为 func NewQiNiuFactory() IStorageFactory

返回参数的类型调整,使用 IStorageFactory 进行约束

修改后如下:

go 复制代码
type QiNiu struct {
	accessKey     string
	secretKey     string
	uploadManager *uploader.UploadManager
	opt           QiNiuOption
}

type QiNiuOptions func(option *QiNiuOption)

type QiNiuOption struct {
	domain string
	bucket string
}

func newQiNiuOption(opts ...QiNiuOptions) QiNiuOption {
	o := QiNiuOption{}
	for _, opt := range opts {
		opt(&o)
	}

	return o
}

func WithDomain(domain string) QiNiuOptions {
	return func(opt *QiNiuOption) {
		opt.domain = domain
	}
}

func WithBucket(bucket string) QiNiuOptions {
	return func(opt *QiNiuOption) {
		opt.bucket = bucket
	}
}

func NewQiNiu(accessKey, secretKey string, opts ...QiNiuOptions) *QiNiu {
	opt := newQiNiuOption(opts...)
	return &QiNiu{
		accessKey: accessKey,
		secretKey: secretKey,
		opt:       opt,
	}
}

func (q *QiNiu) GetUploadManager() IStorage {
	mac := credentials.NewCredentials(q.accessKey, q.secretKey)
	q.uploadManager = uploader.NewUploadManager(&uploader.UploadManagerOptions{
		Options: http_client.Options{
			Credentials: mac,
		},
	})

	return q
}

func (q *QiNiu) UploadLocalFile(filePath string) (string, error) {
	if len(q.opt.bucket) == 0 {
		return "", fmt.Errorf("bucket is empty")
	}

	if q.uploadManager == nil {
		return "", fmt.Errorf("uploadManager not exist")
	}

	fileName := path.Base(filePath)
	fileKey := time.Now().Format(timeFormatLayout) + "/" + fileName
	err := q.uploadManager.UploadFile(context.Background(), fileName, &uploader.ObjectOptions{
		BucketName: q.opt.bucket,
		ObjectName: &fileKey,
		FileName:   fileKey,
	}, nil)
	if err != nil {
		return "", fmt.Errorf("local file upload error:%v", err)
	}

	if len(q.opt.domain) != 0 {
		return q.opt.domain + "/" + fileKey, nil
	}

	return "", nil
}

func (q *QiNiu) UploadBinary(fileReader io.Reader, fileName string) (string, error) {
	if len(q.opt.bucket) == 0 {
		return "", fmt.Errorf("bucket is empty")
	}

	if q.uploadManager == nil {
		return "", fmt.Errorf("uploadManager not exist")
	}

	fileKey := time.Now().Format(timeFormatLayout) + "/" + fileName
	err := q.uploadManager.UploadReader(context.Background(), fileReader, &uploader.ObjectOptions{
		BucketName: q.opt.bucket,
		ObjectName: &fileKey,
		FileName:   fileKey,
	}, nil)
	if err != nil {
		return "", fmt.Errorf("local file upload error:%v", err)
	}

	if len(q.opt.domain) != 0 {
		return q.opt.domain + "/" + fileKey, nil
	}

	return "", nil
}

type QiNiuFactory struct{}

func NewQiNiuFactory() IStorageFactory {
	return &QiNiuFactory{}
}

func (f *QiNiuFactory) Build() IStorage {
	accessKey := "your access key"
	secretKey := "your secret key"
	return NewQiNiu(
		accessKey,
		secretKey,
		WithDomain("http://xxx.com"),
		WithBucket("med-assets"),
	)
}

阿里云存储

段代码中 func (a *AliYun) GetUploadManager() Storage 完全是为了兼容 原先对接的QiNiu

最小改动嘛,小伙伴们应该都理解。

不能影响原已经对接了七牛云存储的功能,这个在工作中是特别重要的,一定要考虑!

go 复制代码
// AliYun 结构体,适配阿里云
type AliYun struct {
	client *oss.Client
	bucket *oss.Bucket
	opt    AliYunOption
}

type AliYunOption struct {
	endpoint string
	bucket   string
}

type AliYunOptions func(option *AliYunOption)

func WithEndpoint(endpoint string) AliYunOptions {
	return func(opt *AliYunOption) {
		opt.endpoint = endpoint
	}
}

func WithAliYunBucket(bucket string) AliYunOptions {
	return func(opt *AliYunOption) {
		opt.bucket = bucket
	}
}

func NewAliYun(accessKey, secretKey string, opts ...AliYunOptions) *AliYun {
	opt := AliYunOption{}
	for _, optFunc := range opts {
		optFunc(&opt)
	}

	client, err := oss.New(opt.endpoint, accessKey, secretKey)
	if err != nil {
		panic(fmt.Sprintf("Failed to create OSS client: %v", err))
	}

	bucket, _ := client.Bucket(opt.bucket)
	return &AliYun{
		client: client,
		bucket: bucket,
		opt:    opt,
	}
}

func (a *AliYun) GetUploadManager() IStorage {
	return a
}

func (a *AliYun) UploadLocalFile(filePath string) (string, error) {
	if a.bucket == nil {
		return "", fmt.Errorf("bucket not exist")
	}

	fileName := path.Base(filePath)
	fileKey := time.Now().Format(timeFormatLayout) + "/" + fileName

	err := a.bucket.PutObjectFromFile(fileKey, filePath)
	if err != nil {
		return "", fmt.Errorf("local file upload error:%v", err)
	}

	return fmt.Sprintf("http://%s.%s/%s", a.opt.bucket, a.opt.endpoint, fileKey), nil
}

func (a *AliYun) UploadBinary(fileReader io.Reader, fileName string) (string, error) {
	if a.bucket == nil {
		return "", fmt.Errorf("bucket not exist")
	}

	fileKey := time.Now().Format(timeFormatLayout) + "/" + fileName

	err := a.bucket.PutObject(fileKey, fileReader)
	if err != nil {
		return "", fmt.Errorf("binary file upload error:%v", err)
	}

	return fmt.Sprintf("http://%s.%s/%s", a.opt.bucket, a.opt.endpoint, fileKey), nil
}

// AliYunFactory 阿里云工厂
type AliYunFactory struct{}

func NewAliYunFactory() IStorageFactory {
	return &AliYunFactory{}
}

func (f *AliYunFactory) Build() IStorage {
	accessKey := "your_aliyun_access_key"
	secretKey := "your_aliyun_secret_key"
	return NewAliYun(
		accessKey,
		secretKey,
		WithEndpoint("oss-cn-hangzhou.aliyuncs.com"),
		WithAliYunBucket("your-bucket"),
	)
}

好了,到此就算是修改好了

测试七牛云

go 复制代码
qiNiu := NewQiNiuFactory().Build()

// 本地文件上传
fileUrl, err := qiNiu.GetUploadManager().UploadLocalFile("./avatar.png")
if err != nil {
	fmt.Println(err)
}

fmt.Println(fileUrl)

// 数据流上传
filePath := "./nginx.jpg"
fileReader, err := os.Open(filePath)
if err != nil {
	fmt.Println("file open error", err)
	return
}
defer fileReader.Close()

fileUrl, err = qiNiu.GetUploadManager().UploadBinary(fileReader, path.Base(filePath))
if err != nil {
	fmt.Println(err)
}

fmt.Println(fileUrl)

测试阿里云

go 复制代码
// 使用阿里云存储
aliYunFactory := NewAliYunFactory()
aliYun := aliYunFactory.Build()

// 本地文件上传
fileUrl, err = aliYun.UploadLocalFile("./avatar.png")
if err != nil {
	fmt.Println("Error:", err)
	return
}
fmt.Println("阿里云上传结果:", fileUrl)

// 数据流上传
filePath = "./nginx.jpg"
fileReader, err = os.Open(filePath)
if err != nil {
	fmt.Println("文件打开错误:", err)
	return
}
defer fileReader.Close()

fileUrl, err = aliYun.UploadBinary(fileReader, path.Base(filePath))
if err != nil {
	fmt.Println("Error:", err)
	return
}
fmt.Println("阿里云数据流上传结果:", fileUrl)

完整代码

请根据自己的go版本进行安装 阿里云的sdk

go version go1.19.13
github.com/aliyun/aliyun-oss-go-sdk/oss 用的是 1.9.0

go 复制代码
go get github.com/aliyun/aliyun-oss-go-sdk/oss@1.9.0
go 复制代码
package main

import (
	"context"
	"fmt"
	"io"
	"os"
	"path"
	"time"

	"github.com/aliyun/aliyun-oss-go-sdk/oss"

	"github.com/qiniu/go-sdk/v7/storagev2/credentials"
	"github.com/qiniu/go-sdk/v7/storagev2/http_client"
	"github.com/qiniu/go-sdk/v7/storagev2/uploader"
)

const timeFormatLayout = "20060102"

// IStorage 接口,定义统一的存储操作
type IStorage interface {
	GetUploadManager() IStorage
	UploadLocalFile(filePath string) (string, error)
	UploadBinary(fileReader io.Reader, fileName string) (string, error)
}

// IStorageFactory 工厂接口
type IStorageFactory interface {
	Build() IStorage
}

type QiNiu struct {
	accessKey     string
	secretKey     string
	uploadManager *uploader.UploadManager
	opt           QiNiuOption
}

type QiNiuOptions func(option *QiNiuOption)

type QiNiuOption struct {
	domain string
	bucket string
}

func newQiNiuOption(opts ...QiNiuOptions) QiNiuOption {
	o := QiNiuOption{}
	for _, opt := range opts {
		opt(&o)
	}

	return o
}

func WithDomain(domain string) QiNiuOptions {
	return func(opt *QiNiuOption) {
		opt.domain = domain
	}
}

func WithBucket(bucket string) QiNiuOptions {
	return func(opt *QiNiuOption) {
		opt.bucket = bucket
	}
}

func NewQiNiu(accessKey, secretKey string, opts ...QiNiuOptions) *QiNiu {
	opt := newQiNiuOption(opts...)
	return &QiNiu{
		accessKey: accessKey,
		secretKey: secretKey,
		opt:       opt,
	}
}

func (q *QiNiu) GetUploadManager() IStorage {
	mac := credentials.NewCredentials(q.accessKey, q.secretKey)
	q.uploadManager = uploader.NewUploadManager(&uploader.UploadManagerOptions{
		Options: http_client.Options{
			Credentials: mac,
		},
	})

	return q
}

func (q *QiNiu) UploadLocalFile(filePath string) (string, error) {
	if len(q.opt.bucket) == 0 {
		return "", fmt.Errorf("bucket is empty")
	}

	if q.uploadManager == nil {
		return "", fmt.Errorf("uploadManager not exist")
	}

	fileName := path.Base(filePath)
	fileKey := time.Now().Format(timeFormatLayout) + "/" + fileName
	err := q.uploadManager.UploadFile(context.Background(), fileName, &uploader.ObjectOptions{
		BucketName: q.opt.bucket,
		ObjectName: &fileKey,
		FileName:   fileKey,
	}, nil)
	if err != nil {
		return "", fmt.Errorf("local file upload error:%v", err)
	}

	if len(q.opt.domain) != 0 {
		return q.opt.domain + "/" + fileKey, nil
	}

	return "", nil
}

func (q *QiNiu) UploadBinary(fileReader io.Reader, fileName string) (string, error) {
	if len(q.opt.bucket) == 0 {
		return "", fmt.Errorf("bucket is empty")
	}

	if q.uploadManager == nil {
		return "", fmt.Errorf("uploadManager not exist")
	}

	fileKey := time.Now().Format(timeFormatLayout) + "/" + fileName
	err := q.uploadManager.UploadReader(context.Background(), fileReader, &uploader.ObjectOptions{
		BucketName: q.opt.bucket,
		ObjectName: &fileKey,
		FileName:   fileKey,
	}, nil)
	if err != nil {
		return "", fmt.Errorf("local file upload error:%v", err)
	}

	if len(q.opt.domain) != 0 {
		return q.opt.domain + "/" + fileKey, nil
	}

	return "", nil
}

type QiNiuFactory struct{}

func NewQiNiuFactory() IStorageFactory {
	return &QiNiuFactory{}
}

func (f *QiNiuFactory) Build() IStorage {
	accessKey := "your access key"
	secretKey := "your secret key"
	return NewQiNiu(
		accessKey,
		secretKey,
		WithDomain("http://xxx.com"),
		WithBucket("med-assets"),
	)
}

// AliYun 结构体,适配阿里云
type AliYun struct {
	client *oss.Client
	bucket *oss.Bucket
	opt    AliYunOption
}

type AliYunOption struct {
	endpoint string
	bucket   string
}

type AliYunOptions func(option *AliYunOption)

func WithEndpoint(endpoint string) AliYunOptions {
	return func(opt *AliYunOption) {
		opt.endpoint = endpoint
	}
}

func WithAliYunBucket(bucket string) AliYunOptions {
	return func(opt *AliYunOption) {
		opt.bucket = bucket
	}
}

func NewAliYun(accessKey, secretKey string, opts ...AliYunOptions) *AliYun {
	opt := AliYunOption{}
	for _, optFunc := range opts {
		optFunc(&opt)
	}

	client, err := oss.New(opt.endpoint, accessKey, secretKey)
	if err != nil {
		panic(fmt.Sprintf("Failed to create OSS client: %v", err))
	}

	bucket, _ := client.Bucket(opt.bucket)
	return &AliYun{
		client: client,
		bucket: bucket,
		opt:    opt,
	}
}

func (a *AliYun) GetUploadManager() IStorage {
	return a
}

func (a *AliYun) UploadLocalFile(filePath string) (string, error) {
	if a.bucket == nil {
		return "", fmt.Errorf("bucket not exist")
	}

	fileName := path.Base(filePath)
	fileKey := time.Now().Format(timeFormatLayout) + "/" + fileName

	err := a.bucket.PutObjectFromFile(fileKey, filePath)
	if err != nil {
		return "", fmt.Errorf("local file upload error:%v", err)
	}

	return fmt.Sprintf("http://%s.%s/%s", a.opt.bucket, a.opt.endpoint, fileKey), nil
}

func (a *AliYun) UploadBinary(fileReader io.Reader, fileName string) (string, error) {
	if a.bucket == nil {
		return "", fmt.Errorf("bucket not exist")
	}

	fileKey := time.Now().Format(timeFormatLayout) + "/" + fileName

	err := a.bucket.PutObject(fileKey, fileReader)
	if err != nil {
		return "", fmt.Errorf("binary file upload error:%v", err)
	}

	return fmt.Sprintf("http://%s.%s/%s", a.opt.bucket, a.opt.endpoint, fileKey), nil
}

// AliYunFactory 阿里云工厂
type AliYunFactory struct{}

func NewAliYunFactory() IStorageFactory {
	return &AliYunFactory{}
}

func (f *AliYunFactory) Build() IStorage {
	accessKey := "your_aliyun_access_key"
	secretKey := "your_aliyun_secret_key"
	return NewAliYun(
		accessKey,
		secretKey,
		WithEndpoint("oss-cn-hangzhou.aliyuncs.com"),
		WithAliYunBucket("your-bucket"),
	)
}

func main() {
	qiNiu := NewQiNiuFactory().Build()

	// 本地文件上传
	fileUrl, err := qiNiu.GetUploadManager().UploadLocalFile("./avatar.png")
	if err != nil {
		fmt.Println(err)
	}

	fmt.Println(fileUrl)

	// 数据流上传
	filePath := "./nginx.jpg"
	fileReader, err := os.Open(filePath)
	if err != nil {
		fmt.Println("file open error", err)
		return
	}
	defer fileReader.Close()

	fileUrl, err = qiNiu.GetUploadManager().UploadBinary(fileReader, path.Base(filePath))
	if err != nil {
		fmt.Println(err)
	}

	fmt.Println(fileUrl)


	// 使用阿里云存储
	aliYunFactory := NewAliYunFactory()
	aliYun := aliYunFactory.Build()

	// 本地文件上传
	fileUrl, err = aliYun.UploadLocalFile("./avatar.png")
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	fmt.Println("阿里云上传结果:", fileUrl)

	// 数据流上传
	filePath = "./nginx.jpg"
	fileReader, err = os.Open(filePath)
	if err != nil {
		fmt.Println("文件打开错误:", err)
		return
	}
	defer fileReader.Close()

	fileUrl, err = aliYun.UploadBinary(fileReader, path.Base(filePath))
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	fmt.Println("阿里云数据流上传结果:", fileUrl)
}

若需要动态的使用不同的存储进行业务逻辑的处理,可以结合策略模式

go 复制代码
type StorageFactoryFunc func() IStorageFactory

var storageFactoryFuncMap = map[string]StorageFactoryFunc{
	"aliyun": NewAliYunFactory,
	"qiniu":  NewQiNiuFactory,
}
go 复制代码
// 数据流上传
filePath = "./nginx.jpg"
fileReader, err = os.Open(filePath)
if err != nil {
	fmt.Println("文件打开错误:", err)
	return
}
defer fileReader.Close()
factoryFunc := storageFactoryFuncMap["aliyun"] // 选择阿里云存储上传
fmt.Println(factoryFunc().Build().UploadBinary(fileReader, path.Base(filePath)))

总结

在扩展功能的时候,一定一定要注意不能影响已有的功能,所以本次扩展使用阿里云存储的时候GetUploadManager也简单的实现了。

适配器模式也体现了对扩展开放,对修改关闭的设计原则。

相关推荐
AronTing7 分钟前
备忘录模式:实现对象状态撤销与恢复的设计模式
java·后端·面试
洛卡卡了7 分钟前
别人都在用 Redis Cluster 高可用了,你还在手动重启 Redis?😅
redis·后端
AronTing10 分钟前
工厂模式:解耦对象创建与使用的设计模式
java·后端·面试
AronTing12 分钟前
解释器模式:自定义语言解析与执行的设计模式
java·后端·设计模式
赵晟鼎17 分钟前
23种设计模式之建造者模式
前端·javascript·设计模式
猿必过20 分钟前
Trae国内最新版本来袭,不服来战
后端
PasteCode23 分钟前
给你们安利一个“敏捷开发框架天花板”的框架,DeepSeek评价的最佳开发框架,主要是针对表单,管理端的
后端
韩zj28 分钟前
Android调用springboot接口上传大字段,偶现接口超时的优化
android·spring boot·后端
Piper蛋窝35 分钟前
Go 1.13 相比 Go 1.12 有哪些值得注意的改动?
go
GeekAGI40 分钟前
如何移除 MongoDB 分片 (Remove Shard)
后端