推荐语:本文详细描述责任链、中介者、适配器等多种设计模式在淘宝营销价格服务中的应用,从而实现了一套可扩展性的架构,应对灵活多变营销价格需求。
——大淘宝技术研发工程师 小枫
每年淘宝都有双11、双12等大促,我们价格服务小组的工作就是提前预计算大促期间消费者将要购买的每一件商品的价格,为消费者选出真正优惠的商品。但是大淘宝营销策略历时已久,种类多样,又紧跟市场波动,千变万化,这就要求我们设计的系统具备高可扩展性。因此今天我向大家介绍一下我们营销域下的价格服务系统是如何做到高可扩展性的。
营销域价格计算
我们营销域的价格服务主要是计算大促商品的消费者到手价。所谓消费者到手价就是该商品在指定的未来某个时间段消费者能购买到的价格,是由商品的活动价减去叠加在商品上面的各种优惠金额后的价格。举个例子,一件衣服参与双十一活动,价格为980元,商家还设置了商品优惠券满800减200元,同时商家还报名了跨店满减(满300减50元)活动,除此之外没有其他优惠了,那该商品到手价就是980-200-[980/300]*50 = 630元。
▐ 种类多样
商品优惠券、满2件打8折、跨店满减、品类券、消费券这些词对于经常网购的朋友来说肯定很熟悉,这些就是我们所说的优惠工具。为了满足不同场景下消费者的需求,我们设计了多种多样的优惠工具。用户在下单的时候往往会用到多种优惠工具,比如在今年淘宝天猫双十一活动中,大部分订单都使用了两类及其以上的优惠工具。有些情况下,为了让消费者更方便计算,优惠工具是互斥使用的。比如在双十一时,一个订单使用了跨店满减,那么就不能再使用商家发行的满折优惠了。我们要做的就是计算出最省钱的优惠工具组合方式,在此基础上得到消费者在大促期间可能买到的价格。
▐ 千变万化
消费者的需求是不断变化的,因此我们的营销策略也是随着消费者的需求不断调整的。
近些年为了让消费者更方便计算,我们开始简化营销。比如让某两类优惠工具不能同时使用,但是还有一些白名单商家或者商品类目又是可以二者同时使用的。
为了解决消费者买多件更贵问题,我们推出了预售直降券。买多件更贵问题是指:由于我们在领取商家发的优惠券时在下单时只能使用一张,在我们下单时买较少件数的价格就可以达到使用该优惠券的门槛,但是当我们买更多件时还是只能使用一张,导致了买多件还不如分开买便宜。为了解决这个问题我们推出了直降券,优惠直接作用在每件商品上面。
我们大淘宝营销工具的业务随着市场不断变化,每次大促活动玩法都各不相同,不断新增优惠和玩法,我们常常需要在短时间内把某项新的优惠玩法纳入到我们的计算链路中,这就要求我们的系统具备高可扩展性。同时如果能够充分利用原有代码,也将会节省我们的开发时间。
为何要引入设计模式
▐ 价格服务业务特点
由前面章节可以看到我们的优惠工具是种类多样且千变万化的,除此之外我们的开发测试资源也非常紧张。
每次新增优惠和玩法等业务或者进行优惠工具规则调整时,不仅留给开发人员的时间短,测试资源也非常紧张。如果我们每次新增加一个优惠等规则进入到我们的计算链路中时都会改动大量的核心计算链路代码,那测试的case也将要覆盖非常多的面。举个例子,平台新增加了官方立减优惠,如果我们系统架构设计的不够好,必须要改动原先核心计算价格的函数才能将该优惠计算规则加入,那么测试的范围可能不仅仅是当前新增加的官方立减优惠,还要包括之前已经实现的店铺优惠券、品类券、跨店满减等等各种通过该核心链路的优惠。无疑大大增加测试的工作量,本就资源紧张的测试人员更加无法完成。因此要求我们的系统能够灵活扩展,且对原有业务的影响要降到最低。
随着业务的拓展和团队的扩张,团队内不断有新人加入,面对着有着十万行以上代码、由多位开发人员合力而成的大项目,如何能够让新人迅速读懂代码,理解如此设计的道理是项目设计时必须要考虑的问题,即增加自己写的代码可读性和架构可解释性也是对我们当前开发人员的一大要求。
因此可以总结我们营销域下价格服务的业务特点:优惠种类多样、业务千变万化、需求开发周期短、测试资源紧张和开发人员迭代频繁等。因为这些特点所以要求我们的系统架构设计要高可扩展、代码高可复用、有统一架构设计标准。
▐ 设计模式
设计模式的概念出自《Design Patterns - Elements of Reusable Object-Oriented Software》中文名是《设计模式 - 可复用的面向对象软件元素》,该书是在1994 年由 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides 四人合力完成。书中提出的面向对象的设计模式七大基本准则也是我们在平时编码时要广泛遵守的,它们包括:
开闭原则(Open Closed Principle,OCP)
对外扩展开放,对内修改关闭。就是说新增需求时尽量通过扩展新类实现,而不是对原有代码修改增删。
单一职责原则(Single Responsibility 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)
一个对象要对其他对象的成员尽可能少的知道。目的就是降低类间耦合度,提高代码的可复用性。
前辈们从对象的创建、对象结构关系及对象的行为总结了适应于不同场景的设计模式,设计模式中我们常常挂在嘴边的有三大类23种,包括创建型的工厂模式、抽象工厂模式、单例模式、建造者模式、原型模式6种,结构型的适配器模式、过滤器模式、装饰器模式、桥接模式、组合模式、享元模式、代理模式和外观模式8中,行为型的责任链模式、命令模式、解释器模式、迭代器模式、中介者模式、备忘录模式、观察者模式、状态模式、空对象模式、策略模式、模板模式、访问者模式。
引入设计模式的优势可以简单概括为:
增加系统可扩展性
增加代码的复用性
增强代码可读性
借助前人经验,提供开发效率
▐ 引入设计模式
总结一下我们价格计算服务业务的项目设计需求:
系统要具备高可扩展性,适应千变万化的营销业务场景;
系统原代码、架构要具备高通用性,为开放时间短、任务重的需求减少新增代码量;
系统要核心和扩展分离,扩展时不修改原核心链路,减少测试压力;
系统设计采用通用规范,便于他人理解。
对我们的项目设计需求总结后可以发现设计模式的优势正好能够帮我们解决上述问题,因此我们的项目架构设计时引入和借鉴了很多经典的设计模式。架构在引入设计模式基础上,我们设计了更多的优化算法,让我们的价格计算撑起了每个大促活动的价格服务。
接下来我将从我们系统中用的设计模式中列举几个,看一看我们是如果借助设计模式对我们的问题进行建模的。
价格计算链路模型
▐ 业务介绍
我们价格服务的基本工作就是计算每一件商品在未来某时间段的消费者到手价。比如在我们组织的每一场大促活动,商家的报名时间是提前很多天的,商家报名商品参加活动的同时,也会为自己的商品设置很多优惠,比如满100减10元,同时商家也可以报名平台发起的优惠活动,比如报名参加跨店满减或者品类券。商品报名完成后,我们需要实时计算商品的消费者到手价,提前判断该商品有没有足够优惠。
▐ 业务模型
▐ 责任链模式
责任链模式属于行为型设计模式,英文名称Chain of Responsibility,其定义:为了避免请求发送者与多个请求处理者耦合在一起,将所有请求的处理者通过前一对象记住其下一个对象的引用而连成一条链;当有请求发生时,可将请求沿着这条链传递,直到有对象处理它为止。
责任链模式将对请求的处理过程划分为多个独立的处理节点,节点间互相不感知具体计算过程。处理节点存在前后顺序处理关系,也可以视具体的业务特点跳过某些节点。
责任链设计模式的优点如下:
▐ 中介者模式
中介者模式(Mediator Pattern)是用来降低多个对象和类之间的通信复杂性。这种模式提供了一个中介类,就像我们在网络通信链路中传递的一个数据包,该类通常处理不同类之间的通信,并支持松耦合,使代码易于维护。中介者模式属于行为型模式。
中介者设计模式的优点:
▐ 设计模式实践
我们在计算商品的消费者到手价时,客户端调用我们的价格计算HSF接口后,我们首先要对请求做基本的限流、验证等处理,验证通过后要查库获取该商品在价格计算中所需的全部信息,结合商品信息还要查询该商品可用的全部优惠,再结合对应的计算规则为该商品筛选其可用的优惠,并按照最低到手价原则为该商品叠加组合出最省钱的优惠工具使用方式,在最优组合基础上计算出该商品的消费者到手价,最后进行存储和返回。同时整个计算链路中还要面临短时间内新增一个环节或者跳过(下线)某个环节的问题。
为更好的解决上述问题,我们引入责任链模式,首先要做的到就是对整个价格计算链路进行责任分割,我们初步分割为请求的基本处理部分、商品信息获取部分、计算规则生成部分、优惠工具获取部分、价格计算部分和结果存储返回部分,每一个部分下面再细分处理节点,整个结构如下图所示。在计算商品消费者到手价的过程中,从接收Request Body到返回Response Body,责任链模式可以说是贯穿始终的。将所有对请求的处理和计算步骤拆分为一个个计算节点,上一节点计算完成后,将结果传到下一节点再进行计算。计算节点间仅仅传递数据,互相不感知具体计算过程。
责任划分完成后,按照责任链模式设计架构,并采用中介者模式,将context对象作为一个中介者,在责任链的不同处理节点间进行数据传递,架构类图如下所示。
看到这里,读者可能会疑惑这里Chain与Processor是组合关系,但是Processor的方法形参中有Chain,又依赖了Chain,有点循环依赖的感觉。这里将chain作为形参和context一起在上下文间传递,是为了在每个Processor的process()方法的最后会调用chain.process()方法,在chain.process()方法中按照初始化的Processor顺序执行调用下一个Processor。
那为什么不将chain抽离出来,仅在chain端调用下一个Processor呢?设计为如下模式。
其实这里的设计是借鉴了Servlet FilterChain过滤器链的结构,在一个 Web 应用程序中可以注册多个 Filter 程序,如果多个 Filter 程序都对同一个 URL 进行拦截,那么这些 Filter 就会组成一个Filter 链,每个Filter节点可以选择将该请求传递到哪个节点、跳过哪些节点和直接返回。
将处理链chain作为process方法的参数在上下文间传递,每个Processor可以更具自己对请求的处理结果,灵活的从chain中选择下一个Processor继续处理请求。否则,如果再回到chain中调用下一个Processor,需要当前Processor处理完成后,传回chain中某种标识,让chain根据标识选择下一个处理节点。此处直接将chain作为参数在上下文间传递,省区了标识传递,简化了设计,而且chain在初始化后是不会变化的,并不会引起在传递过程中被修改的风险。
▐ 引入效果
采用责任链模式和中介者模式相结合完成对整改商品价格计算链路的搭建后,让我们的业务扩展变得非常方便。
适配新业务能力强,可以迅速扩展新节点,编码完成新业务;
受益于每个责任链节点对其他类的依赖关系低,开发过程中对其他代码影响较小;
测试方便,可以针对扩展或修改的节点进行单独测试,很容易构建各种case测试修改节点。
优惠种类多样问题
▐ 业务介绍
我们大淘宝现在的优惠工具种类多样,作用原理又各不相同。
从功能上可以被划分为多个层级,比如店铺优惠层级的店铺券,比如平台优惠层级的跨店满减。每个层级下又有很多细分优惠,比如店铺优惠包括店铺券和店铺满减,店铺券又分为商品优惠券、店铺优惠券。
不同的优惠工具使用场景不同,作用方式不同,有的优惠工具是有门槛的,比如满100减10元的优惠券,有的优惠工具是没有门槛的,比如预售立减;有的门槛是价格,比如商品优惠券;有的门槛是商品购买数量,比如满2减9折;有的是需要凑单后才能使用的,比如跨店满减;有的是指定商品类型才能使用,比如品类券。
▐ 业务模型
▐ 适配器模式
适配器模式属于行为型设计模式,英文名称是Adapter Pattern,其定义为:作为两个不兼容的接口之间的桥梁。这种类型的设计模式属于结构型模式,它结合了两个独立接口的功能。
我们的计算链路设计好后,要在后面的业务扩展时尽可能少的去改动其代码。适配器模式会极大降低客户端的使用难度,让客户端无需关系他所连接的被适配类,只需关系适配类结构即可,适配类一般是不会变化了,即使新增被试适配类,就是我们业务上新增优惠工具、玩法时,在增加该优惠工具的描述类后,再增加一个转换类便可以接入到我们的计算链路中来。
▐ 设计模式实践
不同优惠工具其作用原理、使用方式不同,需要不同的类去描述每一种优惠工具,但是我们在计算商品的消费者到手价时,只关心所要计算的时间内能不能用这张券、商品的价格等自身条件有没有达到使用门槛、和如果能使用那么优惠多少钱。
为了方便我们对商品价格的计算,解决上述三个问题的同时要求我们的计算链路稳定性高且易扩展,就是平台新增一种优惠工具时,尽可能少的去改动我们原有核心计算链路。因此我们引入了适配器模式,这里我简单的将需要被转换的一方成为源端,最终转换成便于上层调用的一方成为宿端。我们的宿端所要实现或者描述的内容就是上述三个问题,即优惠工具使用时间、优惠工具门槛和优惠金额。源端就是诸如跨店满减、品类券、店铺券、店铺满减和官方立减等各式各样的优惠工具。因为不同优惠工具的原理不同,编码实现方式也大相径庭,因为要为每一种优惠工具提供一个转换方法。整体架构模式如下图所示。
▐ 引入效果
通过引入适配器模式,解决了在计算商品消费者到手价时不同优惠使用方式千差万别的有大量if-else的场景。尤其新增加或者减少一种优惠时,只需新增这种优惠的描述类和转换方法即可,基本不修改核心计算链路,而且可以大量复用原有链路中价格计算的代码,扩展性好。
多类型下游模型
▐ 业务介绍
我们价格服务的下游使用方有多个,不同的下游使用方业务不同,目的不同,业务模型有差别,获取的结果中的关键参数自然也不同。同时之前由于业务迭代,对原有模型进行了优化,但是之前老接口已经有调用方在调用,需要做到新老同时支持。而且随着业务发展,我们的接口下游使用方会不断增加。
▐ 业务模型
▐ 策略模式
策略模式(Strategy Pattern)属于对象的行为模式。其用意是针对一组算法,将每一个算法封装到具有共同接口的独立的类中,从而使得它们可以相互替换。策略模式将每个算法从原来统一的方法中拆分出来,每一个算法封装为一个类,这些类共同继承同一个父类或实现一个标准接口。
▐ 设计模式实践
为了减少代码中同一个方法里面大量出现if-else结构,以提高可读性和可扩展性为目的,结合业务场景特点与策略模式优势,我们在设计给下游使用方发送消息的模块时引入了策略模式。
首先抽象出一个消息Sender标准接口,接口中定义了每个消息发送类所必须实现的方法,主要是获取规则(getRules)和发送动作(send)这两个方法,同时对于一些多个类可共用的方法我们在消息Sender抽象父类中实现,向各个下游使用方进行消息发送动作的类需要实现消息Sender接口并继承消息Sender抽象父类。
所有策略的具体实现类完成后,我们业务消息SenderProcessor类中,用map集合记录所有的要发送方的规则代号和对应的消息发送实现类,具体为下图中的ruleSendMapper结构,并在该类的init()方法(创建该类实例后立刻执行该方法)中对该map进行初始化。在运行时进行消息发送时,就可以根据传递过来的规则rule选择对应的消息发送实现类进行发送。
▐ 引入效果
该部分代码中没有出现大量的if-else结构,同时随着业务扩展,在向新的下游使用方发送消息时,只需增加针对该下游使用方的一个类,让该类实现标准接口、继承公用父类即可,不会对原有代码进行修改,扩展非常方便。
多套计算规则问题
▐ 业务介绍
平台价格计算的规则是多种多样的,而且每种规则都是非常复杂的。在不同的站点如淘宝、天猫和聚划算,有不同的优惠筛选策略和计算规则。在不同的时间段如大促和日销,优惠是否计入的规则也是不一样的。
▐ 业务模型
▐ 解释器模式
解释器模式(Interpreter Pattern)提供了评估语言的语法或表达式的方式,它属于行为型模式。这种模式实现了一个表达式接口,该接口解释一个特定的上下文。在调用方和被调用方进行规则策略传递前,双方只需事先约定好每种文法或者策略都由一个代号来代替,当调用方去调用被调用方接口时,只需要传递一个简单的代号即可,在被调用方会根据代号解释翻译为具体的文法或策略。
▐ 设计模式实践
我们在HSF的请求体Request中定义了计算规则reuleId和商品所在站点,站点就包括天猫、淘宝和聚划算等等。ruleId只是一个约定的代号,通过规则翻译解释类将该代号翻译解释为该规则具体的要求,比如是否包将某类优惠计算在内、品类券具体的计入规则和店铺券发行张数的要求等。
规则翻译完成后,我们将其放入到计算链路的上下文Context中,在某些需要根据规则进行区别处理的计算节点中,将取出规则rule执行对应具体规则。
▐ 引入效果
引入后使我们的整个计算链路能够根据不同的计算规则进行灵活切换,具体的计算规则不用通过调用放传递过来,仅需传递一个编号,规则翻译解释类再将编号翻译解释为具体的处理规则,并进行处理。在规则扩展时,修改思路明确,首先在CalculateRule规则类中实现新增的规则描述,然后在所需要修改的计算节点上增加对该规则的解释实现类即可。
总结
我们的项目中使用了多种设计模式,篇幅有限此处只列出了责任链、中介者、适配器、策略和解释器五种设计模式。我们在使用某些设计模式时,并非照本宣科完全按照该模式的架构来搭建我们的系统,而是灵活的结合业务场景做了一些适配、折中和让步,但是总的方向是不违反七大设计原则的。
在一个项目中如果需要用到多种设计模式时,我认为要选择其中一个设计模式作为主设计模式,其他设计模式如果与其存在设计上的冲突时,需要为其让步。具体选择哪种模式作为项目的主设计模式,这要因业务场景而异,比如我们的价格计算服务项目就以责任链模式作为主设计模式。
营销业务对市场波动敏感,业务迭代迅速,优惠工具、规则和玩法种类多样、千变万化,正是得益于我们价格服务项目架构在设计时恰当的引入了经典的设计模式,让系统具备了高可扩展性,最终让我们有把握迅速上线新业务。
团队介绍
我们是大淘宝技术平台营销工具团队,是营销业务核心支撑团队,承担成交杠杆的职责,通过创新不断帮助业务挖掘新场景新用户、结合商业化能力打造技术引擎。
平台营销工具不断沉淀商业能力、运营支撑能力,例如品类券、购物津贴、会员卡等实现动态加码、混合出资,极大提升了面向竞争的灵活性;前N、买就返等形成大促标配组合拳,稳定支撑全年50+大促活动。
¤ 拓展阅读 ¤
3DXR技术 | 终端技术 | 音视频技术
服务端技术 | 技术质量 | 数据算法