集成服务可扩展性设计
概述
可扩展性设计规范定义了如何设计易于扩展和替换的集成服务。核心思想是面向接口编程,使用工厂模式选择具体实现。
接口抽象
为什么需要接口
对于可能更换服务提供商的集成,定义抽象接口:
- ✅ 易于替换:更换服务提供商只需实现同一接口
- ✅ 易于测试:可以使用 Mock 实现进行单元测试
- ✅ 配置驱动:通过配置选择具体实现
接口定义示例
go
// clients/storage/storage.go
package storage
import (
"context"
"io"
)
// Storage 对象存储接口
type Storage interface {
// Upload 上传文件
Upload(ctx context.Context, key string, data io.Reader) (*UploadResult, error)
// Download 下载文件
Download(ctx context.Context, key string) (io.ReadCloser, error)
// Delete 删除文件
Delete(ctx context.Context, key string) error
// GetURL 获取文件访问 URL
GetURL(ctx context.Context, key string, expires time.Duration) (string, error)
// Close 关闭连接
Close() error
}
// UploadResult 上传结果
type UploadResult struct {
Key string
URL string
Size int64
UploadAt time.Time
}多实现支持
本地存储实现
go
// clients/storage/local.go
package storage
import (
"context"
"fmt"
"io"
"os"
"path/filepath"
"time"
)
// LocalStorage 本地存储实现
type LocalStorage struct {
basePath string
baseURL string
}
// NewLocalStorage 创建本地存储实例
func NewLocalStorage(basePath, baseURL string) (*LocalStorage, error) {
// 确保目录存在
if err := os.MkdirAll(basePath, 0755); err != nil {
return nil, fmt.Errorf("failed to create storage directory: %w", err)
}
return &LocalStorage{
basePath: basePath,
baseURL: baseURL,
}, nil
}
// Upload 上传文件到本地目录
func (s *LocalStorage) Upload(ctx context.Context, key string, data io.Reader) (*UploadResult, error) {
filePath := filepath.Join(s.basePath, key)
// 创建文件目录
if err := os.MkdirAll(filepath.Dir(filePath), 0755); err != nil {
return nil, err
}
// 创建文件
file, err := os.Create(filePath)
if err != nil {
return nil, err
}
defer file.Close()
// 写入数据
size, err := io.Copy(file, data)
if err != nil {
return nil, err
}
return &UploadResult{
Key: key,
URL: fmt.Sprintf("%s/%s", s.baseURL, key),
Size: size,
UploadAt: time.Now(),
}, nil
}
func (s *LocalStorage) Download(ctx context.Context, key string) (io.ReadCloser, error) {
filePath := filepath.Join(s.basePath, key)
return os.Open(filePath)
}
func (s *LocalStorage) Delete(ctx context.Context, key string) error {
filePath := filepath.Join(s.basePath, key)
return os.Remove(filePath)
}
func (s *LocalStorage) GetURL(ctx context.Context, key string, expires time.Duration) (string, error) {
return fmt.Sprintf("%s/%s", s.baseURL, key), nil
}
func (s *LocalStorage) Close() error {
return nil
}S3 存储实现
go
// clients/storage/s3.go
package storage
import (
"context"
"io"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/service/s3"
)
// S3Storage S3 存储实现
type S3Storage struct {
client *s3.Client
bucket string
}
// NewS3Storage 创建 S3 存储实例
func NewS3Storage(cfg *StorageConfig) (*S3Storage, error) {
s3Config, err := config.LoadDefaultConfig(context.Background(),
config.WithRegion(cfg.Region),
config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(
cfg.AccessKey,
cfg.SecretKey,
"",
)),
)
if err != nil {
return nil, err
}
return &S3Storage{
client: s3.NewFromConfig(s3Config),
bucket: cfg.Bucket,
}, nil
}
func (s *S3Storage) Upload(ctx context.Context, key string, data io.Reader) (*UploadResult, error) {
_, err := s.client.PutObject(ctx, &s3.PutObjectInput{
Bucket: aws.String(s.bucket),
Key: aws.String(key),
Body: data,
})
if err != nil {
return nil, err
}
return &UploadResult{
Key: key,
UploadAt: time.Now(),
}, nil
}
// 实现其他方法...腾讯云 COS 实现
go
// clients/storage/cos.go
package storage
// COSStorage 腾讯云 COS 存储实现
type COSStorage struct {
// COS 客户端
}
func NewCOSStorage(cfg *StorageConfig) (*COSStorage, error) {
// 初始化 COS 客户端
return &COSStorage{}, nil
}
// 实现 Storage 接口方法...工厂模式
工厂函数
通过配置选择具体实现:
go
// clients/storage/factory.go
package storage
import (
"fmt"
"your-project/config"
)
// NewStorage 创建存储实例(工厂函数)
func NewStorage(cfg *config.StorageConfig) (Storage, error) {
// 校验配置
if err := cfg.Validate(); err != nil {
return nil, fmt.Errorf("invalid storage config: %w", err)
}
// 根据类型创建具体实现
switch cfg.Type {
case "local":
return NewLocalStorage(cfg.BasePath, cfg.BaseURL)
case "s3":
return NewS3Storage(cfg)
case "cos":
return NewCOSStorage(cfg)
case "oss":
return NewOSSStorage(cfg)
default:
return nil, fmt.Errorf("unsupported storage type: %s", cfg.Type)
}
}App 容器使用工厂
go
// app/app.go
func (a *App) initClients() error {
// 使用工厂函数创建 Storage
storageClient, err := storage.NewStorage(&a.config.Storage)
if err != nil {
return fmt.Errorf("failed to init storage: %w", err)
}
a.storageClient = storageClient
return nil
}配置驱动
toml
# config.toml
# 本地存储配置
[storage]
type = "local"
base_path = "/var/app/uploads"
base_url = "http://localhost:8080/uploads"
# 切换为 S3
[storage]
type = "s3"
endpoint = "https://s3.amazonaws.com"
region = "us-west-2"
bucket = "my-app-uploads"新增集成步骤
步骤 1:定义配置
go
// config/config.go
type SMSConfig struct {
Provider string `toml:"provider"` // aliyun/tencent
AppKey string `toml:"app_key"`
AppSecret string `toml:"app_secret"`
SignName string `toml:"sign_name"`
}
func (c *SMSConfig) Validate() error {
if c.Provider == "" {
return errors.New("sms provider is required")
}
if c.AppKey == "" {
return errors.New("sms app_key is required")
}
return nil
}
// 添加到 Config 结构体
type Config struct {
// ...
SMS SMSConfig `toml:"sms"`
}步骤 2:实现 Client
go
// clients/sms/client.go
package sms
import (
"context"
"your-project/config"
)
// Client 短信客户端
type Client struct {
config *config.SMSConfig
}
func NewClient(cfg *config.SMSConfig) (*Client, error) {
if err := cfg.Validate(); err != nil {
return nil, err
}
return &Client{config: cfg}, nil
}
// Send 发送短信
func (c *Client) Send(ctx context.Context, phone, content string) error {
// 实现发送逻辑
return nil
}
func (c *Client) Close() error {
return nil
}步骤 3:添加到 App
go
// app/app.go
type App struct {
// ...
smsClient *sms.Client // 添加 SMS Client 字段
}
func (a *App) initClients() error {
// ...
// 初始化 SMS Client
smsClient, err := sms.NewClient(&a.config.SMS)
if err != nil {
return fmt.Errorf("failed to init sms client: %w", err)
}
a.smsClient = smsClient
return nil
}
func (a *App) Close(ctx context.Context) error {
// ...
if a.smsClient != nil {
a.smsClient.Close()
}
return nil
}步骤 4:注入到 Service
go
// services/user.go
type UserService struct {
db *gorm.DB
smsClient *sms.Client // 注入 SMS Client
}
func NewUserService(db *gorm.DB, smsClient *sms.Client) *UserService {
return &UserService{
db: db,
smsClient: smsClient,
}
}
func (s *UserService) SendVerificationCode(ctx context.Context, phone string) error {
code := generateCode()
// 调用 SMS Client
if err := s.smsClient.Send(ctx, phone, fmt.Sprintf("验证码:%s", code)); err != nil {
return err
}
// 保存验证码到数据库或缓存
return nil
}步骤 5:App 初始化 Service
go
func (a *App) initServices() {
a.userService = services.NewUserService(a.db, a.smsClient)
}Mock 实现
用于测试的 Mock
go
// clients/storage/mock.go
package storage
import "context"
// MockStorage Mock 存储实现(用于测试)
type MockStorage struct {
UploadFunc func(ctx context.Context, key string, data io.Reader) (*UploadResult, error)
DownloadFunc func(ctx context.Context, key string) (io.ReadCloser, error)
DeleteFunc func(ctx context.Context, key string) error
}
func (m *MockStorage) Upload(ctx context.Context, key string, data io.Reader) (*UploadResult, error) {
if m.UploadFunc != nil {
return m.UploadFunc(ctx, key, data)
}
return &UploadResult{Key: key}, nil
}
// 实现其他方法...测试中使用 Mock
go
// services/product_test.go
func TestProductService_UploadImage(t *testing.T) {
// 创建 Mock Storage
mockStorage := &storage.MockStorage{
UploadFunc: func(ctx context.Context, key string, data io.Reader) (*storage.UploadResult, error) {
return &storage.UploadResult{
Key: key,
URL: "http://example.com/" + key,
}, nil
},
}
// 创建 Service(注入 Mock)
service := services.NewProductService(db, mockStorage)
// 测试
err := service.UploadProductImage(ctx, 1, imageData)
assert.NoError(t, err)
}检查清单
- [ ] 对于可能更换的服务定义接口
- [ ] 实现多个服务提供商(如 S3、COS、OSS)
- [ ] 使用工厂函数根据配置选择实现
- [ ] 提供 Mock 实现用于测试
- [ ] 新增集成时遵循 5 步流程
- [ ] 通过配置切换服务提供商,无需修改代码
相关规范
- Client 层设计 - Client 实现规范
- 配置管理 - 配置结构设计
- 依赖注入规范 - Client 依赖管理