从【项目思维】→【产品思维】:代码封装

从【项目思维】→【产品思维】:代码封装(结合你工控 / MES 采集、MinIO/SeaweedFS 上传场景举例)

一句话区分: ✅ 项目思维:为当前这一条产线、这一个需求写代码。能用就行,一次性交付,硬编码,业务逻辑和存储、接口混在一起。改一个地方到处动,换存储(MinIO 切 SeaweedFS)要大面积改代码。 ✅ 产品思维:代码是可复用组件,面向多客户、多产线、可配置、可扩展。把变化的部分和不变的部分隔离,对外提供稳定接口,内部随便换实现,调用方无感知。

产品化:代码版本控制,国际化(界面,提示,日志,手册多国语言),可配置,可扩展,只存win7(补丁),win10,ubuntu,x86,x64,arm芯片,浏览器兼容,找个商业软件参考

一、项目思维写法(反面例子,你大概率见过)

特点:耦合很重,直接在业务代码里写 MinIO 上传逻辑。

// 项目思维:业务代码直接写MinIO,写死bucket、路径
public async Task UploadPic(string imgName, string localPath)
{
    var minioClient = new MinioClient("http://127.0.0.1:9000","ak","sk");
    var args = new PutObjectArgs()
        .WithBucket("factory-img")
        .WithObject($"images/{imgName}")
        .WithFileName(localPath);
    await minioClient.PutObjectAsync(args);
}

问题

  1. 业务层强依赖 MinIO SDK;哪天换成 SeaweedFS,所有调用这个方法的地方都要改。
  2. Bucket、地址硬编码;不同产线不同桶,只能复制改代码。
  3. 没有统一返回结构、统一异常、日志;上传失败处理写死。
  4. 只能上传图片,后续加文件类型、加本地归档、加文件校验,代码越堆越乱。

二、产品思维封装思路(分层、依赖倒置、接口隔离)

核心原则:调用方只关心【要上传文件】,不关心底层是 MinIO / SeaweedFS / 本地磁盘

1. 先定义稳定抽象(接口,不变层,对外契约)

调用方永远只依赖这个接口,底层存储随便替换,上层代码一行不改。

/// <summary>
/// 文件存储统一接口【产品抽象,永不轻易改动】
/// </summary>
public interface IFileStorage
{
    /// <summary>
    /// 统一上传接口,对外只暴露业务名称+本地路径
    /// </summary>
    /// <param name="bizFileName">业务图片名,如line1_cam01.jpg</param>
    /// <param name="localFilePath">本地文件全路径</param>
    Task<FileUploadResult> UploadAsync(string bizFileName, string localFilePath);

    Task<byte[]> DownloadAsync(string fileKey);
    Task<bool> DeleteAsync(string fileKey);
}

/// <summary>
/// 统一返回实体,所有存储后端返回结构一致
/// </summary>
public class FileUploadResult
{
    public string StorageType { get; set; } // MinIO / SeaweedFS
    public string FileKey { get; set; }    // 唯一key:minio对象名 / seaweed fileId
    public string Url { get; set; }
    public bool Success { get; set; }
    public string Msg { get; set; }
}

2. 分别实现不同存储的具体实现(可变层)

每个存储单独一个实现类,各自的 SDK、路径规则放在内部

// MinIO实现
public class MinioFileStorage : IFileStorage
{
    private readonly MinioConfig _config;
    public MinioFileStorage(MinioConfig config)
    {
        _config = config; // 配置从配置文件读取,不硬编码
    }
    public async Task<FileUploadResult> UploadAsync(string bizFileName, string localFilePath)
    {
        // 内部拼接bucket、虚拟目录,调用方完全看不到
        string objName = $"images/{bizFileName}";
        // minio上传逻辑...
        return new FileUploadResult
        {
            StorageType = "MinIO",
            FileKey = objName,
            Success = true
        };
    }
}

// SeaweedFS Filer实现
public class SeaweedFileStorage : IFileStorage
{
    private readonly SeaweedConfig _config;
    public SeaweedFileStorage(SeaweedConfig config)
    {
        _config = config;
    }
    public async Task<FileUploadResult> UploadAsync(string bizFileName, string localFilePath)
    {
        // http上传到Seaweed,拿到fileId
        // 内部处理,调用方不用关心fileId机制
        return new FileUploadResult
        {
            StorageType = "SeaweedFS",
            FileKey = fileId,
            Success = true
        };
    }
}

3. 工厂模式,根据配置自动选择存储(产品核心)

项目思维:写 if else 到处 new 客户端 产品思维:工厂统一创建实例,读配置切换存储,业务代码完全不用改

public class FileStorageFactory
{
    public static IFileStorage GetStorage(StorageSetting setting)
    {
        return setting.StorageType switch
        {
            "MinIO" => new MinioFileStorage(setting.Minio),
            "SeaweedFS" => new SeaweedFileStorage(setting.Seaweed),
            _ => throw new NotSupportedException()
        };
    }
}

4. 业务层调用(最关键:业务代码极度干净)

业务层只依赖 IFileStorage 抽象,完全不知道底层是哪个存储

// MES产线业务代码,永远不变
var storage = FileStorageFactory.GetStorage(config);
var res = await storage.UploadAsync("line1_cam_20260928.jpg", @"D:\data\img.jpg");
if(res.Success)
{
    //数据库保存res.FileKey
}

三、项目思维 vs 产品思维 封装对比(核心差异)

表格

维度项目思维产品思维
依赖业务直接依赖 SDK,强耦合业务依赖抽象接口,底层实现解耦
配置硬编码,改代码才能换地址 /bucket全部抽离配置文件,不用改代码,重启生效
扩展新增存储,业务代码全改新增存储只新增一个实现类,上层不动(开闭原则)
异常 / 日志各处零散 try-catch统一在封装层处理日志、重试、异常,业务干净
交付目标满足当前产线交付一套组件,卖给多个客户,客户可自选存储方案
测试只能整体联调可以 Mock IFileStorage,单元测试,不用启动 MinIO

四、产品思维额外工程能力(工业软件必备,项目一般不做)

  1. 重试机制:网络抖动上传失败,封装层内置重试,业务无感知(产线采集网络不稳非常关键)
  2. 文件校验:文件存在性、大小校验、后缀过滤,放在封装内部
  3. 兜底策略:对象存储故障,可自动降级本地磁盘缓存,后续异步续传(MES 抓拍很实用)
  4. 统一监控埋点:上传耗时、失败计数,埋点写在封装层,业务不用加埋点代码
  5. 版本兼容:接口契约固定,后续升级存储 SDK,不破坏上层业务调用

五、落地精简口诀(方便记)

  1. 抽接口:稳定契约,对外不动
  2. 分实现:每个存储单独写,内部细节隐藏
  3. 配置化:地址、桶名、超时全部放配置,禁止硬编码
  4. 工厂选择:一行配置切换存储,业务代码零改动
  5. 横切封装:日志、重试、异常统一放在底层,业务只写业务

六、你下一步可以直接落地

  1. 先定义IFileStorage接口 + FileUploadResult返回类(这就是你的组件契约)
  2. 先实现 MinIO 版本跑通业务
  3. 后续需要 SeaweedFS 时,新增SeaweedFileStorage,上层 MES 业务代码一行不用改

要不要我把这套完整代码加上重试、日志、文件校验,做成可以直接放进你的采集项目里的完整组件?

www.erwa.cn 二娃备忘

阅读剩余
THE END