经过前面章节的学习,可以说大家已经算Redis开发入门了。已经可以去到项目上磨砺了。
但是今天我还想和大家分享一章:封装自己的Redis C#库,然后打包成Nuget包。
首先要说明的是:不是要自己开发一个Redis客户端库,而是基于上篇文章中介绍的6大库,做一个简单封装,真的很简单的那种,就是包个壳子。
再来说说为什么要做这个事情。
01、原因
1.可测试
我希望代码是以服务的方式注入到程序中,而不是静态方法的方式去调用。使用依赖注入来提供服务使得程序可测试性增强,如果做单元测试,依赖注入的服务很容易通过mock来测试,而静态方法往往很难被模拟,测试起来很不灵活;
2.解耦
对于一个系统来说会使用到各种技术来达到某种能力,实现一个功能可能会有很多种方法,我们在意的是实现这个功能,而不是你用了什么方法。回到主题,我们需要的是使用Redis来实现业务功能,而具体用那个客户端库并不重要。
再举个例子比如今天我们选择了ServiceStack.Redis库接过遇到问题我们解决不了怎么办?难道业务不做了?不可能吧!而恰巧这个时候CSRedisCore库可以解决,你会怎么选?
这时候可能会想换的成本有多大?两种库方法名不统一,功能也不一样,如果系统中到处散落Redis方法调用,这可怎么换啊。
试想如果我们封装了一层,提供了一组接口,打包成Nuget包,这个时候大家用的就是这个Nuget包,而我们只需要把Nuget包里用ServiceStack.Redis实现的方法换成用CSRedisCore实现一下,大家直接更新一下Nuget包,可能一行代码都不用改就完成了替换。
因为我们依赖的是我们自己封装的接口,而不是具体Redis客户端库,因此可以解耦轻松替换。
3.扩展
Redis的原生功能可以理解为基础功能,表明Redis有这种能力。但是我们怎么使用,怎么更好的发挥它的价值,这就是我们自己能力的体现了。
大家相过想过没有,为什么有的库商业化做的特别好,特别简单容易上手。商业化好说明提供的服务好,也就是说明它能帮你做很多事情,它在原生功能上加了很多自己的创作。
项目做多了,我们自己也会遇到一些相似的功能,如果这个时候我们想把这些相似功能封装一下,要放哪里呢?怎么给别人用?如果我们遇到一个功能现有库都没有相应能力,需要我们基于原生自己开发实现又应该在哪做呢?
这些功能积累的多了总要有个地方放吧,而这时如果我们自己封装了一层,放哪的问题是不是一下子就解决了,说不定做着做着就成了一个产品了呢。
不知道到这里,大家有没有感觉思路一下子被打开了。可能我们没写几行代码,但是这个格局一下子就打开了,而这几行代码也可能产生意想不到的收获,可能是无限可能,也可能就是你实现自己Redis客户端库的开端。
当然这也是我自己对编码,对封装的个人拙见。说这么多也是希望可以给大家一些帮助。
02、实现
下面闲话少说进入正题,如果来封装呢?
我们先梳理一下大致思路:
1.我们需要一个接口,里面包含:原库原生能力Client、其他我们自定义功能。
2.一个入口,别人要用,总要有个入口吧。
其实要求就这么简单。
下面我们就以封装CSRedisClient为例,首先定义IRedisService接口,里面包含Client字段以及两个演示的自定义方法
using CSRedis; namespace Redis.RedisExtension { public interface IRedisService { /// <summary> /// RedisClient /// </summary> /// <returns></returns> CSRedisClient Client { get; } #region 自定义方法 /// <summary> /// 获取指定 key 的值 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="key"></param> /// <returns></returns> T Get<T>(string key); /// <summary> /// 获取指定 key 的值 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="key"></param> /// <returns></returns> Task<T> GetAsync<T>(string key); #endregion } }
然后需要实现IRedisService,同时以CSRedisClient为构造函数入参,具体代码如下:
using CSRedis; namespace Redis.RedisExtension { public class RedisService : IRedisService { /// <summary> /// 构造函数 /// </summary> /// <param name="redisClient"></param> public RedisService(CSRedisClient redisClient) { Client = redisClient; } /// <summary> /// CSRedis /// </summary> /// <returns></returns> public CSRedisClient Client { get; } #region 自定义方法 /// <summary> /// 获取指定 key 的值 /// </summary> /// <returns></returns> public T Get<T>(string key) { return Client.Get<T>(key); } /// <summary> /// 获取指定 key 的值 /// </summary> /// <returns></returns> public Task<T> GetAsync<T>(string key) { return Client.GetAsync<T>(key); } #endregion } }
到这里第一个问题就解决了,对于第二个问题我们,我们可以对IServiceCollection进行扩展添加启动扩展方法AddRedisClientSetup,具体代码如下:
using CSRedis; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; namespace Redis.RedisExtension { public static class RedisSetupExtensions { /// <summary> /// Redis客户端启动项 /// </summary> /// <param name="services"></param> /// <returns></returns> public static IServiceCollection AddRedisClientSetup(this IServiceCollection services) { services.AddSingleton<CSRedisClient>(serviceProvider => { var configuration = serviceProvider.GetRequiredService<IConfiguration>(); var setting = configuration["RedisConnectionString"]; return new CSRedisClient(setting); }); services.AddSingleton<IRedisService, RedisService>(); return services; } /// <summary> /// Redis客户端启动项 /// </summary> /// <param name="services"></param> /// <param name="connectionString"></param> /// <returns></returns> public static IServiceCollection AddRedisClientSetup(this IServiceCollection services, string connectionString) { services.AddSingleton<CSRedisClient>(serviceProvider => { return new CSRedisClient(connectionString); }); services.AddSingleton<IRedisService, RedisService>(); return services; } } }
为什么要提供两个重载方法,因为如果用户基于我们的约定,在配置文件中以"RedisConnectionString"命名Redis连接字符串,用户直接调用AddRedisClientSetup()方法即可完成Redis启动,但是可能因为各种原因用户没法遵守约定,因此我们也要提供一个用户可以指定Redis连接字符串方法的入口。
下面我们就用基于约定的方式,在配置文件中加入以下配置:
{ "RedisConnectionString": "127.0.0.1:6379" }
然后使用Client.Set方法设置key1,再用自定义方法Get
public static void Run() { var configuration = new ConfigurationBuilder() .SetBasePath(AppContext.BaseDirectory) .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) .Build(); var services = new ServiceCollection(); services.AddSingleton<IConfiguration>(configuration); services.AddRedisClientSetup(); var redisService = services.BuildServiceProvider().GetService<IRedisService>(); var setResult = redisService.Client.Set("key1", "value1"); Console.WriteLine($"redisService.Client.Set("key1","value1")执行结果:{setResult}"); var value = redisService.Get<string>("key1"); Console.WriteLine($"redisService.Get<string>("key1")执行结果:{value}"); redisService.Client.Del("key1"); }
执行结果如下:
是不是很简单,然后我们只需要把上面三个文件IRedisService、RedisService、RedisSetupExtensions放到单独的类库中,然后打包发布成Nuget包,就可以给大家一起用啦,今天我这边就不发布Nuget包了,后面会有相关的计划,到时候再细聊。
注:测试方法代码以及示例源码都已经上传至代码库,有兴趣的可以看看。https://gitee.com/hugogoos/Planner