Skip to content

集成服务可扩展性设计

概述

可扩展性设计规范定义了如何设计易于扩展和替换的集成服务。核心思想是面向接口编程,使用工厂模式选择具体实现。

接口抽象

为什么需要接口

对于可能更换服务提供商的集成,定义抽象接口

  • 易于替换:更换服务提供商只需实现同一接口
  • 易于测试:可以使用 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 步流程
  • [ ] 通过配置切换服务提供商,无需修改代码

相关规范