随着美团外卖业务的不断迭代与发展,外卖用户数量也在高速地增长。在这个过程中,外卖营销发挥了“中流砥柱”的作用,因为用户的快速增长离不开高效的营销策略。而由于市场环境和业务环境的多变,营销策略往往是复杂多变的,营销技术团队作为营销业务的支持部门,就需要快速高效地响应营销策略变更带来的需求变动。因此,设计并实现易于扩展和维护的营销系统,是美团外卖营销技术团队不懈追求的目标和必修的基本功。
本文通过自顶向下的方式,来介绍设计模式如何帮助我们构建一套易扩展、易维护的营销系统。本文会首先介绍设计模式与领域驱动设计(Domain-DrivenDesign,以下简称为DDD)之间的关系,然后再阐述外卖营销业务引入业务中用到的设计模式以及其具体实践案例。
同时,我们也需要在代码工程中贯彻和实现领域模型。因为代码工程是领域模型在工程实践中的直观体现,也是领域模型在技术层面的直接表述。而设计模式,可以说是连接领域模型与代码工程的一座桥梁,它能有效地解决从领域模型到代码工程的转化。
所谓“模式”,就是一套反复被人使用或验证过的方法论。从抽象或者更宏观的角度上看,只要符合使用场景并且能解决实际问题,模式应该既可以应用在DDD中,也可以应用在设计模式中。事实上,Evans也是这么做的。他在著作中阐述了Strategy和Composite这两个传统的GOF设计模式是如何来解决领域模型建设的。因此,当领域模型需要转化为代码工程时,同构的模式,天然能够将领域模型翻译成代码模型。
营销业务的特点
如前文所述,营销业务与交易等其他模式相对稳定的业务的区别在于,营销需求会随着市场、用户、环境的不断变化而进行调整。也正是因此,外卖营销技术团队选择了DDD进行领域建模,并在适用的场景下,用设计模式在代码工程的层面上实践和反映了领域模型。以此来做到在支持业务变化的同时,让领域和代码模型健康演进,避免模型腐化。
理解设计模式
软件设计模式(Designpattern),又称设计模式,是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码,让代码更容易被他人理解,保证代码可靠性,程序的重用性。可以理解为:“世上本来没有设计模式,用的人多了,便总结出了一套设计模式。”
设计模式原则
面向对象的设计模式有七大基本原则:
简单理解就是:开闭原则是总纲,它指导我们要对扩展开放,对修改关闭;单一职责原则指导我们实现类要职责单一;里氏替换原则指导我们不要破坏继承体系;依赖倒置原则指导我们要面向接口编程;接口隔离原则指导我们在设计接口的时候要精简单一;迪米特法则指导我们要降低耦合。
设计模式就是通过这七个原则,来指导我们如何做一个好的设计。但是设计模式不是一套“奇技淫巧”,它是一套方法论,一种高内聚、低耦合的设计思想。我们可以在此基础上自由的发挥,甚至设计出自己的一套设计模式。
当然,学习设计模式或者是在工程中实践设计模式,必须深入到某一个特定的业务场景中去,再结合对业务场景的理解和领域模型的建立,才能体会到设计模式思想的精髓。如果脱离具体的业务逻辑去学习或者使用设计模式,那是极其空洞的。接下来我们将通过外卖营销业务的实践,来探讨如何用设计模式来实现可重用、易维护的代码。
3.2.1业务简介
“邀请下单”是美团外卖用户邀请其他用户下单后给予奖励的平台。即用户A邀请用户B,并且用户B在美团下单后,给予用户A一定的现金奖励(以下简称返奖)。同时为了协调成本与收益的关系,返奖会有多个计算策略。邀请下单后台主要涉及两个技术要点:
3.2.2返奖规则与设计模式实践
业务建模
如图是返奖规则计算的业务逻辑视图:
从这份业务逻辑图中可以看到返奖金额计算的规则。首先要根据用户状态确定用户是否满足返奖条件。如果满足返奖条件,则继续判断当前用户属于新用户还是老用户,从而给予不同的奖励方案。一共涉及以下几种不同的奖励方案:
新用户
老用户
计算完奖励金额以后,还需要更新用户的奖金信息,以及通知结算服务对用户的金额进行结算。这两个模块对于所有的奖励来说都是一样的。
可以看到,无论是何种用户,对于整体返奖流程是不变的,唯一变化的是返奖规则。此处,我们可参考开闭原则,对于返奖流程保持封闭,对于可能扩展的返奖规则进行开放。我们将返奖规则抽象为返奖策略,即针对不同用户类型的不同返奖方案,我们视为不同的返奖策略,不同的返奖策略会产生不同的返奖金额结果。
在我们的领域模型里,返奖策略是一个值对象,我们通过工厂的方式生产针对不同用户的奖励策略值对象。下文我们将介绍以上领域模型的工程实现,即工厂模式和策略模式的实际应用。
模式:工厂模式
工厂模式又细分为工厂方法模式和抽象工厂模式,本文主要介绍工厂方法模式。
模式定义:定义一个用于创建对象的接口,让子类决定实例化哪一个类。工厂方法是一个类的实例化延迟到其子类。
工厂模式通用类图如下:
我们通过一段较为通用的代码来解释如何使用工厂模式:
//抽象的产品publicabstractclassProduct{publicabstractvoidmethod();}//定义一个具体的产品(可以定义多个具体的产品)classProductAextendsProduct{@Overridepublicvoidmethod(){}//具体的执行逻辑}//抽象的工厂abstractclassFactory
模式定义:定义一系列算法,将每个算法都封装起来,并且它们可以互换。策略模式是一种对象行为模式。
策略模式通用类图如下:
我们通过一段比较通用的代码来解释怎么使用策略模式:
//定义一个策略接口publicinterfaceStrategy{voidstrategyImplementation();}//具体的策略实现(可以定义多个具体的策略实现)publicclassStrategyAimplementsStrategy{@OverridepublicvoidstrategyImplementation(){System.out.println("正在执行策略A");}}//封装策略,屏蔽高层模块对策略、算法的直接访问,屏蔽可能存在的策略变化publicclassContext{privateStrategystrategy=null;publicContext(Strategystrategy){this.strategy=strategy;}publicvoiddoStrategy(){strategy.strategyImplementation();}}工程实践
通过上文介绍的返奖业务模型,我们可以看到返奖的主流程就是选择不同的返奖策略的过程,每个返奖策略都包括返奖金额计算、更新用户奖金信息、以及结算这三个步骤。我们可以使用工厂模式生产出不同的策略,同时使用策略模式来进行不同的策略执行。首先确定我们需要生成出n种不同的返奖策略,其编码如下:
//抽象策略publicabstractclassRewardStrategy{publicabstractvoidreward(longuserId);publicvoidinsertRewardAndSettlement(longuserId,intreward){};//更新用户信息以及结算}//新用户返奖具体策略ApublicclassnewUserRewardStrategyAextendsRewardStrategy{@Overridepublicvoidreward(longuserId){}//具体的计算逻辑,...}//老用户返奖具体策略ApublicclassOldUserRewardStrategyAextendsRewardStrategy{@Overridepublicvoidreward(longuserId){}//具体的计算逻辑,...}//抽象工厂publicabstractclassStrategyFactory
publicclassRewardContext{privateRewardStrategystrategy;publicRewardContext(RewardStrategystrategy){this.strategy=strategy;}publicvoiddoStrategy(longuserId){intrewardMoney=strategy.reward(userId);insertRewardAndSettlement(longuserId,intreward){insertReward(userId,rewardMoney);settlement(userId);}}}接下来我们将工厂模式和策略模式结合在一起,就完成了整个返奖的过程:
publicclassInviteRewardImpl{//返奖主流程publicvoidsendReward(longuserId){FactorRewardStrategyFactorystrategyFactory=newFactorRewardStrategyFactory();//创建工厂Inviteeinvitee=getInviteeByUserId(userId);//根据用户id查询用户信息if(invitee.userType==UserTypeEnum.NEW_USER){//新用户返奖策略NewUserBasicRewardnewUserBasicReward=(NewUserBasicReward)strategyFactory.createStrategy(NewUserBasicReward.class);RewardContextrewardContext=newRewardContext(newUserBasicReward);rewardContext.doStrategy(userId);//执行返奖策略}if(invitee.userType==UserTypeEnum.OLD_USER){}//老用户返奖策略,...}}工厂方法模式帮助我们直接产生一个具体的策略对象,策略模式帮助我们保证这些策略对象可以自由地切换而不需要改动其他逻辑,从而达到解耦的目的。通过这两个模式的组合,当我们系统需要增加一种返奖策略时,只需要实现RewardStrategy接口即可,无需考虑其他的改动。当我们需要改变策略时,只要修改策略的类名即可。不仅增强了系统的可扩展性,避免了大量的条件判断,而且从真正意义上达到了高内聚、低耦合的目的。
3.2.3返奖流程与设计模式实践
当受邀人在接受邀请人的邀请并且下单后,返奖后台接收到受邀人的下单记录,此时邀请人也进入返奖流程。首先我们订阅用户订单消息并对订单进行返奖规则校验。例如,是否使用红包下单,是否在红包有效期内下单,订单是否满足一定的优惠金额等等条件。当满足这些条件以后,我们将订单信息放入延迟队列中进行后续处理。经过T+N天之后处理该延迟消息,判断用户是否对该订单进行了退款,如果未退款,对用户进行返奖。若返奖失败,后台还有返奖补偿流程,再次进行返奖。其流程如下图所示:
我们对上述业务流程进行领域建模:
可以看到,我们通过建模将返奖流程的多个步骤映射为系统的状态。对于系统状态的表述,DDD中常用到的概念是领域事件,另外也提及过事件溯源的实践方案。当然,在设计模式中,也有一种能够表述系统状态的代码模型,那就是状态模式。在邀请下单系统中,我们的主要流程是返奖。对于返奖,每一个状态要进行的动作和操作都是不同的。因此,使用状态模式,能够帮助我们对系统状态以及状态间的流转进行统一的管理和扩展。
模式:状态模式
模式定义:当一个对象内在状态改变时允许其改变行为,这个对象看起来像改变了其类。
状态模式的通用类图如下图所示:
对比策略模式的类型会发现和状态模式的类图很类似,但实际上有很大的区别,具体体现在concreteclass上。策略模式通过Context产生唯一一个ConcreteStrategy作用于代码中,而状态模式则是通过context组织多个ConcreteState形成一个状态转换图来实现业务逻辑。接下来,我们通过一段通用代码来解释怎么使用状态模式:
通过前文对状态模式的简介,我们可以看到当状态之间的转换在不是非常复杂的情况下,通用的状态模式存在大量的与状态无关的动作从而产生大量的无用代码。在我们的实践中,一个状态的下游不会涉及特别多的状态装换,所以我们简化了状态模式。当前的状态只负责当前状态要处理的事情,状态的流转则由第三方类负责。其实践代码如下:
3.3.1业务简介
继续举例,点评App的外卖频道中会预留多个资源位为营销使用,向用户展示一些比较精品美味的外卖食品,为了增加用户点外卖的意向。当用户点击点评首页的“美团外卖”入口时,资源位开始加载,会通过一些规则来筛选出合适的展示Banner。
3.3.2设计模式实践
对于投放业务,就是要在这些资源位中展示符合当前用户的资源。其流程如下图所示:
从流程中我们可以看到,首先运营人员会配置需要展示的资源,以及对资源进行过滤的规则。我们资源的过滤规则相对灵活多变,这里体现为三点:
为了实现过滤规则的解耦,对单个规则值对象的修改封闭,并对规则集合组成的过滤链条开放,我们在资源位过滤的领域服务中引入了责任链模式。
模式:责任链模式
模式定义:使多个对象都有机会处理请求,从而避免了请求的发送者和接受者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有对象处理它为止。
责任链模式通用类图如下:
我们通过一段比较通用的代码来解释如何使用责任链模式:
下面通过代码向大家展示如何实现这一套流程:
本文从营销业务出发,介绍了领域模型到代码工程之间的转化,从DDD引出了设计模式,详细介绍了工厂方法模式、策略模式、责任链模式以及状态模式这四种模式在营销业务中的具体实现。除了这四种模式以外,我们的代码工程中还大量使用了代理模式、单例模式、适配器模式等等,例如在我们对DDD防腐层的实现就使用了适配器模式,通过适配器模式屏蔽了业务逻辑与第三方服务的交互。因篇幅原因不再进行过多的阐述。
对于营销业务来说,业务策略多变导致需求多变是我们面临的主要问题。如何应对复杂多变的需求,是我们提炼领域模型和实现代码模型时必须要考虑的内容。DDD以及设计模式提供了一套相对完整的方法论帮助我们完成了领域建模及工程实现。其实,设计模式就像一面镜子,将领域模型映射到代码模型中,切实地提高代码的复用性、可扩展性,也提高了系统的可维护性。
当然,设计模式只是软件开发领域内多年来的经验总结,任何一个或简单或复杂的设计模式都会遵循上述的七大设计原则,只要大家真正理解了七大设计原则,设计模式对我们来说应该就不再是一件难事。但是,使用设计模式也不是要求我们循规蹈矩,只要我们的代码模型设计遵循了上述的七大原则,我们会发现原来我们的设计中就已经使用了某种设计模式。
吴亮亮,2017年加入美团外卖,美团外卖营销后台团队开发工程师。