适配器模式属于结构型设计模式
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
也简单的实现了。
适配器模式也体现了对扩展开放,对修改关闭的设计原则。