多场景抢红包业务引发.NETCore下使用适配器模式实现业务接口分离
事情的起因
我们公司现有一块业务叫做抢红包,最初的想法只是实现了一个初代版本,就是给指定的好友单发红包,随着业务的发展,发红包和抢红包的场景也越来越多,目前主要应用的场景有:单聊发红包、群聊发红包、名片发红包、直播场景中的主播发红包/观众给主播发红包/定时抢红包,接下来,如果出现其它产品的业务,也将大概率的出现抢红包的需求。
大同小异的抢红包业务
红包的场景无论怎么变化,其核心算法不变,这部分是可以抽象的内容,随着迭代发展,我们之前通常都是通过增加红包的类型(业务)来扩展,但是随着肉眼可见的发展,部分业务的改动如果需要对红包业务进行调整和优化对话,将有可能产生牵一发而动全身的debuff效果。
新的改变
其实这些业务代码早该优化一下,我就是懒+忙(借口),正好有位新同事入职,这块的优化任务就交给他来做了,从头到尾我都没有参与(不知道有没有吐槽我的代码,捂脸~),我初步看了一下,代码的实现质量还是挺高的,正好也是一个比较好的应用场景,我就简单实现一下他做的适配器模式,彻底的将各个红包业务类型分离,很好的实现了设计模式的开闭原则,加入某天某个场景的抢红包业务下线了,这种做法是非常有利于业务的扩展和维护。
定义抢红包接口
public interface iredpacket { string name { get; } string put(int org_id, int money, int count, string reason); string get(int id); }
以上接口包含一个属性和2个方法,用于设置业务名称和收发红包。初次之外,我们还需要定义一个实现业务的基类,用于处理公共业务。
红包基类业务实现
public abstract class redpacket : iredpacket { public abstract string name { get; } public abstract string put(int org_id, int money, int count, string reason); public abstract string get(int id); protected string create(string reason, int money, int count) { console.writeline("创建了红包:{0},金额:money:{1},数量:{2}", reason, money, count); return "成功"; } protected string fighting() { console.writeline("调用了抢红包方法:{0}", nameof(fighting)); return "成功"; } }
在基类中,我们选择不实现接口,将接口方法定义为抽象类型。同时,定义并实现两个受保护的方法 create(创建红包)/fighting(抢红包),接口方法由子类实现具体的业务细节,当子类针对具体的业务细节实现完成后,他们应该会调用create(创建红包)/fighting(抢红包)的方法,直至最终完成整个红包的流程。
实现单聊红包
public class chatoneredpacket : redpacket { public override string name { get; } = "chatone"; public override string put(int org_id, int money, int count, string reason) { console.writeline("检查接收人id:{0}是否存在", org_id); return base.create(reason, money, count); } public override string get(int id) { console.writeline("检查红包id:{0},是否具有领取资格", id); return base.fighting(); } }
群聊红包
public class chatgroupredpacket : redpacket { public override string name { get; } = "chatgroup"; public override string put(int org_id, int money, int count, string reason) { console.writeline("检查群id:{0},是否存在", org_id); return base.create(reason, money, count); } public override string get(int id) { console.writeline("检查是否群id:{0},当前用户是否群成员", id); return base.fighting(); } }
直播红包
public class liveredpacket : redpacket { public override string name { get; } = "live"; public override string put(int org_id, int money, int count, string reason) { console.writeline("检查直播id:{0}是否存在", org_id); return base.create(reason, money, count); } public override string get(int id) { console.writeline("检查红包id:{0} 是否当前主播红包", id); return base.fighting(); } }
为了方便演示,上面的三种红包子类仅简单的实现类属性 name="chatone",除此之外,还实现类接口的收发红包接口,子类实现 name 属性主要是便于我们在di中去灵活的区分调用的主体,实现业务的分离。除了单聊红包外,我们还有群聊和直播红包,都采用上面的处理方式,只是各自实现的 name 属性时,指定不同的名字即可。在接口实现的方法中,各自的业务还需要执行不同的业务检查,比如单聊红包就需要检查接收人是否存在,群聊红包还需要检查群是否存在,该群是否被冻结等等,直播红包需要检查主播是否在直播中,观众是否在直播房间内,这些都是不同业务场景产生的特殊的业务处理需求。
创建容器实例
public void configureservices(iservicecollection services) { services.addscoped(typeof(iredpacket), typeof(chatoneredpacket)) .addscoped(typeof(iredpacket), typeof(chatgroupredpacket)) .addscoped(typeof(iredpacket), typeof(liveredpacket)); ... }
容器实例的创建非常简单,只需要将已实现 iredpacket 接口的子类注册到服务管道即可。
依赖注入,以实例集的方式
[route("api/[controller]")] [apicontroller] public class homecontroller : controllerbase { private readonly ienumerable<iredpacket> redpackets; public homecontroller(ienumerable<iredpacket> redpackets) { this.redpackets = redpackets; } }
通过建立一个控制台 homecontroller 用于演示,在 homecontroller 的构造方法中,使用 ienumerable
为了演示方便,我们构造4中不同的业务实体去调用发红包的接口,分别将结果输出到客户端 输出结果为: 红包业务类型:quanzi不存在 抢红包的过程,传入一个红包id,然后跟进该id到数据库进行查找,得到红包后,根据红包类型找出 iredpacket 的实现类,并进行调用,完成抢红包的操作。可能有的同学会觉得比较奇怪,为什么不直接拆红包呢?这是因为我们要根据红包设计的初衷,不同的红包,其所执行的业务规范性检查是不同的,不能直接进行暴力拆包。 上面我们创建了3个iredpacket的实现类,并将他们注册到服务管道中,然后在homecontroller中获得服务依赖注入的实例对象,通过在不同的参数传入,实现了不同的红包业务场景的拆分,很好的实现了设计模式中所说的开闭原则。 https://github.com/lianggx/examples/tree/master/ron.redpackettest发红包
[httppost]
public actionresult<string> post([frombody] redpacketviewmodel model)
{
var rp = this.redpackets.where(f => f.name == model.type).firstordefault();
if (rp == null)
{
var msg = $"红包业务类型:{model.type}不存在";
console.writeline(msg);
return msg;
}
var result = rp.put(model.org_id, model.money, model.count, model.reason);
return result;
}
// 单聊红包
{
"type":"chatone",
"org_id":1,
"money":8,
"count":1,
"reason":"恭喜发财,大吉大利!"
}
// 群聊红包
{
"type":"chatgroup",
"org_id":2,
"money":9,
"count":3,
"reason":"恭喜发财,大吉大利!"
}
// 直播红包
{
"type":"live",
"org_id":3,
"money":8,
"count":1,
"reason":"恭喜发财,大吉大利!"
}
//圈子红包
{
"type":"quanzi",
"org_id":4,
"money":8,
"count":1,
"reason":"恭喜发财,大吉大利!"
}
// 单聊红包
检查接收人id:1是否存在
红包类型:chatone,创建了红包:恭喜发财,大吉大利!,金额:money:8,数量:1
// 群聊红包
检查群id:2,是否存在
红包类型:chatgroup,创建了红包:恭喜发财,大吉大利!,金额:money:9,数量:3
// 直播红包
检查直播id:3是否存在
红包类型:live,创建了红包:恭喜发财,大吉大利!,金额:money:8,数量:1
//圈子红包
抢红包
[httpget("{id}")]
public actionresult<string> get(int id)
{
// 生产环境下,该红包消息应该是从数据库中读取
var model = getredpacket(id);
var rp = this.redpackets.where(f => f.name == model.type).firstordefault();
var result = rp.get(id);
return result;
}
private redpacketviewmodel getredpacket(int id)
{
int type = --id;
string[] redpackets = { "chatone", "chatgroup", "live" };
var model = new redpacketviewmodel
{
count = 3,
money = 8,
org_id = 115,
reason = "恭喜发财,大吉大利!",
type = redpackets[type]
};
return model;
}
结束语
演示代码下载