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%;" />

我们对上述业务流程进行领域建模:

  1. 在接收到订单消息后,用户进入待校验状态;
  2. 在校验后,若校验通过,用户进入预返奖状态,并放入延迟队列。若校验未通过,用户进入不返奖状态,结束流程;
  3. T+N天后,处理延迟消息,若用户未退款,进入待返奖状态。若用户退款,进入失败状态,结束流程;
  4. 执行返奖,若返奖成功,进入完成状态,结束流程。若返奖不成功,进入待补偿状态;
  5. 待补偿状态的用户会由任务定期触发补偿机制,直至返奖成功,进入完成状态,保障流程结束。

<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:如有侵权,联系删除呀 谢谢

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容

  • 目标V.S 目标状态——TPS中的理想状况 英国演化理论学者理查德·道金斯在《自私的基因》中提到一个有趣的理论困难...
    大灰狼与小白兔阅读 1,616评论 0 0
  • (一)JSP的概述 一、什么是JSP JSP:JavaServer Pages(Java服务器页面),其实就是...
    请重置阅读 264评论 0 0
  • 昨晚接到宝贝一路上我在开车,但她想让我抱,我说警察叔叔如果看到车车前面有小孩子就会把妈妈抓起来,半小时以内都还好,...
    甜心教主阅读 134评论 0 0
  • 现在拿到作业习惯性地想尽早完成,大概算是充分调动起体内的学霸潜力吧,时间安排上来说是满意的,至少效率提升了。 今天...
    蒙蒙小丸子麻麻阅读 175评论 0 1