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

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

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也简单的实现了。

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

相关推荐
JIngJaneIL18 分钟前
基于springboot + vue古城景区管理系统(源码+数据库+文档)
java·开发语言·前端·数据库·vue.js·spring boot·后端
小信啊啊41 分钟前
Go语言切片slice
开发语言·后端·golang
Victor3562 小时前
Netty(20)如何实现基于Netty的WebSocket服务器?
后端
缘不易2 小时前
Springboot 整合JustAuth实现gitee授权登录
spring boot·后端·gitee
Kiri霧3 小时前
Range循环和切片
前端·后端·学习·golang
WizLC3 小时前
【Java】各种IO流知识详解
java·开发语言·后端·spring·intellij idea
Victor3563 小时前
Netty(19)Netty的性能优化手段有哪些?
后端
爬山算法3 小时前
Netty(15)Netty的线程模型是什么?它有哪些线程池类型?
java·后端
白宇横流学长3 小时前
基于SpringBoot实现的冬奥会科普平台设计与实现【源码+文档】
java·spring boot·后端
Python编程学习圈4 小时前
Asciinema - 终端日志记录神器,开发者的福音
后端