究竟什么是好的架构?这是我在实践代码整洁、应用重构和满足业务快速交付下常思考的问题,在本篇文章中我想从以下两个方面来简单谈谈我对 "好" 架构的看法,希望能对大家有所启发:
- 软件架构:这种架构表现的是代码级别,强调代码架构的分层
- 应用架构:这种架构表示的是应用级别,比如单体架构或微服务架构
软件架构
在近些年的工作中,我认为好的软件架构设计有以下几个特点:
- 代码易拓展:
也正是我们常听到的软件设计原则中的"可扩展性",一个好的架构应该能够实现业务与技术组件的分离,使开发者能够专注于业务流程,以填空的方式直接套用开箱即用的组件、框架和解决方案,不必进行大量的重复设计,易于拓展和编写
- 代码易复用:
一个好的架构应该有良好的分层,强调模块的拆分与封装,同层原子模块之间避免互相依赖和耦合,让上层系统能够轻松实现底层业务逻辑的组合复用;另外业务逻辑是建立在数据之上的,一个封装良好的代码架构在实现业务逻辑复用的同时应该有健全的数据模型维护和共享机制,避免对同一个数据对象的重复查询,并能够轻松通过批量操作降低系统的I/O负载
- 代码更健壮:
一个好的代码架构在面对多场景化的需求时,可以做到场景隔离,避免不同场景的特有逻辑之间的互相耦合干扰,出现一个场景需求上线后影响其他业务场景的问题;除此之外,一个好的架构应该通过设计良好的框架和标准模板在有益的程度上对开发者的编码行为进行约束,把规范框架化,而不是过多的依赖人和code review来实现规范统一
- 逻辑易传承:
在工作中曾多次尝试通过维护文档的方式来建立业务知识库,但都以失败告终。在这个过程中我们意识到业务功能都是由我们的代码承接的,它天然具备业务知识库的功能。因此一个好的代码架构不仅能够实现业务功能,而且要承担起传递业务知识的职责:当有新同学加入时,代码能够以最直接的方式帮助他快速建立起对整个业务的宏观认知,而进行具体的需求开发时,又能够按图索骥,快速定位改动点并深入了解其业务细节
接下来我想通过两种软件设计方法来阐述不同方法下定义的不同架构,以及我对不同架构的看法。
数据驱动设计
"数据驱动设计"指的是面向数据库编程,也是近些年工作中开发使用最多的软件设计方法,一般它的系统基于 MVC 架构设计。它最为突出的特点就是 "简单","快",没有过多的封装和设计,开发起来没有约束,如行云流水般,在业务简单且快速扩张使其能够快速实现功能,完成交付。在代码中,通常数据查询逻辑和业务逻辑交织在一起,随着业务不断地发展变得复杂,代码随之腐化,阅读和理解逐渐变得困难。比如,在团队内没有很好的代码评审机制,很容易让大家陷入"打补丁"式的开发中:每个新的业务需求都会在现有的流程中增加一个 if...else
分支,然后直接在新分支内实现业务逻辑。if
分支的层层嵌套,导致 代码逻辑圈复杂度不断飙升,本来应该通用的逻辑对不同场景的适配性越来越低 。渐渐的,我们发现新增需求的开发越来越"不简单"了:在试图复用一段看起来相似的代码逻辑时会有很多纠结和不尽人意的地方,对代码执行流程的认知也不似以往那么清晰了,为了防止对旧的业务流程造成影响,我们开始增加更多的 if
分支,这反过来进一步加剧了情况的恶化,重复代码不断增多,嵌套逻辑也更加复杂。还有一点,这种架构没有防腐机制,我想强调的是对外部组件(数据模型,外部接口等等)的强依赖,如果这些组件发生变更或者需要升级或替换,在这些组件没有提供向后兼容的机制时,往往对系统的冲击很大,需要花费大力气去改造。
但是这并不代表这种软件设计方法不好,对于简单又需要快速交付的业务来说非常合适。至于代码的可扩展和易复用性,要想保证这两点需要团队架构师在需求迭代的技术评审和代码评审中进行纠偏和控制,而逻辑的易传承性实际上并不能在这种架构中很好的保证,因为各个业务逻辑会分散在多个不同的 Service
中,没有划分统一的位置。
领域驱动设计
"领域驱动设计"区别于"数据驱动设计",它通常是自上而下设计系统,由业务来驱动:业务驱动领域建模,领域建模再驱动软件开发设计,不再是表结构先行的设计方式。精准的领域划分,需要业务、产品和研发的共同参与,这样能够保证代码真实地反映并解决业务的核心问题。
领域的划分反映到代码层面会对应不同的 聚合 ,在本篇文章中我们不教条地引用 DDD 中关于聚合的定义,它的含义可以通俗的理解为:一组关联密切且关系明确的实体或值对象的集合,一个聚合通常会支撑着一个功能极其内聚的上层业务模块 。DDD 中聚合中会定义为一个 聚合根 对象,聚合根是整个聚合中实体操作的中心,聚合中的全部实体都可以通过聚合根直接或间接的访问到。比如在订单领域中,订单 Order
实体便是一个聚合根。根据业务流程设计的实体不再与数据模型中底层的库表一一对应,而是从业务本质出发完成实体划分和实体的定义,在聚合中体现实体之间的关联关系,完成数据模型和业务模型的分离。
领域驱动设计一般会采用六边形架构设计,核心是领域层,它不依赖任何其他层,只会依赖抽象,实现则由基础设施层去完成,是 "依赖倒置" 设计原则的体现。这样的好处是使 代码更健壮 ,因为这样的设计很好的实现了 防腐 ,避免外部组件或服务的变更影响核心逻辑,并且调整起来只需要改动基础设施层,相对容易完成。并且,如果扩展新的实现(如原有为 MySql,现在需要扩展 Oracle)也非常容易,只需要在基础设施层针对领域层的抽象提供新的实现即可,体现了 代码易拓展 的目标。当然,代码的扩展性并不仅仅体现在"如此大"的基础技术组件的扩展上,还体现在业务逻辑的扩展上,比如说增加了新的计费场景等等,需要在原有逻辑上改动,那么这就 需要合理使用设计模式 来增加扩展性,这一点在领域驱动设计和数据驱动设计中都需要注意。
还有,当有新同学加入时,可以通过阅读聚合代码来了解模型之间的关联关系,不再需要从代码中四处搜集蛛丝马迹,通过阅读领域层定义的业务逻辑来快速熟悉业务,这也体现了架构目标中 "逻辑易传承" 的要求。一般情况下,采用领域驱动设计的架构会将业务逻辑都写在领域层中,在应用层中仅仅是对业务逻辑进行编排,所以能够通过阅读领域层代码来深入理解业务,不过这一点需要在团队内达成共识,并在代码评审中进行约束,否则业务逻辑很容易溢出到应用层中,造成业务知识的分散。
总结
软件设计中并不存在所谓的银弹,即便是领域驱动设计的方法提供了以上种种好处,但是它的应用场景还是相对有限:它更适合于较为复杂和成熟的业务,并且团队对业务有较为深入的理解,针对业务的发展有较为清晰的方向。
首先,领域的划分就是对团队成员的考验,在执行该软件设计方法时,会使用不同的方法(用例分析、四色建模或事件风暴等)来识别和划分领域模型,在这一点上便会花费较长的时间。如果领域划分的不不清晰,将影响代码设计和开发并持续影响后续的迭代过程。
其次,领域驱动设计的迭代速度相比于数据驱动设计缓慢,对于想要完成快速迭代、交付的业务团队可能并不适合。实践过领域驱动设计的开发者可能会能够理解,在这种架构设计下,为了完成代码的防腐,在各个层之间需要不断地对类型进行转换,并且开发过程中也要遵守团队内既定的原则,开发起来并不"自由"。
在以上内容中,我并没有将"代码可读性"作为衡量好架构的标准,我觉得这一点更多的是由开发者的个人素养来决定,在统一代码风格上花费力气可能得不偿失,但坚持写好代码和在团队内建立"写容易读懂的代码"的原则总是没错的。
应用架构
在业务发展的早期,应用都相对较小,一般采用的是单体架构,在这种架构下应用开发简单,易于对应用程序进行大规模的更改,并且事务控制相对容易,部署起来也非常省事儿。随着业务不断地发展,单体架构逐渐发展成一个"单体巨无霸":复杂性不断增加,开发者很难理解它的全部内容,修复问题和添加新的实现会变得困难且耗时,并且这会引发恶性循环,每次迭代变更都会使代码库变得越复杂,开发起来出错的概率也随之增加,交付速度越来越慢。
在这个时候单体应用需要由单体架构切换到微服务架构。对于微服务架构的描述,在《微服务架构设计模式》这本书中,它对微服务架构的介绍很有启发性,所以我也在此处引用一下:
文中说扩展一个应用程序可以从 X, Y 和 Z 轴三个维度展开:
- X 轴扩展:通过多个实例实现请求的负载均衡。这也是我在工作中最常见的方法,用于抗大流量的服务会部署上百至几百台服务实例,通过负载均衡器将请求分散到多个相同的实例上
- Z 轴扩展:根据请求的属性路由请求。Z 轴扩展也需要部署部署多实例,但是每个实例只负责处理部分数据范围内的请求,比如在多个实例下,实例1 负责处理用户a ~ h的请求,实例2 负责处理用户i ~ p的请求... 通过路由器将请求路由到适当的实例
X 轴和 Z 轴扩展有效地提高了应用的吞吐量和可用性,但是这两种方式并没有解决上文中提到的应用复杂性的问题。为了解决这些问题,便需要采用 Y 轴扩展,也就是 功能性分解。
- Y 轴扩展:根据功能把应用拆分为服务 。"服务" 本质上是一个麻雀虽小但五脏俱全的应用,它实现了一组相关的功能,比如订单管理、客户管理。服务可以在需要的时候在 X 轴或 Y 轴方向上进行扩展。比如,订单服务可以被部署为一组多实例负载均衡的服务。
所以对于微服务架构的定义是:把应用程序按照功能性分解为"一组服务"的架构风格 ,它的"微"不是强调服务要足够小,而强调的是 每个服务都是由一组专注、内聚的功能组成。
在微服务架构中对服务的划分,可以是上文中在单体应用中,将专注、内聚的功能性拆分为微服务;也可以借助领域驱动设计的方法,按照领域来帮助识别微服务划分的粒度,最终根据职责进行微服务的划分,比如在我当前工作的延保业务中,大的领域划分是"营销域、交易域和投保域",而在每个大的领域下又有更细的划分,比如在交易域下,则是多个应用的划分,一个负责承接流量、校验和执行业务逻辑等,在这个应用中并不直接连接数据库,由它来提供统一的对外服务,也包括缓存和布隆过滤器服务都是在这个应用中完成的,处理完校验和业务逻辑后,通过消息来通知后置的服务消费;一个应用直连数据库,提供内部应用调用的较为通用的查询接口以及负责消费前置应用发送的消息,将订单等数据落库,等等。
同样地,这两种架构并无好坏的区分,单体应用架构有它的好处,在业务很简单的情况下迭代快且易于维护;而微服务架构更适合较为复杂的业务场景,划分多个微服务来运转业务,相比于单体架构开发和维护要复杂得多:服务划分和定义是最先的挑战,至于后续的进程间的通讯问题、分布式事务问题和微服务的部署问题相比与单体应用要复杂得多。
这还仅仅是技术上,在组织、开发和交付流程上也需要做一些工作:将开发团队划分为一系列小团队,每个团队足够小,一般在 8 ~ 12 人,通常以敏捷团队的形式。每个团队负责开发和维护多个服务,这些服务提供了一个或多个业务能力,各个团队可以独立地完成开发、测试和部署任务,而不需要频繁地与其他团队沟通或者协调。
总结
架构的重要性在于它影响了应用的 非功能性需求 ,或者称其为 质量属性。在不同的场景下选择合适的架构至关重要,它不仅会影响系统的稳定性、性能和可维护性等,还会直接关系到需求的迭代速度、团队的协作效率以及最终产品的用户体验。
因此,在选择架构时,需要综合考虑项目的具体需求、团队的技术能力、项目的发展阶段以及未来的扩展性等因素。没有一种架构是万能的,适合的才是最好的。同时,随着项目的发展,可能需要从一种架构逐渐过渡到另一种架构,这要求团队具备灵活应变的能力和持续优化的意识。