ag真人-重构:改善饿了么生意业务系统的设计思路

作者:ag真人发布时间:2021-08-13 00:02

本文摘要:我在2017年5月加入饿了么的生意业务部门,先后卖力搜索、订单、超时、赔付、条约、交付、金额盘算以及评价等系统,后期开始做些整体系统升级的事情。这篇文章成型于生意业务系统重构一期之后,主要是反思其历程中做决议的思路,我没有使用「架构」这个词语,是因为它给人的感受充满权利和神秘感,谈论「架构」让人有一种正在举行责任重大的决议或者深度技术分析的感受。

ag真人

我在2017年5月加入饿了么的生意业务部门,先后卖力搜索、订单、超时、赔付、条约、交付、金额盘算以及评价等系统,后期开始做些整体系统升级的事情。这篇文章成型于生意业务系统重构一期之后,主要是反思其历程中做决议的思路,我没有使用「架构」这个词语,是因为它给人的感受充满权利和神秘感,谈论「架构」让人有一种正在举行责任重大的决议或者深度技术分析的感受。如毕玄在系统设计的套路这篇文章里所提:回首了下自己做过的几个系统的设计,发现现在自己在做系统设计的时候确实是会根据一个套路去做,这个套路就是:系统设计的目的->系统设计的目的->围绕目的的焦点设计->围绕焦点设计形成的设计原则->各子系统,模块的详细设计在举行系统设计时,摸清楚目的,并形成可权衡的目的是第一步。

"Soft" wareSoftware拆开来划分是soft ware,即灵活的产物 -- 鲍勃大叔重构前的生意业务系统第一版的代码可以追溯到8年前,这期间也履历过拆解重构,17年我来到时,主要系统是这样:这套系统驮着业务从百万级订单跑到了千万级订单,从压测体现来看,它可以再支撑业务多翻几倍的量,也就是说如果没有啥变化,它可以继续稳定运行着,但如果发生点变化呢,谜底可能就不这么肯定了。在我入职的这两年里,系统承载的业务迭增变化:从单一的餐饮外卖到与新零售及品牌餐饮三方并行,又从抵家模式衍生至到店,随之而来的是业务连续不停的差异化定制,另有并行上线的要求。

另一面,随着公司组织架构变化,有的项目需要三地协同推进才气完成,相同协作成本翻倍提升。几方面联合起来,导致开发没有精神对大部门系统的演进都举行完善的计划。

几个月前,业务提了一个简朴的需求:对生意业务的评价做自动审核并举行相应的处罚。其时评价焦点“域模型”是这样的:设计自身的优劣这里暂不举行讨论,只是举例说明为了满足这个诉求,会涉及多个评价子模块要改动,开发评估下来的事情量远远超出了预期,业务方对此不满足,类似的冲突在其他系统里也经常泛起。但实际上,团队里没人偷懒,和之前一样努力事情,只是不管投入了几多小我私家时间,救了几多次火,加了几多次班,产出始终上不去,因为开发大部门时间都在系统的修修补补上,而不是真正完成实际的新功效,一直在拆东墙补西墙,周而往复。

为什么会导致这样的效果,我想应该是因为大部门系统已经演变到很难响应需求的变换了,业务认为的小小变换,对开发来说都是系统的一次大手术,但系统本不应该往这个偏向生长的,它和hardware有着庞大的区别就在于:变换对软件来说应该是简朴灵活的。所以我们思考设计的焦点目的:**“接纳好的软件架构来节约项目构建和维护的人力成本,让每一次变换都短小简朴,易于实施,而且制止缺陷,用最小的成本,最大水平地满足功效性和灵活性的要求”。

Source code is the design提到软件设计,大家脑壳里可能会想到一幅幅结构清晰的架构图,认为关于软件架构的所有秘密都隐藏在图里了,但履历过一些项目后发现,这往往是不够的。Jack Reeves在1992年揭晓了一篇论文《源代码即设计》,他在文中提出一个看法:高层结构的设计不是完整的软件设计,它只是细节设计的一个结构框架。在严格地验证高层设计方面,我们的能力是很是有限的。

详细设计最终会对高层设计造成的影响至少和其他的因素一样多(或者应该允许这种影响)。对设计的各个方面举行革新,是一个应该贯串整个设计周期的历程在踩过一些坑之后,这种强调详细设计重要性的看法在我看来很实在接地气,简朴来说:“自顶向下的设计通常是不靠谱的,编码即是设计历程的一部门”,小我私家认为:系统设计应该是从下到上,随着抽象条理的提升,不停演化而获得良好的高层设计。**编程范式从下向上,那就应该从编码开始审视,饿了么生意业务系统最开始是由Python编写,Python足够灵活,可以很是快速的产出mvp的系统版本,这也和其时的公司生长状态相关: 产物迭代迅速,新项目的压力很大。最近这次重构,顺应团体趋势,我们使用Java来举行编写,不外在这之前有一个小插曲:17年底,因为预估到当前系统框架在单量到达下一个量级时会遇到瓶颈,所以针对一些新业务逐渐开始使用Go语言编写,但在这个历程里,经常会听到一些言论:用go来写业务不舒服。

为什么会不舒服?大致是因为没有框架,没有泛型,没有try catch,确实,在解决业务问题的这个大的上下文中,go语言不是最优的选择,但语法简朴,可以极大水平的制止普通法式员堕落的概率。那么Python呢,任何事物都有双刃剑,虽然Python具有强表达力,可是灵活性也把许多人惯坏了,代码写的糙,动态语言写太多坑也多,容易堕落,在大项目上的工程治理和维护上有一定劣势,所以rails作者提到:“灵活性被太过高估——约束才是解放”也有一定原理为制止引起语言战,这里不外多讨论,只是想引出:我从C++写到Go,又从Python写到Java,在这个历程里体会到--编程范式也许是学习任何一门编程语言时要明白的最重要的术语,简朴来说它是法式员看待法式应该具有的看法,但却容易被忽视。

生意业务老系统的代码,不管是针对什么业务逻辑,险些都是OPP一杆到底,类似的代码在系统里随处可见。我们似乎完全遗忘了OOP,这项古老的武艺被淡化了,我这里不是说一定要OOP就是完美的,准确来说我是“面向问题”范式的拥趸者,好比,Java从骨子里就是要OOP,可是业务流程纷歧定需要OOP。一些生意业务业务就是第一步怎么样,第二步怎么样,接纳OPP的范式就是好的解法。

这时,弄很庞大的类设计有时并不须要,反而还会带来贫苦此外,同一个问题还可以拆解为差别的条理,差别的条理可以使用各自适合的方式。好比高层的可以OOP,详细到某个执行逻辑里可以用FP,好比:针对订单的金额盘算,我们用Go写了一版FP的底层盘算服务,性能高、语法简朴以及堕落少等是语言附带的优点,焦点还是因为该类问题自身适合。

然而,劈面向整个生意业务领域时,针对繁复多样的业务场景,合理运用OOP的设计思想已经被证明确实可以支撑起庞大庞大的软件设计,所以我们作出第一个决议:接纳以OOP为主的“混淆”范式。原则和模式The difference between a bad programmer and a good one is whether he considers his code or hisdata structures more important. Bad programmers worry about the code. Good programmers worry about data structures and their relationships. -- Linus Torvalds不管是接纳哪种编程范式、编程语言,结构出来的基础模块就像盖楼的砖头,如果砖头质量欠好,最终大楼也不会牢靠,引用里的一大段话,relationships才是我最想强调的:我明白它是指类之间的交互关系,“关系”的优劣通常等价于软件设计的优劣,设计欠好的软件结构多数有些配合特征:僵化性:难以对软件举行改动,一般会引发连锁改动,好比下单时增加一个新的营销类型,订单中心和相关上下游都要感知到并去做改动懦弱性:简朴的改动会引发其他意想不到的问题,甚至观点完全不相关牢靠性:设计中有对其他系统有用的部门,可是拆出来的风险和成本很高,好比订单中心针对外卖场景的支付能力并不能支持会员卡等虚拟商品的支付需求不须要的庞大性:这个通常是指过分设计艰涩性:随时间演化,模块难以明白,代码越来越难读懂,好比购物车阶段的焦点代码已经长成了一个近千行的大函数...接纳合适的范式后,我们需要向上抽一个条理,来关注代码之上的逻辑,多年软件工程的生长沉淀下来了一些基本原则和模式,并被证明可以指导我们如何把数据和函数封装起来,然后再把它们组织起来成为法式。

SOLID有人将这些原则重新排列下顺序,将首字母组成SOLID,划分是:SRP、OCP、LSP、ISP、DIP。这里针对其中几个原则来举些例子。

SRP(单一职责):这个原则很简朴,即任何一个软件模块都应该只对一类用户卖力,所以代码和数据应该因为和某一类用户关系精密而被组织到一起。实际上我们大部门的事情就是在发现职责,然后拆开他们。我认为该原则的焦点在于用户的界说,18年去听Qcon时,听到俞军的分享,其中一段正好可以拿来诠释什么是用户,俞军说:“用户不是人,是需求的荟萃”。在我们重构的历程中,曾经对生意业务系统里的交付环节有过争论,现在饿了么支持商家自配宁静台托管以及选择配送(好比跑腿),这几类配送的算价方式,配送逻辑,和使用场景都纷歧样,所以我们基于此做了拆解,一开始大家都认同这种剖析方式。

但厥后商户群体调整了,新零售商户和餐饮商户举行分拆,对应着业务方的运营方式也开始泛起差异,导致在每个配送方式下也有了差别诉求,陪同这些变化,最后我们选择做了第二次拆解。对于单一职责,这里有个小tips:大家如果实在欠好分析的话,可以多视察那些因为分支合并而发生冲突的代码,因为这很可能是因为针对差别需求,大家同时改了同一个模块。DIP(依赖倒置):有人说依赖反转是OOP和OPP的分水岭,因为在历程化设计里所建立的依赖关系,计谋是依赖于细节的--也就是高层依赖于底层,但这通常会让计谋因为细节改变而受到影响,举个例子:在外卖场景下,一旦用户因为某些原因收不到餐了,商户会赔代金券抚慰用户,此时OPP可以这样做:而过一阵子,因为代金券通常不能跨店使用,平台想让用户继续复购,就想通过赔付通用红包来挽留,这个时候就需要改动老的代码,通过增加对红包赔付逻辑的依赖,才可以来满足诉求。

但如果换个方式,接纳DIP的话,问题也许可以被更优雅的解决了:固然这个示例是简化后的版本,实际事情里另有许多越发庞大的场景存在,但本质都是一样:接纳OOP倒置了计谋对细节的依赖,使细节依赖于抽象,而且经常是客户拥有服务接口,这个历程中的焦点是需要我们做好抽象。OCP(开闭原则):如果仔细分析,会发现这个原则其实是我们一开始定的系统设计的目的,也是其他原则最终想告竣的目的,好比:通过SRP,把每个业务线的模块拆解出来,将变更隔离,可是平台还要做一定的抽象,将焦点业务流程沉淀下来,并开放出去每个业务线自己界说,这时候就又会应用到DIP。其他的几个原则就不举例子了,固然除了SOLID,另有其他类型的原则,好比IoC:用外卖生意业务平台举例子,商户向用户卖饭,一手交钱一手交货,所以,基本上来说用户和商户必须强耦合(必须晤面)。

这个时候,饿了么平台出来做担保,用户把钱先垫到平台,平台让商家接单然后出餐,用户收到餐后,平台再把钱打给商家。这就是反转控制,买卖双方把对对方的直接依赖和控制,反转到了让对方来依赖一个尺度的生意业务模型的接口。可以发现只要总结纪律,总会泛起这样或那样的原则,但每个的原则的使用都不是一劳永逸的--需要不停凭据实际的需求变化做代码调整,原则也不是万金油,不能无条件使用,否则会因为太过遵循也会带来不须要的庞大性,好比经常见到一些使用了工厂模式的代码,内里一个new其实就是违反了DIP,所以适度即可。

演进到模式这里的模式就是我们常说的设计模式,用演进这个词,是因为我以为模式不是起点,而是设计的终点。《设计模式》这本书的内容不是作者的发现缔造,而是其从大量实际的系统里提取出来的,它们多数是早已存在并已经广泛使用的做法,只不外没有被系统的梳理。换句话说,只要遵循前面叙述的某些原则,这些模式完全可能会自然在系统代码中体现出来,在《敏捷软件开发》这本书里,就特意有一个章节,形貌了一段代码随着调整逐步演进到了视察者模式的历程。

拥有模式虽然是好的,好比搜索系统里,通过Template Method模式,界说一套完整的搜索参数剖析模版,只需要增加设置就可以定制差别的查询诉求。这里最想强调的是不要设计模式驱动编程,拿生意业务系统里的状态机来举例子(状态机简直太常见了,简朴如家里使用的台灯,都有一个开和关的状态,只是生意业务场景下会越发庞大),在餐饮外卖生意业务有如下的状态流转模型:实现这样的一个有限状态机,最直接的方式是使用嵌套switch/case语句,简略的代码好比:public class Order { // States public static final int ACCEPT = 5; public static final int SETTLED = 9; .. // Events public static final int ARRIVED = 1; // 订单送达 public void event(int event) { switch (state) { case ACCEPT: switch (event) { case ARRIVED: state = SETTLED; //to do action break case } } }} 因为是简写了流程,所以上面的代码看起来还是挺能接受的,可是对于订单状态这么庞大的状态机,这个switch/case语句会无限膨胀,可读性很差,另一个问题是状态的逻辑和行动没有拆开,《设计模式》提供了一个State 模式,详细做法是这样:这个模式确实分散了状态机的行动和逻辑,可是随着状态的增加,不停增加State的类会让系统变得异常庞大,而且对OCP的支持也欠好:对切换状态这个场景,新增类会引起状态切换类的修改,最不能忍受的是这个方式会把整个状态机的逻辑隐藏在零星的代码里。

旧版的生意业务系统就使用的是解释迁移表来实现的,简化版本是这样的:# 完结订单add_transition(trigger=ARRIVED, src=ACCEPT, dest=SETTLED, on_start=_set_order_settled_at, set_state=_set_state_with_record, // 变换状态 on_end=_push_to_transcore)...# 引擎def event_fire(event, current_state): for transition in transitions: if transition.on_start == current_state && transition.trigger == event: transition.on_start() current_state = transition.dest transition.on_end() 这个版本很是容易明白,状态逻辑集中在一起,也没有和行动耦合起来,扩展性也比力强,唯一缺点的话是遍历的时间,但也可以通过字典表来优化,但它总体带来的利益越发显着。不外随着业务生长,生意业务系统需要同时支持多套状态机,意味着会泛起多个迁移表,而且另有凭据业务做扩展定制的需求,这套解决方案会导致代码编写变得庞大起来,我们在重构时接纳了二级编排+流程引擎的方式来优化了这个问题,只是不在我们讨论的规模内,这里只想强调第二个决议:代码上要灵活通过设计原则分析问题,再通过合适的设计模式解决问题,不能设计模式驱动编程,好比有时候一个全局变量就可以替代所谓的单例模式。富厚的领域寄义一旦你想解说美,而不提拥有这种特质的工具,那么就完全无法解释清楚了用个不那么贴切的说法,如果前面说的是针对静态问题的计谋,现在我们需要讨论面临动态问题的解决措施:纵然没有风,人们也不会以为一片树叶是稳定的,所以人们界说稳定的时候和变换的频繁度无关,而是和变换需要的成本有关,因为吹一口吻,树叶就会随之摇摆了。

我们除了要写好当前代码,让其足够清晰合理,还要能写好应对需求变化的“树叶”代码。面向业务变化的设计首先就是要明白业务的焦点问题,进而举行拆解划分为各个子领域,DDD--也就是领域驱动设计,已经被证明是一个很好的切入点。这里不是把它看成技术来学习,而是作为指导开发的方法论,成为第三个决议,而且我小我私家仍处在低级阶段,所以只说一些明白深刻的点。通用语言设计良好的架构在行为上对系统另有一个最重要的作用:就是明确的显式的反映系统设计的意图,简朴来说,在你拉下某些服务的代码的时候,或许扫一眼就可以以为:嗯,这个“看起来” 就像一个生意业务系统的应用。

我们不能嘴上在谈论业务逻辑,手上却敲出另一份容貌的代码,简朴来说,不能见人说人话,见鬼说鬼话。可以对比一下这两类分包的方式,哪一个更容易明白:发现领域通用语言的目的之一是可以通过抓住领域内在来应该需求变换,这个需要许多客观条件,好比团队里有一个领域专家。但没有的时候,我们也可以向内求解,**我有次看到一位在丁香园事情的法式员朋侪,购置了一大批医学的书籍,不用去问,我就猜他一定是成了DDD的教徒。

针对这个点,我们这次重构时还做了些让“源代码即设计”的事情:领域元素可视化,当系统领域内的一些观点已经和产物告竣一致之后,便增加约定好的注解,代码编译时便可以扫描并收集起来发送给前端,用于绘图。回到前面提到的评价域模型,厥后在和产物多次相同后意识到,产物没有希望评价这么多种类,对它来说商品也好、骑手也好,都属于被评价的工具,从领域模型来看,之前的设计更多是面临场景,而不是面临行为,所以合理的域模型应该是:限界上下文这个在我们平时开发历程中会很常见。拿用户系统举例:一个User的Object,如果是从用户自身的视角来看,就可以登陆、登出,修改昵称;如果是从其他普通用户来看,就只能看看昵称之类的;如果从后台治理员来看,就可以注销或者踢出登陆。

这时就需要界定一个Scope,来说明现在的User到底是哪个Scope,这其实就是DDD中限界上下文的理念。限界上下文可以很好的隔离相同事物的差别内在,通过严格规范可以进入上下文的工具模型,从而掩护业务抽象行为的一致性,回到生意业务领域,饿了么是最开始支持超级会员玩法的,为了支持对应的结算诉求,需要接入生意业务系统来完成这个业务,我们通太过解问题域来降低庞大度,这个时候就对应切割为会员域和生意业务域,为了掩护超会卡在进入生意业务领域的时候,不扰乱生意业务内部的业务逻辑,我们做了一次映射:切分当所有代码完成之后,随着法式增长,会有越来越多的人到场进来,为了利便协作,就必须把这些代码划分成一些利便小我私家或者团队维护的组。

凭据软件变换速度差别,可以把上文提到的代码化为几个组件:Extension:扩展包,这里存放着前面提到的业务定制包,面向工具的思想,最焦点的孝敬在于通过多态,允许插件化的切换一段法式的逻辑,其实软件开发技术生长的历史就是一个想法设法利便的增加插件,从而建立一个可扩展,可维护的系统架构的历程。Domain: 领域包,存放着具备领域通用语言的焦点业务包,它最为稳定。Business:业务包,存放着详细的业务逻辑,它和Domain包的区别在于,可能Domain包会提供一个people.run()的方法,他会用这个方法去跑着送外卖,或者去健身。

Infra: 基础设置包,存放这对数据库及种种中间件的依赖,他们都属于业务逻辑之外的细节。然后是分层依赖,Martin Flower已经提供了一套经典的分层封装的模式,拿简化的订单模块举例:然而如果有的同学制止做种种类型的转换,不想严格遵守分层依赖,以为一些查询(这里指Query,Query != Read)可以直接绕过领域层,这样就酿成了CQRS模式:可是最理想的还是下面这种方式,领域层作为焦点业务逻辑,不应该依赖基础设施的细节,通过这种方式,代码的可测性也会提升上去单体法式的组件拆分完毕后,再向上一层,我们开始关注四个焦点服务:Booking被分拆为Cart、Buy、Calculate,Eos被分拆为Procee、Query、Timeout,Blink一部门和商户订单相关的功效被分拆到Process、Query,和物流交付的部门单独成一块Delivery,最后生意业务的焦点服务拆解成下图:到现在,算上这个切分的方式,加起来一共就四个决议,其实也没须要分序列,它们焦点都是围绕着软件灵活性这个目的,从法式范式到组件编写,最后再到分层,我们主动选择或避开的一些教条限制,所以业务架构从某种意义上来讲,也是在某种领域中限制法式员的一些行为,让他往我们所希望的规范偏向编码。从而到达整个系统的灵活可靠。

"No Silver Bullet"“个体和交互胜过历程和工具”,敏捷宣言第一条现在系统架构是什么样子并不重要,因为它可能会随着时间还会拆解成其他容貌,重要的是,我们要认识到对于如何制作一个灵活的生意业务系统——没有银弹。如果仔细视察的话,会发现当前系统里仍有许多问题等着被解决。好比一些横跨型变换:系统链路里会因为某个服务的接口增加了字段,而导致上下游随着一起改。

更为尴尬的是,原来我们拆分服务就是为相识耦合,但有时还会泛起服务公布依赖的现象。系统演进是一场持久的战争,“个体和交互胜过历程和工具”,人才是胜利的焦点因素。

已往的两年里,我们没有停止过思考和实践,经常可以看到生意业务团队内部成员的争执,小到一个接口字段变换,大到领域之间的界限,大家为拿到一个合理的技术方案做了许多讨论,这让我想起《禅与摩托车维修艺术》里所提到的良质,有人点评说:关于良质,法式员可能有这样的履历——写出了一段绝妙的代码,你会以为“不是你写出了代码,这段代码一直存在,而你发现了它”。参考书籍《软件设计的哲学》--John Ousterhout《禅与摩托维修艺术》--Robert M.Pirsig《领域驱动设计》--Eric Evans《敏捷软件开发》--Uncle Bob《架构整洁之道》--Uncle Bob《极客与团队》--Brian W.FItzapatrick作者:盛赫。


本文关键词:真人,-重构,改善,饿了,么,生意,业务,系统,的,ag真人

本文来源:ag真人-www.hxtuzhuang.com