当前位置: 首页 > 科技观察

架构漫谈:如何从架构的角度写出好的代码

时间:2023-03-17 17:45:07 科技观察

软件架构其实包括:代码架构,以及承载代码运行的硬件部署架构。实际上,硬件部署架构最终是由代码的架构决定的。由于代码结构不合理,不可能将一个操作单元拆分成多个,所以硬件架构能够拆分的非常有限,最终整个系统也很难做大。所以我们经常听到,重写代码,推翻原有架构,重新设计等等,来说明架构的演进。这实际上是为了完成任务而没有充分考虑任务的结果。这不是架构演进的问题,而是个人对问题域的理解逐渐深入的过程。所以有必要再讨论一下代码结构应该是怎样的。本文将在前几篇文章的基础上进一步探讨如何将架构思想落地并提炼到我们的代码实践中,尽量不让代码成为系统增长的瓶颈,降低架构拆分的成本。前面我们提到,软件其实就是对现实生活的模拟,虚拟化。这是一个很重要的前提,它直接决定了我们的代码应该分成多少部分。结合各个部署单元的职责,可以明确分为两种不同的职责:表达业务逻辑的代码。许多人称这部分为领域逻辑,或领域模型。这部分其实来源于生活,一定要符合现实生活,不能人为抽象。为用户提供访问权限并保存运行业务逻辑结果的代码。计算机的状态保存存在缺陷。将业务运营结果保存在本地存在很大问题。一般保存在外接存储设备上,也便于扩展。因此,单个部署单元的代码可以分为两部分,如下图所示:从图中可以看出,软件代码的相关利益是运行时的访问人员和存储设备。业务的代码最复杂,需要服务三方,编码人员的负担最重。为了尽量减少这三方的变更对服务的影响,必须将服务进一步分为三部分,让每一部分都可以独立变更,这样三方的变更才不会产生连锁反应并降低服务成本。成本。如下图所示:这样划分成几个职责:Service以用户的需求为中心,结合GlueCode提供的服务来完成需求。GlueCode专注于编写业务调用,管理Business中对象的生命周期,并通过Repository保存或加载Business的状态。业务侧重于实现业务的核心模型。Repository侧重于数据存储,与存储设备一一对应。大家注意,还是树结构。而左边的主要是需要计算机的相关理论知识,必须直接面对用户的需求。越靠右边越需要面对业务的核心。这些部分的开发人员只要相互讨论接口定义,就可以并行进行这些部分的开发,大大提高了开发效率,缩短了开发时间。做好这几部分,还需要注意逻辑只允许存在于业务中,业务逻辑中不允许存在Service、GlueCode、Repository。为什么?首先,我们来看看什么是业务逻辑。什么是业务逻辑?首先,这个定义的前提是指软件代码中的逻辑,而不是现实生活中的逻辑。在软件代码中,不需要缩进和计算的顺序调用,包括以捕获异常为目的的缩进代码,都不算逻辑,其他都是逻辑。下面使用严格的顺序调用来指代这种代码。因为顺序调用是计算机的一个特性,是由编译器决定的。当然,最本质的是我们计算的基础是图灵机。在现实生活中,顺序调用也是逻辑,所以不要把它们和我们这里说的业务逻辑搞混了。为什么除了业务代码,其他地方就不能有逻辑呢?我们分别对每个部分进行分析:如果服务的调用顺序不严,有很多分支,说明该服务做了两件或两件以上的事情。事情。这个服务一定要拆分,保证每个服务只做一件事。因为如果不这样拆分,一旦服务的某一部分发生变化,其他部分的执行肯定会受到影响。确定具体影响的沟通成本非常高,其他利益相关者没有合作的动力,我们通常不会投入精力进行仔细评估。最后上线的时候会出现很多不可预知的问题,最终导致用户利益的损失,肯定会导致返工,损害自己的利益。如果有计算逻辑,比如收益计算、订单金额计算等,那么这部分应该由业务代码来完成,不能由服务代码来实现。如果不按严格顺序调用GlueCode,就会遇到和服务一样的问题。如果没有严格的顺序调用Repository,包括访问存储的代码(比如SQL),就会导致逻辑进入存储设备。存储设备的主要用途是存储。一旦成为逻辑计算的主体,存储设备将无法通过增加机器来横向增长。这个时候没有架构,只能换一台性能更好的机器。这称为放大。只有横向扩展才能被认为是架构。以上种种都会导致架构无法快速横向扩展和拆分,增加修改成本,不符合开发者和商家的利益。这样做有什么好处?Service、GlueCode、Repository中的代码调用顺序严格,所以这些代码只需要测试连通性,不需要进行单元测试。因为这些代码需要处理很多上下文,所以很难做单元测试。这才是真正的结合。业务不访问任何上下文,不访问任何具体设备,所以这部分代码非常容易写单元测试,单元测试必须100%覆盖。因为其他地方没有业务逻辑,一旦出现问题,可以断定是Model的问题,单元测试肯定能找到。如果单元测试没有发现问题,那么一定是单元测试出了问题。线上问题的模拟变得很简单,单元测试可以进一步补充。Repository可以根据存储设备本身的最小访问粒度轻松完成工作,比如DB,完全可以实现单表访问。因为此时存储设备只关心访问数据,与业务无关。拆分表也很容易。存储设备可以通过增加机器进行水平扩展。很多人担心如果没有join,会不会访问DB的次数变多,导致性能下降?根据目前的网络情况,网络访问和DiskIO访问之间的差距并不大。多次访问数据库不会导致此问题。另外,如果有多个DB,也可以通过并行来加速访问。由于Service、GlueCode、Repository代码简单,我们的开发人员可以花更多的时间研究业务。毕竟这部分才是软件真正服务的。我们再来看一个实际的例子,如下图:Manager类其实就是GlueCode。有几点需要注意:业务模型不能被视为数据对象。Model关心的其实是业务行为,数据只是这些行为的结果。因此,GlueCode需要将Model转换为Entity,Entity在存储设备中与存储粒度一一对应。比如在DB中,每个Entity对应一张表,随着表的变化而变化,从而保证存储的变化不会影响到Model。同样,Service和用户之间的数据交互也不会和Model相关,保证用户需求的变化不会影响Model。因为用户的需求变化最频繁,没有逻辑,这让我可以快速满足业务需求。在Service这里,最好不要考虑代码复用。因为当多个不同的角色访问同一个接口时,一旦某个角色的需求发生变化,就会需要开发人员进行修改。而这种修改往往会影响到其他角色,而这些角色需要配合才能确定是否受到影响,但是这些角色往往因为没有需求而不配合。这给开发者造成了很多不必要的沟通,成本非常高。它最终会导致在线错误并影响最终用户。所以尽量给不同的角色不同的服务,避免重用,降低沟通成本。很多人会说这样服务太多了?这样就出现了服务注册、搜索等管理需求,服务管理中心就是来解决这个问题的。因为Service里面没有逻辑,所以开发和管理都非常简单,可以快速响应业务变化。只有更快、更容易地改变,才能更好地应对变化。必须重用业务模型。一旦复用出现问题,就意味着BusinessModel的识别出现了问题。这是一个信号,表明我们需要重新考虑该模型。BusinessModel必须是一棵完美的树,如果不是,也说明Model的识别有问题。在实际运行中,Service、GlueCode、Repository是不能有逻辑的。其实这和很多人的观念相冲突,认为这根本做不到。这样做需要很大的学习成本,但是绝对可以做到。当发现做不到时,可以断定是业务分析有问题。比如不该合并的合并,不该统计的统计。一定有办法解决这个问题,不做也是有理由的。无非是想早点完成自己的工作。虽然一开始会比较难,但是一旦有了这个观念,开发的质量和效率就会立马提高好几个档次。我的游泳教练曾对我说过这样的话,我至今还历历在目:“业余游泳运动员,越是想浮出水面,越是想把头抬起来,身体反而往下沉。唯一的克服恐惧的方法是把头在水里往下压,让身体浮出水面。真正专业人士的习惯往往与我们的日常行为相反。”如果我们真的想快速完成代码工作,就必须克服对时间的恐惧,真正去研究业务问题,利益相关者的利益,并养成这样的习惯。写代码的时候,在该出现逻辑的地方让逻辑出现,在不该出现的地方阻止。一旦逻辑出现在一个不该出现的地方,你必须马上意识到这个地方是个坑,这个问题肯定和业务分析不完整有关。很多人可能会将这种做法与MartinFowler提出的充血模型和贫血模型进行比较,与领域驱动设计进行比较,其实大可不必。这种分裂完全来源于软件解决的问题和软件架构。在很多地方,两位前辈的观点是一致的,但又不完全相同。以上只是对单个Service部署单元的分析,其他部署单元展开时类似。每个单元的下层可以认为是Repository,每个单元的上层可以认为是User。这些实践在我自己的项目中都有使用,非常有效,迭代速度也非常快。很多人担心商业模式建不好。其实没关系。可以一开始比较粗糙,以后可以慢慢完善。这种架构隔离了各部分变更对其他部分的影响,变更成本在可控范围内。