2020-03-23
美团设计模式在外卖营销业务中的实践-学习笔记(一)
[TOC]
看了美团技术团队的 设计模式在外卖营销业务中的实践 一文后的一些记录,emmm为了证明我看过,哈哈,还有最后一个责任链模式暂时还不知道怎么弄。。。 感兴趣的可以点这里去看看原文,干货满满。第一次写学习笔记,不知道写的好不好,欢迎各位大佬评论交流,大家一起学习啊。
一、设计模式原则
面向对象的设计模式有七大基本原则:
- 开闭原则 (Open Closed Principle, OCP)
- 单一职责原则(Single Responseibility Principle, SRP)
- 里氏代换原则(Liskov Substitution Principle, LSP)
- 依赖倒转原则(Dependency Inversion Principle, DIP)
- 接口隔离原则(Interface Segregation Principle, ISP)
- 合成/聚合复用原则(Composite/Aggregate Reuse Principle, CARP)
- 最少知识原则(Least Knowledge Principle, LKP)或者迪米特法则(Law of Demeter, LOD)
简单理解就是:开闭原则是总纲,它指导我们要对扩展开放,对修改关闭;单一职责原则则指导我们实现类要职责单一;里氏替换原则知道我们不要破坏继承体系;依赖倒置原则指导我们要面向接口编程;接口隔离原则指导我们在设计接口的时候要精简单一;迪米特法则则指导我们要降低耦合。
二、设计模式在美团外卖营销业务中的具体案例
2.1、工厂模式和策略模式
学习设计模式或者是在工程中实践设计模式,必须深入到某一个特定的业务场景中去,再结合对业务场景的理解和领域模型的建立,才能体会到设计模式思想的精髓。在这里美团结合了其“邀请下单”业务来进行设计模式实践与分享。
2.1.1 业务简介
“邀请下单”是美团外面用户邀请其他用户下单后给予奖励的平台。即用户A邀请用户B,并且用户B在美团下单后给予用户A一定的现金奖励。同时为了协调成本与收益的关系,返奖会有多个计算策略。邀请下单后台主要涉及两个技术要点:
- 返奖金额计算,涉及到不同的计算原则。
- 从邀请开始到返奖结束的整个流程。
2.1.2 返奖规则与设计模式
业务建模
如图是美团邀请下单业务返奖规则计算的业务逻辑视图:
<img src="https://tva1.sinaimg.cn/large/00831rSTgy1gd3v2l36x6j30u00elwgc.jpg" style="zoom: 67%;" />
从图中可以看到返奖金额的计算规则。首先需要判断用户状态是否符合返奖规则,符合则继续判断用户是老用户还是新用户,从而给予不同的奖励方案。
在计算完用户返奖金额后还需要更新用户的奖金信息及通知结算服务对用户的金额进行结算。这两个模块对所有的奖励来说否是一样的。
可以看到,对用户的整体返奖流程是不变的,变化的只有对反奖金额的计算流程即返奖规则。此处我们可以参考开闭原则,对于返奖流程保持封闭,对于可能扩展的返奖规则进行开放。将放奖规则抽象为返奖策略,即针对不同的用户类型的不同反奖方案我们视为不同的返奖策略,不同的返奖策略会产生不同的返奖金额结果。
其中返奖策略最终产生的是一个值对象,我们通过工厂的方式生产针对不同的用户的奖励策略。主要涉及的设计模式为工厂模式和策略模式。
模式:工厂模式
模式定义:
定义一个用于创建对象接口,让子类决定实例化哪一个类。工厂方法是一个类的实例化延迟到子类。
工厂模式通用类如下:
<img src="https://tva1.sinaimg.cn/large/00831rSTgy1gd3wcu8y3dj30oo0b4mxm.jpg" style="zoom:50%;" />
代码实例如下:
// 抽象的产品
public abstract class Product {
public abstract void method();
}
// 产品A
public class ProductA extends Product {
@Override
public void method() {
System.out.println("This is ProductA");
}
}
// 产品B
public class ProductB extends Product {
@Override
public void method() {
System.out.println("This is Product B");
}
}
// 创建一个抽象的工厂
public abstract class Factory<T> {
public abstract Product createProduct(Class<T> c) throws Exception;
}
// 具体分工厂实现 (注意 这里知识命名为FactoryA,不是ProductA的专属工厂)
public class FactoryA extends Factory {
@Override
public Product createProduct(Class c) throws Exception {
// 利用反射动态创建实例
return (Product) Class.forName(c.getName()).newInstance();
}
}
// 具体使用
public static void main(String[] args) throws Exception {
//创建工厂
FactoryA factoryA = new FactoryA();
// 使用工厂创建具体的产品
Product product = factoryA.createProduct(ProductA.class);
product.method();
Product product1 = factoryA.createProduct(ProductB.class);
product1.method();
}
// 输出结果
Connected to the target VM, address: '127.0.0.1:65319', transport: 'socket'
This is ProductA
This is Product B
Disconnected from the target VM, address: '127.0.0.1:65319', transport: 'socket'
Process finished with exit code 0
模式:策略模式
模式定义:定义一系列算法,将每个算法都封装起来,并且他们可以互换。策略模式是一种对象行为模式。
策略模式通用类图如下:
<img src="https://tva1.sinaimg.cn/large/00831rSTgy1gd3zy1h1zgj30no086jrr.jpg" style="zoom:50%;" />
代码示例:
// 定义一个策略接口
public interface Strategy {
void strategyImplementation();
}
// 具体的策略实现
public class StrategyA implements Strategy {
@Override
public void strategyImplementation() {
System.out.println("正在执行策略A");
}
}
public class StrategyB implements Strategy {
@Override
public void strategyImplementation() {
System.out.println("正在执行策略B");
}
}
// 策略封装 屏蔽高层模块对策略、算法的直接访问 使用context同一操作
public class Context {
private Strategy strategy = null;
public Context(Strategy strategy){
this.strategy = strategy;
}
public void doStrategy(){
strategy.strategyImplementation();
}
}
// 具体使用
public static void main(String[] args) throws Exception {
StrategyA strategy = new StrategyA();
Context contextA = new Context(strategy);
contextA.doStrategy();
StrategyB strategyB = new StrategyB();
Context contextB = new Context(strategyB);
contextB.doStrategy();
}
// 输出结果
Connected to the target VM, address: '127.0.0.1:65488', transport: 'socket'
正在执行策略A
正在执行策略B
Disconnected from the target VM, address: '127.0.0.1:65488', transport: 'socket'
Process finished with exit code 0
工程实践:
通过上文介绍的返奖业务模型,我们可以看到返奖的主流程就是选择不同的返奖策略的过程,每个返奖策略都包含返奖金额计算、更新用户奖金信息及结算这三个步骤。我们可以使用工厂模式生产出不同的策略,同时使用策略模式来进行不同的策略执行。示例代码如下:
// 抽象策略
public abstract class RewardStrategy {
// 生成的返奖金额 不同策略实现不同 定义成抽象的 由子类自己实现
public abstract int reward(long userId);
// 更新账户及结算信息 每个用户和返奖规则最后都要执行 统一实现
public void insertRewardAndSettlement(long userId, int reward){
System.out.println("更新用户信息以及结算成功:userId => " + userId + ",reward => " + reward);
}
}
// 新用户返奖策略 (这里使用随机数模拟返奖金额 😁)
public class NewUserRewardStrategy extends RewardStrategy {
@Override
public int reward(long userId) {
System.out.println("新用户反奖策略,用户 => " + userId);
return (int)(Math.random() * 10);
}
}
// 老用户返奖策略 (这里也使用随机数模拟返奖金额 😁)
public class OldUserRewardStrategy extends RewardStrategy {
@Override
public int reward(long userId) {
System.out.println("老用户反奖策略,用户 => " + userId);
return (int)(Math.random() * 10);
}
}
// 抽象工厂
public abstract class StrategyFactory<T> {
abstract RewardStrategy createStrategy(Class<T> c);
}
// 具体的工厂 (根据具体的类生成不同的策略)
public class FactorStrategyFactory extends StrategyFactory {
@Override
public RewardStrategy createStrategy(Class c) {
RewardStrategy strategy = null;
try{
strategy = (RewardStrategy) Class.forName(c.getName()).newInstance();
}catch (Exception e){
e.printStackTrace();
}
return strategy;
}
}
// 使用策略模式来执行具体的策略
public class RewardContext {
private RewardStrategy strategy;
// 构造方法 传入具体到的策略
public RewardContext(RewardStrategy strategy){
this.strategy = strategy;
}
public void doStrategy(long userId){
int reward = strategy.reward(userId);
strategy.insertRewardAndSettlement(userId, reward);
}
}
// 具体使用 使用时没有直接对策略、算法的直接访问 而是通过context进行操作
// 这里使用随机数的大小来判断 使用 老用户策略还是新用户策略 😄
public static void main(String[] args) {
FactorStrategyFactory factory = new FactorStrategyFactory();
RewardContext context;
RewardStrategy strategy;
double i = Math.random();
System.out.println(i);
if(i > 0.4){
strategy = factory.createStrategy(OldUserRewardStrategy.class);
context = new RewardContext(strategy);
context.doStrategy(123456);
}else {
strategy = factory.createStrategy(NewUserRewardStrategy.class);
context = new RewardContext(strategy);
context.doStrategy(456789);
}
}
// 执行结果
Connected to the target VM, address: '127.0.0.1:49241', transport: 'socket'
0.4496623743530703 // 这个是生成的随机数
老用户反奖策略,用户 => 123456
更新用户信息以及结算成功:userId => 123456,reward => 3
Disconnected from the target VM, address: '127.0.0.1:49241', transport: 'socket'
Process finished with exit code 0
工厂方法模式帮助我们直接产生一个具体的策略对象,策略模式帮助我们保证这些策略对象可以自由的切换而不需要改动其他逻辑,从而达到解耦的目的。通过这两个模式组合,当我们系统需要增加一种返奖策略时,只需要实现RewardStrategy接口即可,无需考虑其他的改动。当我们需要改变策略时,只需要修改策略的类名即可。不仅增强了系统的可扩展性,避免了大量的条件判断,而且从真正意义上达到了高内聚、低耦合的目的。
2.1.3 返奖流程与设计模式实践
业务建模
当受邀人在接受邀请人的邀请并且下单后,返奖后台接收到受邀人的下单记录,此时邀请人也进入返奖流程。首先我们订阅用户订单消息并对订单进行返奖规则校验。例如,是否使用红包下单,是否在红包有效期内下单,订单是否满足一定的优惠金额等等条件。当满足这些条件以后,我们将订单信息放入延迟队列中进行后续处理。经过T+N天之后处理该延迟消息,判断用户是否对该订单进行了退款,如果未退款,对用户进行返奖。若返奖失败,后台还有返奖补偿流程,再次进行返奖。其流程如下图所示:
<img src="https://tva1.sinaimg.cn/large/00831rSTgy1gd3yfickakj30u009lt9x.jpg" style="zoom: 80%;" />
我们对上述业务流程进行领域建模:
- 在接收到订单消息后,用户进入待校验状态;
- 在校验后,若校验通过,用户进入预返奖状态,并放入延迟队列。若校验未通过,用户进入不返奖状态,结束流程;
- T+N天后,处理延迟消息,若用户未退款,进入待返奖状态。若用户退款,进入失败状态,结束流程;
- 执行返奖,若返奖成功,进入完成状态,结束流程。若返奖不成功,进入待补偿状态;
- 待补偿状态的用户会由任务定期触发补偿机制,直至返奖成功,进入完成状态,保障流程结束。
<img src="https://tva1.sinaimg.cn/large/00831rSTgy1gd3ygq8et6j30u00abq3r.jpg" style="zoom:80%;" />
通过建模将返奖流程的多个步骤映射位系统的状态。在邀请下单系统中,我们的主要流程是返奖。对于返奖,每一个状态要进行的动作和操作都是不同的。因此,使用状态模式,能够帮助我们对系统状态以及状态间的流转进行统一的管理和扩展。
模式:状态模式
模式定义:当一个对象内在改变状态时允许其改变行为,这个对象看起来想改变了其类。(看完一脸懵逼。。。)
状态模式的通用类图如下图所示:
<img src="https://tva1.sinaimg.cn/large/00831rSTgy1gd3ykw0vkhj30nc0b4aak.jpg" style="zoom: 50%;" />
对比策略模式的类型会发现和状态模式的类图很类似,但实际上有很大的区别,具体体现在concrete class上。策略模式通过Context产生唯一一个ConcreteStrategy作用于代码中,而状态模式则是通过context组织多个ConcreteState形成一个状态转换图来实现业务逻辑。代码示例:
// 定义一个抽象的状态类
public abstract class State {
protected StateContext context;
public void setContext(StateContext context){
this.context = context;
}
public abstract void handle1();
public abstract void handle2();
}
// 定义A状态
public class ConcreteStateA extends State {
@Override
public void handle1() {
System.out.println("执行状态1。。。");
}
@Override
public void handle2() {
//切换为状态B
super.context.setCurrentState(StateContext.concreteStateB);
//执行状态B的任务
super.context.handle2();
}
}
// 定义B状态
public class ConcreteStateB extends State {
@Override
public void handle1() {
//切换回状态A
super.context.setCurrentState(StateContext.cincreteStateA);
//执行状态A的任务
super.context.handle1();
}
@Override
public void handle2() {
System.out.println("正在执行状态B");
}
}
// 定义一个上下文管理环境
public class StateContext {
public final static ConcreteStateB concreteStateB = new ConcreteStateB();
public final static ConcreteStateA concreteStateA = new ConcreteStateA();
private State currentState;
public State getCurrentState(){
return currentState;
}
public void setCurrentState(State currentState){
this.currentState = currentState;
this.currentState.setContext(this);
}
public void handle1() {this.currentState.handle1();}
public void handle2() {this.currentState.handle2();}
}
// 使用示例
public static void main(String[] args) {
StateContext context = new StateContext();
context.setCurrentState(new CincreteStateA());
context.handle1();
context.handle2();
}
// 运行结果 (在A状态中转换成状态B并执行状态B的方法)
Connected to the target VM, address: '127.0.0.1:49837', transport: 'socket'
执行状态1。。。
正在执行状态B
Disconnected from the target VM, address: '127.0.0.1:49837', transport: 'socket'
Process finished with exit code 0
工程实践
通过前文对状态模式的简介,我们可以看到当状态之间的转换在不是非常复杂的情况下,通用的状态模式存在大量的与状态无关的动作从而产生大量的无用代码。在美团的实践中,一个状态的下游不会涉及特别多的状态装换,所以我们简化了状态模式。当前的状态只负责当前状态要处理的事情,状态的流转则由第三方类负责。其实践代码如下:
// 返奖状态执行的上下文
public class StateContext {
private State state;
public void setState(State state){
this.state = state;
}
public State getState(){return state;}
public void echo(StateContext context){
state.doReward(context);
}
public boolean isResultFlag(){
return state.isResultFlag();
}
}
// 返奖状态抽象类
public abstract class State {
// 具体执行
public abstract void doReward(StateContext context);
// 判断是否通过改判断
public abstract boolean isResultFlag();
}
// 各状态下的处理逻辑 根据上面的业务建模来实现的 这里还是用随机数来模拟流程是否执行成功
// 订单状态检查
public class CheckOrderState extends State{
private boolean flag = false;
@Override
public void doReward(StateContext context) {
System.out.println(context.getClass().getName());
System.out.println("CheckOrderState 订单状态检查...");
double i = Math.random();
if(i > 0.4){
flag = true;
}
}
@Override
public boolean isResultFlag() {
return flag;
}
}
// 预返奖检查
public class BeforeRewardCheckState extends State{
private boolean flag = false;
@Override
public void doReward(StateContext context) {
System.out.println(context.getClass().getName());
System.out.println("BeforeRewardCheckState 预反奖状态检查...");
double i = Math.random();
if(i > 0.4){
flag = true;
}
}
@Override
public boolean isResultFlag() {
return flag;
}
}
// 返奖流程
public class SendRewardCheckState extends State{
private boolean flag = false;
@Override
public void doReward(StateContext context) {
System.out.println(context.getClass().getName());
System.out.println("SendRewardCheckState 待反奖状态检查...");
double i = Math.random();
if(i > 0.4){
flag = true;
}
}
@Override
public boolean isResultFlag() {
return flag;
}
}
// 补偿放奖流程
public class CompentstateRewardState extends State{
private boolean flag = false;
@Override
public void doReward(StateContext context) {
System.out.println(context.getClass().getName());
System.out.println("CompentstateRewardState 补偿反奖状态...");
double i = Math.random();
if(i > 0.4){
flag = true;
}
}
@Override
public boolean isResultFlag() {
return true;
}
}
// 返奖失败状态
public class RewardFailState extends State{
@Override
public void doReward(StateContext context) {
System.out.println(context.getClass().getName());
System.out.println("RewardFailState 反奖失败状态...");
}
@Override
public boolean isResultFlag() {
return false;
}
}
// 返奖成功状态
public class RewardSuccessState extends State{
@Override
public void doReward(StateContext context) {
System.out.println(context.getClass().getName());
System.out.println("RewardSuccessState 反奖成功状态...");
}
@Override
public boolean isResultFlag() {
return false;
}
}
// 全部流程整合
public static void main(String[] args) {
dosomething();
}
public static boolean dosomething(){
StateContext context = new StateContext();
context.setState(new CheckOrderState());
context.echo(context); //订单流程校验
//此处的if-else逻辑只是为了表达状态的转换过程,并非实际的业务逻辑
if(context.isResultFlag()){ // 订单校验成功 进入预返奖状态
context.setState(new BeforeRewardCheckState());
context.echo(context);
}else {// 订单校验失败 进入返奖失败状态
context.setState(new RewardFailState());
context.echo(context);
return false;
}
if(context.isResultFlag()){ // 预返奖检查成功 进入返奖状态
context.setState(new SendRewardCheckState());
context.echo(context);
}else { // 预返奖检查失败 进入返奖失败状态
context.setState(new RewardFailState());
context.echo(context);
return false;
}
if(context.isResultFlag()){ // 返奖成功 进入返奖成功状态
context.setState(new RewardSuccessState());
context.echo(context);
}else { // 返奖失败。进入补偿放奖状态
context.setState(new CompentstateRewardState());
context.echo(context);
}
if(context.isResultFlag()){ // 补偿返奖成功 进入成功状态
context.setState(new RewardSuccessState());
context.echo(context);
}else { // 补偿返奖失败 这里可以继续补偿返奖 可以认为控制补偿返奖次数。这里直接退出了
System.out.println("补偿反奖失败");
}
return true;
}
状态模式的核心是封装,将状态以及状态转换逻辑封装到类的内部来实现,也很好的体现了“开闭原则”和“单一职责原则”。每一个状态都是一个子类,不管是修改还是增加状态,只需要修改或者增加一个子类即可。在我们的应用场景中,状态数量以及状态转换远比上述例子复杂,通过“状态模式”避免了大量的if-else代码,让我们的逻辑变得更加清晰。同时由于状态模式的良好的封装性以及遵循的设计原则,让我们在复杂的业务场景中,能够游刃有余地管理各个状态。
还有一个责任链模式,我暂时还没搞懂,这里就不贴了,感兴趣的大佬可以去这里看原文呀。
ps:如有侵权,联系删除呀 谢谢