【开源】轻量架构() ——单例模式框架缓存设计的三次迭代从依赖注入到开箱即用

2026-07-24

作者:周方勇 / 咏方舟-长江支流(金质打印通、用宝框架开源作者)

用宝框架 · 拥抱第一 · 一次书写 · 三端复用

用宝框架是开源轻量级企业级分层架构基架,也是开放的、可扩展的架构体系。.NET Standard 2.0,零依赖,支持 MySQL / SQL Server / Oracle / SQLite并可扩展。SQL就是最好的跨平台语言,可跨语言无缝迁移至鸿蒙 ArkTS 及 Java 技术栈,接口名、类名、方法签名三端完全一致。

用宝架构开发的所有应用,可直接移植到 Java/ArkTS,无需重新设计架构、无需重新分层、无需重新抽象业务逻辑,只需按目标语言语法做形式上的转换。

本文侧重:将缓存类设计为实现缓存提供者接口,应用单例模式,避免使用都忘记DI注册,开箱即用。


在这里插入图片描述


一、引言

用宝框架作为一个轻量级、跨语言的开发框架,缓存能力是必不可少的。

但在设计缓存模块时,我经历了三次迭代,从“优雅的依赖注入”到“开箱即用的默认实现”,中间踩了不少坑。

今天把这三次设计的思路和演变过程记录下来,希望对大家有所启发。

二、第一次设计:接口作为成员(依赖注入)

代码示例

public interface ICacheProvider
{
    T Get<T>(string key);
    void Set<T>(string key, T value, TimeSpan? expiration = null);
    void Remove(string key);
    void Clear();
}

public class XmlEntityCacheManager
{
    private readonly ICacheProvider _cacheProvider;  // ← 接口作为成员变量

    public XmlEntityCacheManager(ICacheProvider cacheProvider)
    {
        _cacheProvider = cacheProvider;
    }

    public T GetOrLoad(string key, Func<string, T> loadFunc)
    {
        var cached = _cacheProvider.Get<T>(key);
        if (cached != null) return cached;

        var value = loadFunc(key);
        _cacheProvider.Set(key, value);
        return value;
    }
}

使用方式(必须依赖注入)

// Program.cs 中必须注册
builder.Services.AddSingleton<ICacheProvider, MemoryCacheProvider>();
builder.Services.AddScoped<XmlEntityCacheManager>();

这里出现了第一个问题

很多人不知道要注册 ICacheProvider,或者忘了注册。

我们来看看,如果你是一个普通的开发者,拿到用宝框架想快速跑起来,会怎么做?

你可能直接在控制器里写:

public class ProductController : ControllerBase
{
    private readonly XmlEntityCacheManager _cache;

    public ProductController(XmlEntityCacheManager cache)  // ← 你以为这样就行了
    {
        _cache = cache;
    }
}

运行后,你会看到这样的错误:

System.InvalidOperationException: Unable to resolve service for type 'ICacheProvider'

你一头雾水:“我明明注入了 XmlEntityCacheManager,为什么还报错?”

因为你不知道 XmlEntityCacheManager 还依赖 ICacheProvider,而你没有在 Program.cs 里注册它。

这就是第一个设计的核心问题:把“接口作为成员”,相当于强迫所有使用者都必须理解依赖注入的完整链路,否则程序跑不起来。

为了兼容“忘记注册”的情况,我们加了空缓存降级、加了 _fallbackCache,代码越来越复杂,使用者却越来越困惑。

三、第二次设计:单例 + 成员(伪简化)

为了解决依赖注入的问题,我们想了一个办法:提供一个静态默认实例。

public class XmlEntityCacheManager
{
    private readonly ICacheProvider _cacheProvider;

    private static XmlEntityCacheManager _default;
    public static XmlEntityCacheManager Default
    {
        get
        {
            if (_default == null)
            {
                _default = new XmlEntityCacheManager(new NullCacheProvider());  // ← 默认用空缓存
            }
            return _default;
        }
    }

    public XmlEntityCacheManager(ICacheProvider cacheProvider)
    {
        _cacheProvider = cacheProvider;
    }
}

问题依然存在

虽然提供了 Default 实例,但内部还是持有一个 ICacheProvider 成员变量。

它只是把“必须依赖注入”变成了“默认用空缓存”,但本质没有变。

使用者还是会遇到:

  • 默认用的是 NullCacheProvider,缓存其实不生效
  • 想换成真实的缓存,还是得去注册 ICacheProvider
  • 注册了之后,Default 实例还是空缓存,因为默认实例已经创建了

这个设计只解决了“不报错”的问题,没有解决“好用”的问题。

四、第三次设计:类实现接口 + 默认单例

核心转变

把“接口作为成员”改成“类实现接口”。

public class UserBaoCache<T> : ICacheProvider
{
    private readonly DataDictionary<string, T> _cache = new DataDictionary<string, T>();

    // ★★★ 默认单例,开箱即用 ★★★
    public static UserBaoCache<T> Default { get; } = new UserBaoCache<T>();

    // 实现 ICacheProvider 接口
    public T Get<T>(string key) { /* 从字典取 */ }
    public void Set<T>(string key, T value, TimeSpan? expiration = null) { /* 存入字典 */ }
    public void Remove(string key) { /* 从字典删除 */ }
    public void Clear() { /* 清空字典 */ }
}

使用方式(零配置)

// 不需要注册任何服务,直接使用
var cache = UserBaoCache<XmlMapEntity>.Default;
var template = cache.GetOrLoad("products", key => LoadFromFile(key));

这就是最终设计:缓存本身就是 ICacheProvider,默认实例就是内存缓存,开箱即用。

如果用户想用 Redis 或其他外部缓存怎么办?

// 用户可以自己实现 ICacheProvider
public class RedisCacheProvider : ICacheProvider { ... }

// 然后注入到自己的服务中
builder.Services.AddSingleton<ICacheProvider, RedisCacheProvider>();

但这是用户的主动选择,不是框架的强制要求。

五、三次设计对比

维度设计一(成员+DI)设计二(单例+成员)设计三(实现接口+默认实例)
是否必须依赖注入✅ 是❌ 否❌ 否
默认可用❌ 否⚠️ 可用但缓存不生效✅ 是,直接生效
用户代码量多(需注册)中(可用默认)少(直接用)
代码复杂度高中低
缓存能力外部决定外部决定自带内存缓存

六、总结

接口作为成员,是“依赖别人”的思维。

类实现接口,是“自己成为能力”的思维。

设计一个框架的能力时,不要一开始就想着“用户会依赖注入”,要先想着“用户怎么开箱即用”。

用宝框架 UserBaoCache<T> 的原则:

  • 默认是内存缓存,零配置可用
  • 实现 ICacheProvider 接口,保持扩展性
  • 提供 Default 静态实例,全局共享
  • 用户想换缓存,自己实现接口替换即可

简单、直接、少一层判断,往往是最正确的设计。


七、代码

using System;
using UserBaoTech.Foundation.Data;

namespace UserBaoTech.Foundation.Infrastructure
{
    /// <summary>
    /// 作者:长江支流 2026-7-23
    /// 用宝框架默认缓存实现 —— 轻量级内存缓存,开箱即用
    /// </summary>
    /// <typeparam name="T">缓存值的类型</typeparam>
    /// <remarks>
    /// <para><b>设计目标:</b></para>
    /// <list type="bullet">
    ///   <item><description>实现 <see cref="ICacheProvider"/> 接口,保持与框架缓存抽象一致</description></item>
    ///   <item><description>提供 <see cref="Default"/> 静态单例,开箱即用,无需依赖注入</description></item>
    ///   <item><description>内部使用 <see cref="DataDictionary{TKey, TValue}"/> 存储,轻量高效</description></item>
    ///   <item><description>支持 Key 前缀隔离,不同模块可独立使用不同实例</description></item>
    /// </list>
    /// <para><b>使用示例:</b></para>
    /// <code>
    /// // 直接使用默认单例
    /// var cache = UserBaoCache&lt;XmlMapEntity&gt;.Default;
    /// var entity = cache.GetOrLoad("products", key =&gt; LoadFromFile(key));
    /// 
    /// // 或创建独立实例(带 Key 前缀)
    /// var reportCache = new UserBaoCache&lt;WebMisControllerCore&gt;("Report_");
    /// var controller = reportCache.GetOrLoad("products", key =&gt; ParseXml(key));
    /// </code>
    /// </remarks>
    public class UserBaoCache<T> : ICacheProvider
    {
        private static UserBaoCache<T> _default;
        private static readonly object _lock = new object();

        /// <summary>
        /// 默认单例实例 —— 全局共享,整个应用程序生命周期内只有一个实例
        /// </summary>
        /// <remarks>
        /// 适用场景:
        ///   - 应用级缓存(如 XML 配置、实体模板等)
        ///   - 所有用户共享的缓存数据
        ///   - 不需要依赖注入的简单场景
        /// </remarks>
        public static UserBaoCache<T> Default
        {
            get
            {
                if (_default == null)
                {
                    lock (_lock)
                    {
                        if (_default == null)
                        {
                            _default = new UserBaoCache<T>();
                        }
                    }
                }
                return _default;
            }
        }

        /// <summary>
        /// 内部缓存存储(内存字典)
        /// </summary>
        private readonly IDataDictionary<string, T> _cache;
        private readonly string _keyPrefix;

        /// <summary>
        /// 无参构造函数(默认无 Key 前缀)
        /// </summary>
        public UserBaoCache() : this("")
        {
        }

        /// <summary>
        /// 构造函数(指定 Key 前缀)
        /// </summary>
        /// <param name="keyPrefix">Key 前缀,用于隔离不同模块的缓存</param>
        public UserBaoCache(string keyPrefix)
        {
            _cache = new DataDictionary<string, T>();
            _keyPrefix = keyPrefix ?? "";
        }

        /// <summary>
        /// 获取或加载缓存项(便捷方法)
        /// </summary>
        /// <param name="key">缓存键</param>
        /// <param name="loadFunc">缓存未命中时的加载函数</param>
        /// <param name="expiration">过期时间(可选,默认永不过期)</param>
        /// <returns>缓存值</returns>
        public T GetOrLoad(string key, Func<string, T> loadFunc, TimeSpan? expiration = null)
        {
            if (string.IsNullOrEmpty(key))
            {
                throw new ArgumentException("缓存键不能为空", nameof(key));
            }

            var cacheKey = _keyPrefix + key;

            if (_cache.ContainsKey(cacheKey))
            {
                return _cache[cacheKey];
            }

            if (loadFunc == null)
            {
                return default(T);
            }

            var value = loadFunc(key);
            if (value != null)
            {
                _cache[cacheKey] = value;
            }
            return value;
        }

        #region ICacheProvider 接口实现

        /// <summary>
        /// 从缓存中获取指定键的值
        /// </summary>
        public TValue Get<TValue>(string key)
        {
            if (string.IsNullOrEmpty(key))
                return default(TValue);

            var cacheKey = _keyPrefix + key;
            if (_cache.ContainsKey(cacheKey))
            {
                var value = _cache[cacheKey];
                // 尝试将 T 转换为 TValue
                try
                {
                    return (TValue)(object)value;
                }
                catch (InvalidCastException)
                {
                    // 类型不匹配,返回默认值
                    return default(TValue);
                }
            }
            return default(TValue);
        }

        /// <summary>
        /// 将值存入缓存
        /// </summary>
        public void Set<TValue>(string key, TValue value, TimeSpan? expiration = null)
        {
            if (string.IsNullOrEmpty(key))
                return;

            // 只允许存储 T 类型的值(或可转换为 T 的值)
            if (value is T typedValue)
            {
                var cacheKey = _keyPrefix + key;
                _cache[cacheKey] = typedValue;
            }
            else
            {
                // 尝试转换
                try
                {
                    var converted = (T)(object)value;
                    var cacheKey = _keyPrefix + key;
                    _cache[cacheKey] = converted;
                }
                catch (InvalidCastException)
                {
                    // 类型不匹配,忽略存储
                }
            }
        }

        /// <summary>
        /// 从缓存中移除指定键
        /// </summary>
        public void Remove(string key)
        {
            if (string.IsNullOrEmpty(key))
                return;

            var cacheKey = _keyPrefix + key;
            if (_cache.ContainsKey(cacheKey))
            {
                _cache.Remove(cacheKey);
            }
        }

        /// <summary>
        /// 检查缓存中是否存在指定键
        /// </summary>
        public bool Exists(string key)
        {
            if (string.IsNullOrEmpty(key))
                return false;

            var cacheKey = _keyPrefix + key;
            return _cache.ContainsKey(cacheKey);
        }

        /// <summary>
        /// 清空所有缓存
        /// </summary>
        public void Clear()
        {
            _cache.Clear();
        }

        #endregion
    }
}

欢迎拷贝、转载,无需授权
反馈与交流:欢迎在评论区留言或私信交流。如果你正在寻找一套 不用 EF、不用 Dapper、可跨语言迁移 的轻量级基架,用宝框架或许是一个值得尝试的选择。


用宝框架,拥抱第一 · 一次书写 · 三端复用——不仅是用户的朋友,而且是用户的宝贝
用宝框架开源地址:GitHub / Gitee 搜索「用宝框架」或「XmlORM」官网
系列博文:跨语言·跨平台·跨数据库 —— 用宝框架系列
csdn首发:https://blog.csdn.net/flygoldfish

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐