饥饿面条交易系统的改造与完善

我于2017年5月加入饥饿交易部。我负责搜索、订单、加班、薪酬、合同、交货、金额计算和评估系统。我在后期开始升级整个系统。本文第

条是在第一阶段贸易体制改革后形成的。它主要反映在决策过程中的思维。我没有使用“框架”这个词,因为它给人一种力量和神秘的感觉。谈论“框架”给人的感觉是,他们正在做出负责任的决策或深入的技术分析。

,毕宣在《系统设计的常规》一文中提到:

回顾了我做过的几个系统设计,发现我在做系统设计时确实遵循了一个常规。这个例程的目的是:系统设计->;系统设计的目标->;围绕目标的核心设计->;围绕核心设计形成的设计原则->;每个子系统和模块的详细设计

在设计系统时,首先要清楚地理解目的并形成可测量的目标

"soft ware" ware

软件被分解为软件,即灵活产品bob叔叔的

重新配置之前的第一版交易系统的代码可以追溯到8年前。在此期间,它还经历了拆卸和重新配置。当我在17年后到达时,主要系统如下:

饿了么超时怎么赔付

饿了么超时怎么赔付

这个系统将业务从数百万个订单传送到数百万个订单。从压力测量的性能来看,它可以支持业务翻几倍以上,也就是说,如果没有变化,它可以继续稳定运行,但如果有一些变化,答案可能不太确定。

在我就职的两年中,系统所承载的业务有所增加和变化:从单一餐饮外卖到与新的零售和品牌餐饮并行,从家庭模式到商店模式,接着是业务的持续差异化定制和并行在线的需求另一方面,随着公司组织结构的变化,一些项目需要在三个地方进行,沟通和合作的成本增加了一倍。几个方面的结合导致开发没有精力为大多数系统的进化制定完美的计划。

个多月前,该企业提出了一个简单的要求:自动审核交易评估并实施相应的处罚。当时,评估核心“领域模型”如下:

饿了么超时怎么赔付

设计本身的优缺点暂时不在此讨论,只是为了说明为了满足这一需求,需要改变多个评估子模块,开发和评估的工作量远远超出预期,业务方对此并不满意,类似的冲突经常发生在其他系统中。然而,事实上,团队中没有一个人像以前一样懒惰和努力工作,除了无论投入多少个人时间,节省了多少火,增加了多少班次,都无法达到产出,因为大部分开发时间都花在系统修复上,而不是实际完成新的功能,而且总是拆东墙补西墙,循环往复。为什么

会导致这样的结果,我认为这应该是因为大多数系统已经进化到很难响应需求的变化。业务视图中的小变化是系统开发的主要操作,但是系统不应该朝这个方向发展。它与硬件有很大的不同,因为软件的变化应该是简单和灵活的。

所以我们考虑设计的核心目标:*“采用一个好的软件架构来节省项目建设和维护的人工成本,使每一个变更都简短、易于实现、避免缺陷,并以最小的成本最大限度地满足功能和灵活性的要求。”当

源代码是设计时,

提到软件设计时,人们可能会一个接一个地想到结构清晰的体系结构图,认为软件体系结构的所有秘密都隐藏在图中,但是在经历了一些项目之后,这往往是不够的。杰克·里夫斯在1992年发表了一篇题为《源代码就是设计》的论文,其中他提出了一个观点:

高层结构的设计不是一个完整的软件设计,它只是一个用于详细设计的结构框架。我们严格验证高级设计的能力非常有限。详细设计最终对高层设计的影响至少与其他因素一样大(或者这种影响应该被允许)改进设计的所有方面是一个应该贯穿整个设计周期的过程。在踩了一些坑之后,这种强调详细设计重要性的观点在我看来是很有根据的。简而言之,“自上而下的设计通常是不可靠的,编码是设计过程的一部分”。就个人而言,随着抽象水平的提高,系统设计应该自下而上地发展,并获得良好的高层设计。

编程范式

从下到上,应该从编码开始,饥饿交易系统最初是由Python编写的,Python足够灵活,能够非常快地生成mvp系统版本,这也与当时公司的发展状况有关:产品迭代速度快,新项目压力大。

最近进行了重组,以顺应集团的趋势。我们用Java写的。然而,在此之前,发生了一个小事件:在17年末,因为估计当前的系统框架在单个卷中达到下一个数量级时会遇到瓶颈,我们逐渐开始为一些新业务编写Go语言。然而,在这个过程中,我们经常听到一些评论:用go写业务是不舒服的为什么不舒服?这大概是因为没有框架,没有泛型,也没有尝试捕捉。事实上,在解决业务问题的大环境中,go语言并不是最好的选择,但是语法很简单,可以大大避免普通程序员出错的可能性。

至于Python,任何东西都有一把双刃剑。虽然Python有很强的表达能力,但是灵活性也让很多人感到不快。代码编写很粗糙,动态语言编写有太多漏洞,容易出错。它在大型工程的工程管理和维护方面存在一定的缺点。因此,rails的作者提到“灵活性被高估了——约束是解放”,这是有一定原因的

。为了避免引起一场语言战争,这里没有太多的讨论,但我只想指出:我从C写到了Go,从Python写到了Java。在这个过程中,我意识到在学习任何编程语言时,编程范式可能是最重要的术语。简而言之,这是程序员在查看程序时应该具备的一点,但这很容易被忽略。旧交易系统的代码,不管它针对的是什么样的商业逻辑,几乎都是OPP。类似的代码在系统中随处可见。

我们似乎完全忘记了OOP。这项古老的技能已经被稀释了。我在这里不是说面向对象必须是完美的。准确地说,我是“以问题为导向”范式的支持者。例如,Java本质上需要面向对象,但业务流程不一定需要面向对象。有些交易业务是第一步,第二步,OPP范式是一个很好的解决方案此时,有时没有必要进行复杂的类设计,但它也会引起麻烦

。另外,同样的问题可以分解成不同的层次,不同的层次可以使用自己合适的方法。例如,顶层可以使用面向对象,而特定的执行逻辑可以使用浮点运算。例如,对于订单数量的计算,我们已经用Go编写了一个FP底层计算服务的版本。高性能、简单的语法和较少的错误是该语言的优势。核心是因为这种问题本身是适合的。

但是,当面对整个交易领域时,面向对象设计概念的合理使用已经证明能够支持复杂和多样的业务场景的复杂和庞大的软件设计,所以我们做出了第一个决定:采用由面向对象主导的“混合”范例

原则和模式

一个糟糕的程序员和一个好的程序员的区别在于他是否认为他的代码或他的数据结构更重要。糟糕的程序员担心无论采用哪种编程范式或语言,构建的基本模块都像盖楼的砖块。如果砖的质量不好,最终的建筑就不会牢固。在这段引语中,关系是我最想强调的:我理解它指的是类之间的交互。“关系”的质量通常等同于软件设计的质量。设计不好的软件结构有一些共同的特点:

刚性:很难对软件进行更改,这通常会导致连锁变化。例如,当下订单以添加新的营销类型时,订单中心和相关的上游和下游部门都应该察觉并改变漏洞:简单的改变将导致其他意想不到的问题,甚至概念是完全不相关和坚定的:在其他系统的设计中有一些有用的部分,但是拆卸的风险和成本非常高。例如,订单中心支付外卖场景的能力不支持对虚拟商品(如会员卡)支付要求的不必要的复杂性。这通常是指过度设计的晦涩:随着时间的推移,模块很难理解,代码也越来越难理解。例如,购物车阶段的核心代码已经成长为一个将近1000行的大函数...在采用适当的范例之后,我们需要制定一个标准来关注代码之上的逻辑。多年来,软件工程的发展催生了一些基本的原则和模式,这些原则和模式已经被证明可以指导我们如何封装数据和函数,然后将它们组织成程序。

SOLID

有些人重新安排了这些原则,把第一个字母变成了SOLID,分别是SRP、OCP、LSP、ISP和DIP这里有一些这些原则的例子。

SRP(单一责任):这个原则非常简单,也就是说,任何软件模块都应该只对一种类型的用户负责,所以代码和数据应该组织在一起,因为它们与某种类型的用户有着密切的关系。事实上,我们的大部分工作是找到责任,然后打开它们。

我认为这个原则的核心在于用户的定义。当我听了18年的Qcon时,我听到了俞军的分享,其中一个可以用来解释到底什么是用户。俞军说:“用户不是人,他们是需求的集合。”在我们重建的过程中,有一场关于贸易体系中交货环节的争论。你现在是否渴望支持商家的自我分销、平台托管和分销选择(例如跑腿)。这些类型的分发的定价方法、分发逻辑和使用场景是不同的,因此我们基于此进行了分解,起初每个人都同意这种分解方法

但随后商户群进行了调整,新的零售商户和餐饮商户被拆分,相应的商户的运营模式也开始不同,导致每种分销模式下的需求不同。有了这些变化,我们最终选择了进行第二次拆分。

对于单个责任,这里有一个小提示:如果你真的不擅长分析,你可以观察到更多由于分支合并而产生冲突的代码,因为这可能是因为你在同一时间为不同的需求改变了同一个模块。

DIP(依赖倒置):有人说依赖倒置是面向对象和OPP之间的分水岭,因为在过程设计中创建的依赖关系依赖于细节——也就是说,顶层依赖于底层,但这通常会由于细节的变化而影响策略。例如,在外卖场景中,一旦用户由于某种原因未能接收到膳食,商家将补偿优惠券以安抚用户。OPP此时可以做到这一点:

已经过去一段时间了,因为代金券通常不会在商店间使用。该平台希望用户继续购买,所以它希望通过支付一般红包来留住他们。此时,它需要改变旧的代码,只有通过增加对红包支付逻辑的依赖,它才能满足需求。

但是如果以另一种方式采用DIP,问题可能会得到更好的解决:

饿了么超时怎么赔付

当然这个例子是一个简化的版本,在实际工作中有许多更复杂的场景,但本质是一样的:采用OOP来逆转策略对细节的依赖,使细节依赖于抽象,并且客户通常有服务接口。这个过程的核心是我们需要做好抽象。

OCP(开放封闭原则):如果你仔细分析,你会发现这个原则实际上是我们在开始时设定的系统设计的目标,也是其他原则想要达到的最终目标。例如,通过SRP,每个业务线的模块将被分解,变更将被隔离。但是,该平台在一定程度上仍然需要抽象,核心业务流程会沉淀下来,各条业务线会按照自己的定义开放,然后再应用到DIP中。

没有其他原则的例子。当然,除了固体,还有其他类型的原则,如IoC:以外卖交易平台为例。商家向用户出售大米,同时支付货款和送货。因此,基本上,用户和商家必须是强耦合的(他们必须满足)这时,饥饿的平台作为保证出现了。用户先把钱放进平台,平台要求商家接收订单,然后分发食物。用户收到餐后,平台会用钱给商家打电话。这是反向控制,买家和卖家将他们对彼此的直接依赖和控制,转换到彼此依赖标准交易模式的界面。

可以发现,只要对法律进行总结,总会有这样或那样的原则,但每个原则的使用并不是一劳永逸的——代码的调整需要根据实际需求的变化而不断进行,而且该原则不是万能的,不能无条件地使用,否则会因过度遵从而带来不必要的复杂性。例如,一些使用工厂模式的代码经常出现,其中的一个新代码实际上违反了DIP,所以适度就足够了

发展到模式

。这里的模式就是我们通常所说的设计模式。使用进化这个词是因为我认为模式不是设计的起点,而是终点。《设计模式》一书的内容不是作者的发明和创造,而是从大量实际系统中提炼出来的。其中大部分是已经存在了很长时间并且已经被广泛使用的实践,但是还没有被系统地梳理。换句话说,只要遵循上面描述的一些原则,这些模式自然会反映在系统代码中。在《敏捷软件开发》一书中,有一章专门描述了代码随着调整逐渐演变成观察者模式的过程。

所有权模式当然是好的。例如,在搜索系统中,通过模板方法模式,可以定义一套完整的搜索参数分析模板,不同的查询需求只能通过增加配置来定制。我在这里最想强调的不是设计模式驱动的编程,以交易系统中的状态机为例(状态机太普通了,就像家里用的台灯一样简单,有开有关的状态,但在交易场景中会更复杂)。在餐饮和外卖交易中,有以下状态流模型:

饿了么超时怎么赔付

实现这样一个有限状态机。最直接的方法是使用嵌套的switch/case语句,用简单的代码如

public class order {//States public static final int accept = 5;公共静态最终int SETTLED = 9;..//事件公共静态最终int到达= 1;//订单交付到publicvoid事件(int event){ switch(state){ case accept:switch(event){ case到达:state =已解决;//To Do Action Breakcase}}}上面的代码看起来很容易接受,因为它简化了过程。然而,对于具有如此复杂的顺序状态的状态机来说,这个switch/case语句将无限扩展并且可读性很差。另一个问题是国家的逻辑和行动没有被分解。设计模式提供状态模式。具体方法如下:

饿了么超时怎么赔付

确实将状态机的动作和逻辑分开,但随着状态的增加,状态类的增加会使系统变得极其复杂,对OCP的支持也不好:对于切换状态的场景,新的类会导致状态切换类的修改,而最不能容忍的是这种方法将整个状态机的逻辑隐藏在分散的代码中。以前版本的

的交易系统是通过解释迁移表来实现的。简化版本如下:

#结束订单添加转换(触发器=到达,SRC =接受,DEST =已解决,开_开始= _设置_订单_已解决_ AT,设置_状态= _设置_状态_带_记录,//改变状态开_结束= _推_到_转换...# engine defevent _ fire(事件,当前状态):对于转换中的转换:if转换。on _ start = =当前状态& & amp过渡的版本。触发器= =事件:转换。on _ start () current _ state =转换。desttransition。on _ end()很容易理解。状态逻辑是集中的,不与动作耦合。可伸缩性相对较强。唯一的缺点是遍历时间,但它也可以通过字典表进行优化,但它的整体优势更明显

然而,随着业务的发展,交易系统需要同时支持多组状态机,这意味着将有多个迁移表,并且还需要根据业务进行扩展和定制。这个解决方案会使代码编写更加复杂。在重新配置期间,我们采用了两级流程编排引擎的方法来优化这个问题,但是这超出了我们的讨论范围。在这里,我只想强调第二个决定:代码应该通过设计原则灵活地分析问题,然后通过适当的设计模式解决问题。设计模式驱动的编程是不可能的。例如,有时一个全局变量可以代替所谓的单例模式。

丰富的领域意味着

一旦你想解释美而不提及具有这种特征的东西,那么用一种不太恰当的方式解释

是完全不可能的。如果你之前说的是静态问题的策略,现在我们需要讨论动态问题的解决方案:即使没有风,人们也不会认为树叶是稳定的,所以人们定义的稳定时间与变化的频率无关,而是与变化的成本有关,因为吹一口气,树叶就会随之摇摆我们不仅要把当前的代码写得足够好,使之清晰合理,还要写能应对需求变化的“叶子”代码。

面向业务变化的设计首先要理解业务的核心问题,然后分解并划分为子领域。DDD——也就是领域驱动的设计,已经被证明是一个很好的起点。这并不是要把它作为一种技术来研究,而是作为一种方法论来指导发展,成为第三种决策,而我还处于初级阶段,所以我只会说一些理解深刻的观点。

公共语言

一个设计良好的架构对系统的行为有一个最重要的影响:它清楚而明确地反映了系统的设计意图。简而言之,当你拉下一些服务的代码时,你一眼就能想到:嗯,这“看起来”像一个交易系统的应用程序。我们不能在嘴里谈论业务逻辑,但可以在手中敲出另一个代码。简而言之,我们看不到人们在和人们交谈,我们也不能在地狱里胡说八道。我们可以比较这两种分包方式,比较容易理解:

饿了么超时怎么赔付

饿了么超时怎么赔付

。发现领域通用语言的目的之一是通过掌握领域内涵来改变需求。这需要许多客观条件,比如团队中有一个领域专家。但是有时候,我们也可以在内部解决问题,* *我曾经见过一个在丁香园工作的程序员朋友,他买了大量的医学书籍。不用问,我猜他一定是DDD的信徒。

对于这一点,我们在重构过程中也做了一些工作,使“源代码就是设计”:可视化领域元素。在系统领域中的一些概念与产品达成一致后,我们将添加一致的注释。当代码被编译时,它可以被扫描和收集并发送到前端进行绘制。

回到了前面提到的评估领域模型,在与产品进行了多次沟通后,后来意识到产品并不期望评估这么多种类的产品。对它来说,货物和乘客都是要评估的对象。从领域模型的角度来看,以前的设计更多的是面向场景而不是行为,所以合理的领域模型应该是

饿了么超时怎么赔付

边界上下文

,这在我们通常的开发过程中是很常见的以用户系统为例:如果从用户自己的角度来看,用户的对象可以登录、注销和修改昵称。如果你从其他普通用户那里看到它,你只能看到昵称之类的东西。如果您从后台管理员那里看到它,您可以注销或退出登录。此时,有必要定义一个范围来解释现在的范围用户是谁,这实际上是DDD的有界上下文的概念

绑定上下文可以很好地隔离同一事物的不同内涵,并可以通过严格的规范进入上下文的对象模型,从而保护业务抽象行为的一致性。回到交易领域,饥饿是第一个支持svip游戏的人。为了支持相应的结算需求,需要访问交易系统来完成业务。我们通过分解问题域来降低复杂性,此时我们相应地切入成员域和交易域。为了防止超级卡在进入交易字段时干扰交易内部的业务逻辑,我们做了一个映射:

饿了么超时怎么赔付

拆分

当所有的代码都完成后,随着程序的增长,会有越来越多的人参与进来。为了便于协作,这些代码必须分成便于个人或团队维护的组。根据软件变化的不同速度,上述代码可以转换成几个组件:

扩展:扩展包,存储上述业务定制包,以及面向对象的思想。核心贡献在于允许插件通过多态性切换程序的逻辑。事实上,软件开发技术发展的历史是一个尝试方便地添加插件以创建可扩展和可维护的系统架构的过程。领域:领域包是最稳定的,它包含了核心业务包和领域通用语言业务:业务包存储特定的业务逻辑。它和域包的区别在于域包可能提供了一个people.run()方法,他将使用它来运送外卖食物或进行锻炼。基础设置包,存储对数据库和各种中间件的依赖,都属于业务逻辑之外的细节。然后是等级依赖。马丁·弗劳尔已经提供了一套经典的分层封装模式。以简化订单模块为例:

。但是,如果一些学生避免进行各种类型的转换,不想严格遵守层次依赖,而觉得有些查询(这里,查询!=读取)可以直接绕过域层,从而成为CQRS模式:

饿了么超时怎么赔付

。然而,最理想的方式是以下一种。作为核心业务逻辑,域层不应该依赖于基础设施的细节。这样,代码的可测试性也将提高到

饿了么超时怎么赔付

。在单个程序的组件被分离后,代码将被升级到更高的级别。我们开始关注四项核心服务:预订分为购物车、购买和计算,状态信息分为处理、查询和超时,与商户订单相关的部分Blink功能分为处理、查询,部分物流交付分为一次交付。最后,事务的核心服务被分解成如下图:

饿了么超时怎么赔付

饿了么超时怎么赔付

到目前为止,包括这种拆分方法,共有四个决策,实际上没有必要将它们分成序列。他们的核心是围绕软件灵活性的目标,从程序范例到组件编写,最后到分层,我们积极选择或避免一些教条式的限制。因此,业务架构在某种意义上也限制了程序员在某些领域的一些行为,允许他们按照我们想要标准化的方向编码。从而实现整个系统的灵活性和可靠性

“无银弹”

“个人和互动比流程和工具更好。”敏捷宣言

的第1条当前的系统架构看起来像什么并不重要,因为随着时间的推移它可能会分解成其他的形状。重要的是,我们应该认识到,对于如何建立一个灵活的贸易体系,没有灵丹妙药。

如果你仔细观察,你会发现在当前的系统中还有许多问题需要解决。例如,一些跨境更改:某个服务的接口将添加系统链接中的字段,导致上游和下游一起更改。更令人尴尬的是,最初我们分割服务来解耦,但有时会出现服务发布依赖的现象。系统进化是一场持久的战争,“个人和互动比过程和工具更好”,人才是胜利的核心因素。在过去的两年里,我们没有停止思考和实践。我们经常看到交易团队成员之间的争议,从界面字段的变化到字段之间的边界。人们已经进行了大量的讨论来获得一个合理的技术解决方案。这让我想起了禅宗和摩托车维修艺术中提到的良好品质。一些人评论说,程序员可能有这样的关于良好品质的经验——他们写了一个很棒的代码,你会觉得“你没有写代码,代码总是存在的,你找到了它。”

参考书

软件设计哲学-约翰·特罗霍夫

+

禅宗和摩托车维修艺术-罗伯特·皮尔希

+

领域驱动设计-埃里克·埃文斯

敏捷软件开发-鲍勃叔叔

“清理结构的方法”-鲍勃叔叔

“极客和团队”-布莱恩·W·菲茨帕特里克256

大家都在看

相关专题