从顶层视角解读技术架构设计:实用指南与最佳实践
本文将从顶层视角出发,深入剖析架构的本质,让你明白各种架构图的实际用途。文章主要介绍了技术架构的设计思路,包括如何使技术架构适应产品架构的演进,以及如何设计功能模块和代码模块。同时,也介绍了软件系统的设计,包括分层、模块设计以及数据流模型设计等方面,并特别强调了灵活性和优化的理念。 |
---|
前言
在工作中,我们经常需要进行前端的架构设计以满足各种系统需求。在学习 DDD(领域驱动设计)之后,我深受启发,于是不断地在各种系统需求中进行实践并归类。本文将尝试总结各种需求的架构经验和模式,以此为基石,指导并提升我的工作方法。
本文由总到分,由粗到细,解释了作者眼中架构的由来发展到具体的设计经验。
架构前言浅谈
架构存在的意义?
我们世界的本质就是由信息组成的,我们很有可能生活在虚拟世界中。在人类创造的事物中,一切都围绕着人类的基本欲望进行创造和发展。因此我认为,软件和架构的本质是对人做事进行管理的系统工程。当人做事时遇到违反预期、违反欲望的阻碍时,在架构和产品上就会进行优化解决。
所以分析架构和建设架构需要从系统各方的需求出发。
架构的发展?
根据上述观点,架构的发展是为了满足人类的期望。在一个系统中,有不同角色的人参与,他们有不同的需求。在动态平衡的状态下,就形成了一个平衡的架构。因此,架构可以被描述为"角色(人)+产物(满足不同角色的人的需求)"。因此,架构的发展可以被看作是动态平衡的演变,根据各种趋势可以预见架构的演变。
例如,系统的使用方要求实现万物互联,因此系统架构朝着微内核、高扩展性的方向演变。而该内核的内容就是实现万物互联的基本要素,即网络库和操作系统内核相关的代码。不同需要联网的电子产品,在该内核的基础上,再组合所需的库或插件,完成整体的联网功能和自己领域的功能。
对于人们日益增长的需求,人与系统之间的数据传输速率需要越来越快,从系统中获得的信息质量也需要越来越好,因此才会有文字→图片→视频→3D→脑机接口的发展历程。每一次迭代都极大地提高了人与系统交互的带宽。
架构的基础抽象
根据上面的说法,架构的基础抽象就是 角色 + 系统(不同角色需求平衡下的产物)。再细化一点,软件中有消费者、生产者、开发者,这些基本角色。
对于消费者和生产者,他们直接操作的可能就是UI界面,这部分涉及到产品架构,由开发者中的PM、UX来构建,这个领域内,如何理解和满足用户需求是最重要和基础的。但是不在本文讨论范围内。
对于开发者角色中,研发是直接构建和实现UI以及数据存储的人群,本文旨在讨论这部分的内容。
所以先有用户需求,产生PM视角的产品架构,然后研发再由产品架构,产生技术架构。提问:产品架构还会产生什么领域的架构?
- 组织架构
- 数据架构:确定如何组织、存储、管理和保护数据。
- 安全架构:确保产品的安全性,包括身份验证、授权、加密和其他安全机制。
- 网络架构:涉及产品的网络通信,包括服务器、负载均衡器、网络拓扑等。
- 股权架构
- 合规架构:确保产品遵守相关法律法规和标准。
- 营销架构:营销策略与产品架构的协同,确保市场定位和推广效果。
- ...
总之架构的本质其实还是人的组织,人的不同组织架构会影响不同的领域架构。 比如人事组织架构会影响产品架构,或者产品架构也可以影响组织架构。我们从不同公司的组织架构都可以映射到他们的产品架构。
图片来源:bonkersworld.net/organizatio... |
---|
技术架构的范围
我们从基础抽象中得知,技术架构是由产品架构产生的。一个产品的实现需要满足基本需求,包括运行时、产物和数据。
作为项目经理(PM),需要定义产品的运行状态和信息需求。研发团队则负责将产品架构转化为系统、数据,并进行部署和维护,使其能够在运行时正常使用。用户直接使用的是运行中的系统。
因此,我们在技术架构讨论中关注的领域包括:如何组织和编写我们的产物、如何维护我们的运行时、以及如何定义数据的生产、消费和存储协议。
技术架构的设计目标
需适应产品架构的演进
技术架构需要适应产品架构的演进,这是最基本的一点。例如,能够以更低的成本快速进行产品迭代,或者支持承载超大用户流量等。
在设计技术架构时,我们的起点是产品的架构。除了基本实现产品的架构,还需要考虑架构能否随着产品架构的演变而变化。可以从人的几个维度来解释产品的基本演变趋势。根据之前提到的架构基本抽象和角色,我们可以得出以下几点:
- 产品使用者规模和角色:用户规模的趋势和用户需求的演变,为了满足用户需求和优化维护运行时的稳定,我们需要对整个系统的架构进行调整。例如,从最初的抖音到剪映、单机房到多机房等。
- 开发者规模和角色:随着系统的扩大,开发者数量也会增加,需要提高开发效率,因此也需要进行调整。例如,从最初的大服务到微服务。
技术架构的设计思路
我们做架构设计前,一定要做好竞品产品分析,了解他们的产品架构,这样对我们设计技术架构能提供更全面的思路
功能模块设计:先粗后细,再由细到粗
功能模块是对产品运行时各个系统的抽象划分。划分模块其实也是划分出做事的团队,和管理的上下文范围。 |
---|
我们在设计系统时,不太好全面的分析产品的功能,这个时候我们可以通过各种用户故事利用DDD的流程进行分析。
这块我直接按照DDD的领域建模流程来说明:
- 首先,需要根据产品架构,梳理出大概的模块。
业务产品研发,根据PRD、用例、获得对产品的认知。 |
---|
例如:
我们提供一个开放平台,提供如下功能
-
然后,梳理事件风暴,梳理出产品的业务模块(每个模块维护的人数 不超过"两个披萨团队"。"两个披萨团队"得名的由来,是因为团队的成员很少,只有6-10人,用两个披萨就能喂饱他们。"两个披萨团队"最重要的不是规模,而是它的"适度职责"。)。
- 事物出发为事件点,我们基于对业务的主观了解,先列出一系列业务的状态、参与者、系统等,他们刚开始是无序的。
- 点成线:可以将业务状态和事件链接归类为不同的领域事件和参与者,进行初步的分类。
- 线生面:务状态和事件有各种组合,在业务逻辑下,我们推演、组成了各种领域事件发生过程:触发参与者、命令、事件、结果状态。 如下面事件风暴的图
- 根据事件风暴梳理领域实体图
- 最后根据领域实体图得到总体的业务领域模块设计和其依赖
上面的流程演示了一个DDD的对系统建模的流程,它主要明确了运行时的功能模块设计,这部分架构是和产品架构强相关的,也是后面几种架构的基础。
可以想想为什么是强相关? 产品架构往往指向系统运行时各种模块的交互逻辑,而此技术架构也是描述的系统运行时的模块关系,所以是和产品架构直接相关的。
需要注意的是,DDD架构不一定适用于所有项目,在功能模块的设计中,我们主要能根据复杂度,得出团队内认可的模块图就可以,不一定需要走事件风暴等比较耗时的流程。
代码模块设计:打横还是打竖
代码模块设计是对静态文件的结构设计。 |
---|
为什么要对代码文件结构进行设计? 其本质原因是代码更改的合作流程,也就是GIT,我们将维护一个模块所需修改的文件范围缩的越小,上下文越集中,越能有助于我们快速安全的进行代码迭代。
为什么要打横和打竖?
打横就是分层。对于事物发展过程,我们可以 以不变和变来划分, 不变的部分会固化,变化的部分会流动。比如大浪淘沙,轻沙子跟着水流动,重沙子沉底筑基。同时可以类比自然界的发展过程,水流可以冲刷基地改变固化的河道。我们的模块设计也是需要遵循类似这样的自然的过程。在设计方案前,一定要对业务中信息的可靠度和变化程度进行分析,越可靠变化越少的信息,一定作为核心实体和层。
比如:维度和指标的关系,当维度不可靠时,指标为核心实体维护,当指标匹配不同维度时,由指标绑定规则引擎来匹配维度。当指标不可靠时,维度作为核心实体维护,最后就是像meego那样对指标进行分组查询。
因此,打横就是类似于泥沙沉底分层的过程,也就是隔离变化快的部分和不快的部分,这样可以提高总体系统的可维护性和稳定性。
在软件系统中,变化慢的大概有工具层、领域层、变化快的有适配层和应用层,我们将核心的变化慢的放内层,变化快的放在外层,这样就有了DDD的层级架构。这里面有个原则,就是外层只能单向依赖内层,内层不能依赖外层。否则就失去了分层的意义。
打竖就是分业务功能模块,在只打横的基础上,我们的各种业务模块如果都放在一起,则可能面临分工不明确、文件太臃肿的问题,所以在分层的同时,也需要对太臃肿的模块进行打竖,以进一步提高可维护性。但是如果只打竖的话,则就没法很好的隔离变化,使得系统有更好的扩展性。
所以总的来说,我们当需要隔离变化和整治依赖的时候需要进行分层 ,当需要更好的分工的时候需要打竖。需要注意的是打横还是打竖是分不同情况来设计的,不能按照一个静态的模板来设计。比如,我们预测到系统会在某个方面扩展,则我们需要保障在扩展时,新模块和旧模块不存在层间耦合。
比如下面的一个例子,当我们分层过多,将一个竖向的模块强行拆分到各层级中,这样接入新模块的时候,会存在横向层间耦合,会不好扩展。
改进如下:将模块打竖,这样新模块接入时,可以以很小的成本进行扩展。
如何进行设计:
对于一个系统来说,运行时的接口或者UI,对应不同的模块,这个模块有多深就是它涉及的层级,有多宽就是它在层内的涉及的文件有多少。
在设计系统时,我们可以从横向和竖向两种思路进行设计,如果我们的系统对外部变化很灵活,变化速度快,则我们应以横向进行切入,优先隔离变化,进行分层设计。而竖向的设计思路比较简单,可以单纯分模块分接口分服务,都可以分出来。比较难的一点是分多少层,一层有多厚?
在上述的功能模块设计中,我们明确了系统的领域范围和领域模块,接下来我们可以全面审视所有模块的深度,并根据变化和不变的原则进行分层切割。以常见的前端架构为例(以下架构分析仅为推测):
当我们实现一个 UI 页面时,前端会将其拆分为多个组件模块。以抖音的前端架构为例,我们可以按照入口组件向下划分,包括组件、逻辑和基建交互。然后,我们可以横向划分,根据变化度进行隔离,将每个模块划分为基建层、逻辑层和视图层。逻辑层和视图层用虚线表示,因为我们不一定按照层级将竖向的模块拆分到各个层中,而是根据是否是通用的组件或逻辑来考虑进行分层。至此,整个页面的分层和模块结构已经清晰明了。也有一些模块比较浅显或通用,比如按钮模块。实线会导致模块被文件实际分割,比如基建层不包含在模块文件夹下,而是基建层文件夹下。
我们在设计中遵循的一个原则是尽量简化业务,分层尽量少。因为如果分层太多,会导致模块过于破碎,增加了上下文的复杂性,从而降低业务开发效率。我们可以看到,每个模块层之间基本没有耦合,也就是说每个模块相对独立,那么其实可以合并为模块层和通用层即可。如果每个模块中都依赖一些通用的业务逻辑,则可以考虑将这些逻辑抽离出来形成一个独立的层。 总之,抽层的本质就是隔离变化。
后续我们按照横竖分文件夹,需要注意的是,我们需要按照前端团队熟悉的范式进行名称设计,否则会导致上下文同步效率低下,使得成员无法顺利交接和融入架构。横向设计提现在文件树顶部层级,竖向体现在文件树叶子结点部。
这个架构适合大部分前端页面的架构,对于微前端来说,模块层可以是 pages 或者 apps,我们可以简单直接地按照模块和通用进行分层,模块内部再进行分层。由于大部分前端页面的模块都是相对独立的,因此对于系统的发展来说,就是对页面模块的维护。这样的架构将更改的文件范围集中在模块范围内。如果强行按照视图层和逻辑层对模块进行实际分割,那么模块的上下文也会被文件分割,反而会影响维护。
再举个需要分层的例子:一个低代码前端项目至于插件系统
在一个基本的低码编辑器中,由可视化编辑器、物料库、属性面板、逻辑面板构成,我们单纯按照模块依赖来看,各个模块之间有相互依赖的时候, 那就是需要分层的意思了,我们将公共依赖往下沉,设计核心层,为Schema编辑逻辑和物料资料的使用逻辑等。 通过分层,这样就解决了依赖混乱的问题,外层统一依赖内层。
后续业务演变,我们发现各个模块之间对不同模块加载的时序有依赖,比如属性面板模块必须要等编辑器模块和物料模块加载好后再加载,否则无法拿到选中的物料的属性数据。 这种也属于相互依赖,我们都考虑做一些能力到内层。 比如我们可以将这些功能作为插件,然后做一个插件管理器,来管理顺序,同时也带来了可插拔的特性。
解决和维护依赖分层的常用手段:
依赖倒置:这是一种软件设计原则,其中高层模块不应该依赖于底层模块,而是应该依赖于抽象接口。这有助于减少模块之间的耦合,并使代码更易于测试和维护。
-
依赖注入:这是一种将依赖项(例如服务、数据访问对象等)注入到应用程序中的技术。这有助于提高代码的可维护性和可测试性,并使应用程序更易于扩展。
-
服务定位器模式:这是一种用于在应用程序中定位和获取服务的模式。服务定位器模式将服务的创建和使用分开,从而使应用程序更易于扩展和维护。
-
控制反转:这是一种将应用程序的控制逻辑从具体的实现中分离出来的技术。通过使用控制反转,应用程序可以更容易地适应不同的需求和环境。
-
抽象工厂模式:这是一种用于创建不同类型对象的工厂模式。抽象工厂模式将对象的创建和使用分开,从而使应用程序更易于扩展和维护。
-
外观模式:这是一种用于简化复杂系统的模式。外观模式提供了一个单一的接口来访问系统的不同部分,从而使系统更易于使用和理解。
-
策略模式:这是一种用于封装不同算法或行为的模式。策略模式将算法的实现和使用分开,从而使应用程序更易于扩展和维护。
-
桥接模式:这是一种用于将抽象类和具体类分离的模式。桥接模式将抽象类的实现和使用分开,从而使应用程序更易于扩展和维护。
-
观察者模式:这是一种用于通知对象更改的模式。观察者模式允许对象在发生更改时向其他对象发送通知,从而使应用程序更具灵活性和可扩展性。
-
命令模式:这是一种用于封装命令的模式。命令模式将命令的执行和使用分开,从而使应用程序更易于扩展和维护。
这些模块也就是插件层了,经常迭代的插件能力也被分层隔离其中。往往当核心大改的时候,这个产品也会整体改变。同时可以预见到,物料和插件是经常变化的,所以对于物料模块内肯定也会有其核心层和适配层以适应环境。
再举个微服务的例子,为什么微服务要做六边形架构,我们运行的程序要实现其稳定性,需要隔离外部经常变化的环境,比如数据库连接方式发生变动、上下游服务变更等,所以最基础的就是将这种和外界交互的东西分层出去,也就形成了六边形架构。将外界交互的部分在适配器实现,然后适配器就是将外界的信息映射为自己领域内。
以CQRS举例,一个业务系统经常读多写少的时候,如果每次新增读操作,都需要被层硬性分割的话,则使得构建一个读请求变得繁琐。 所以我们才打竖,将读写的架构分离,得到读架构和写架构,经常写后端同学都知道,读逻辑比写逻辑少考虑很多东西,所以读架构也会比写架构自然的少了一些领域逻辑的校验和构造,提高了写逻辑的开发效率。 这也是当层级影响了扩展和维护效率的时候,进行打竖的实践。
所以在明显的可以预见的快速变化的模块,互相依赖的模块,则可以分层,将变化和依赖隔离。不然,我们可以先按照模块打竖之后,再分层,这样可以让模块设计尽量的简单。在发现某个被层级强行分割的领域或者模块,需要经常维护或者变更的时候,再进行打竖,提高这个模块的开发维护效率。
所以模块设计的哲学,就是研究如何打竖还是打横,精确的隔离出变化域,提高总体的维护开发效率。
为什么要分层
效仿自然:
在自然界中,分层是一种自然现象,比如降雨带分层,导致的气候分类、植物种类分类等,我们为了研究庞大的自然系统,会对各种生物、地理进行分层。
所以,分层分类是一种人类研究系统的基本方法,分层的意义就是将庞大系统拆开来看,这样更容易产生领域的认知,缺点就是缺乏对总体的认知。
软件行业中的分层:
软件行业属于服务业,它的生产资料就是人,产生的商品是软件,软件的介质是"文档"。 一个复杂庞大的系统,如果没有做好分解是不可维护的。
我们从文档管理员的角度来整理理解,我们会将经常改动的文档放在靠近房间入口的位置,将不经常改动的文档放到房间最里层。 这是按照改动频率的分层。
如果以搭积木的角度,我们会把最结实最有支持的文档放到最下面,最轻最脆弱的文档放到最上面。 这是按照支撑能力的分层
分层之后的分模块也可以看做类似竖向的分层
分层之后,如果我们想改动层中某一代码并且想将影响降至最小,就需要进一步分模块,将影响限制到模块内。
所以这是一个关于打横和打竖的讨论
分层设计
战略设计-横向
明确分层
战略设计关注的是大局,它帮助团队识别和界定业务领域的边界,以及这些领域间的关系。它的目的是确保团队能够在正确的上下文中解决正确的问题,从而实现高效和有针对性的软件设计和开发。主要概念包括:
-
领域:特定业务领域的知识和活动集合。
-
子域:大型领域中的一个具体区域,通常分为核心域、支撑域和通用域。
-
界限上下文(Bounded Context) :明确界定的责任边界内的模型和语言。不同的界限上下文可以有自己的模型,即使是相同的业务概念。
-
上下文映射:不同界限上下文之间的关系和交互方式。
通过战略设计,团队能够确定哪些业务领域是核心业务,哪些是次要的,这有助于资源优先级的分配。同时,明确的界限上下文和上下文间的映射,确保了系统的不同部分在概念和实现上的一致性和清晰度。
战术设计-纵向
明确模块
与战略设计的宏观视角相比,战术设计更注重具体实现。它提供了一系列模式和实践,指导开发者如何在确定的界限上下文内设计和实现领域模型。主要概念包括:
-
实体(Entity) :具有唯一标识的对象,其标识在其生命周期内保持不变。
-
值对象(Value Object) :没有唯一标识,仅通过其属性值定义的对象。
-
聚合(Aggregate) :一组具有统一根实体的对象集合,它们作为数据修改的单元,并保证数据的一致性。
-
聚合根(Aggregate Root) :聚合内的主要实体,外部对聚合的引用都通过聚合根进行。
-
领域服务(Domain Service) :当某个操作不自然属于任何实体或值对象时,这个操作可定义为领域服务。
-
仓储(Repository) :提供了一种从持久层查找和存储聚合或实体的机制。
-
领域事件(Domain Event) :反映领域内发生的有意义的事件,这些事件是状态变更的结果。
以上讲的是偏向于业务实现的分模块,但是在实际前端开发中,不同场景会有不同的实践,我们以具体情况具体分析。
分层实战经验
按照重逻辑还是重视图可以对不同需求进行分解。
重逻辑和重视图,意味着该需求一般都有明确的领域概念范围划分,从PRD就可以知道。同时逻辑层和渲染层都有相当的体量。比如 飞书文档、低代码业务、渲染框架库等
重逻辑和轻视图,意味着该需求更加注重系统的内在逻辑和数据处理,可能都没有界面。比如 babel编译器、Node服务等。
重视图轻逻辑,比如组件库、
场景1:重视图轻逻辑:一个MIS服务,对应前端平台,例如 运营平台、管理平台;
在这种情况下,前端承载很少的业务逻辑,是比较单纯的视图层,所以我们的设计也不要太复杂。然后模块划分尽量和后端的领域划分保持一致。
分层设计:一个common 一个 apps。 apps层 存储不同模块。common 储存通用模块。
理由:前端无领域服务 ,总体上可以看做后端领域服务的一个适配层,不宜做太复杂的分层。
模块设计:每个领域服务对应一个模块
理由: 同上,前端只是领域服务的适配器,直接按照后端的领域服务分模块即可
示例:
根据后端的领域服务进行模块区分,后端的领域服务会通过BAM生成代码,前端再根据BAM间接引用了后端领域服务。为什么这样做呢? 后端领域服务划分清楚后,其需求变更也会根据这个服务来划分,前端保持和后端同样的模块划分 可以降低变更导致的对其他模块的影响。比如签约服务有更改,前端也只用变更签约服务模块。
场景2:轻视图重逻辑: 一个Node服务
分层设计:
一个比较完全的DDD分层,注意需要结合业务具体的体量和复杂度,进行简略合并层数,比如,简单的业务可以把领域和领域服务给合并为Service层,用户接口层可以去除,直接暴露controll,也就是只有Controller和 Service两个层。如果更复杂点,可以按照每个层的规划进行增加和分解。
TypeScript 领域驱动设计(DDD) └── 应用层:处理应用逻辑,协调领域对象以完成业务用例。 ├── 服务 │ ├── 应用服务:用于处理应用逻辑和协调领域对象以完成业务用例。它们负责调用领域服务、仓储和其他应用服务来实现业务流程。 ├── 调用领域服务 ├── 调用仓储 └── 调用其他应用服务 └── 事件: 处理领域事件的发布和订阅,以实现不同领域模型或应用服务之间的松耦合通信。 ├── 命令:用户或系统发出的操作指令。 └── 事件处理器:处理领域事件 例如: └── 领域层 ├── 领域模型:反映业务领域的核心概念和逻辑 │ ├── 实体:具有唯一标识的对象,通常可以对应到一个表里 │ ├── 值对象:不可变对象,用于描述某些属性,比如日期、状态枚举 │ ├── 聚合根:聚合的入口点,负责维护聚合的一致性,比如删除一个订单聚合根,会连带着删除历史记录表。 │ └── 工厂:创建复杂对象的工厂方法。 └── 领域服务:领域服务是领域层的概念,用于处理那些不适合放在任何单个实体或值对象中的业务逻辑。它们通常是无状态的,并且其操作与领域模型紧密相关。 └── 基础设施层 ├── 仓储(Repository):负责聚合根的持久化和检索。 │ ├── 数据访问对象(DAO):封装数据库操作。 │ └── 数据库连接:管理与数据库的连接。 ├── 消息队列:用于异步事件处理。 └── 外部服务集成:与外部系统或服务的集成。 └── 用户接口层 ├── 控制器:处理用户请求,调用应用服务。 ├── 视图:展示数据给用户 └── 数据传输对象(DTO) └── 工具层 |
---|
模块设计:每个层每个领域对应一个模块
场景3: 重视图重逻辑: 一个低代码编辑平台
在这种情况下,每个模块可能都有非常多的业务逻辑,并且模块之间会有相互依赖等情况。在这种情况下,我们需要从总体视角下,从领域角度划分模块,然后尝试解耦。 比如类似微服务的分治,按照DDD模式重构物料模块,编辑器模块,属性编辑器模块,每个模块有对应的界线上下文,这个是通过一种服务协议规定的(如图)。
每个模块拥有其自己的领域服务,然后通过模块管理器,比如依赖注入器,根据注册协议进行注册(如图),实现每个模块的服务发现,通过依赖注入器实现每个模块间的代码解耦。
场景4: 轻视图轻逻辑:一个纯页面或者模块
这种情况下,无需额外设计,一个文件直出即可,不过我有个最佳实践:
可以讲一下我的一般模块设计原则:
-
前端可能会有Adapter讲后端接口转换为前端描述,但是这样会有个坏处,导致理解成本提高,相当于换了一套语言映射后端的结构。一般情况下,不用Adapter,除非以下情况:
- 后端数据结构频繁变化,然而前端相对稳定
- 时间问题,前端需要赶时间做功能,得自己先实现一个State
不然一般情况下,由后端给出定义后,前端直接按照后端结构处理即可。
- 那么之前的Adapter具有将后端结构转化为前端数据的功能,这个给放哪里了呢?答:这个用Computed来实现了。
- 那State一般放些什么?答:State主要放后端的状态,Computed根据State数据 计算出视图数据。
- 之后这个前端的Manager一般的,都具有init方法,然后会有各种asyncAction、action等,直接通过方法透出即可。
- 那么除了后端的状态外,其他纯前端的状态怎么放呢?答:直接用useState单独管理。
- 模块总体太大了怎么办?答:根据功能分出子模块,在总模块里实例化后透出
- 最后使用Provider将Manager在此Manger负责的模块里注入。这样取消了层层Prop传递,又有模块的界线,总之很好用。
- 子模块太多,怎么管理?答:后续考虑往下分层,然后使用EventBus或者DI等技术进行解耦。这样会演变为更复杂的场景。
代码示例:
| TypeScript function useManagerFactory() { const [state, setState] = useState(); const [initLoading, setInitLoading] = useState(); const init = useCallback(async () => { const res = await Service.get(); setState(res); }, []) const loginManager = useLoginManagerFactory(); const userName = useMemo(() => state.user.name, [state]) return { initLoading, loginManager, userName, init } } // 创建Manager注入器 export type ManagerIns = ReturnType; export const ManagerContext = React.createContext<ManagerIns | undefined>(undefined); export const ManagerProvider = ManagerContext.Provider; | | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
结语
本文探讨了分层实践的几种场景的实践,不过希望实践的时候不要教条主义,任何具体的业务需要我们具体分析结合实际进行分层设计。
根据这些年的实践,我大概想到一个理论:人类的效率优化,会将重复的事情收敛,最后工作结构近乎于树形(和组织架构同构)。我把这个叫做信息树,所以优化的点就在于
- 越公共越重复使用的能力,越需要往父节点方向走。
- 非树形或者树形不明显下也就是依赖混乱的情况下,不是最优架构。具体体现在:沟通不收口、接口不收口、管理不收口。
- 每个节点可以是弱管理子节点(模块管理器、EventBus之类),或者强管理(HardCode),越复杂,弱管理越多。
- 越复杂的系统,此信息树层级越多。
数据流模型设计:数据是怎么流转的
数据流模型设计是对运行时数据流转过程进行设计 |
---|
我们往往使用流程图、时序图来描述数据是如何流转的,我们功能模块设计后,就需要设计数据是怎么在各个功能模块之间流转的,否则这个系统就没有血液流动,没法跑。同时得保持血液健康,没有异常数据流或者流量爆炸等情况。
在设计数据流过程中,我们经常需要注意几点:
性能:
- 数据是否精简
- 频次是否过高
稳定:
- 注意幂等
- 数据校验
- 流量异常
- 容错
维护:
- 可读性
- 可扩展
监控:
-
可发现异常数据和性能瓶颈
-
日志系统,对数据的权限管控
一个通用的数据流优化理念:合并、分片和节流
前端:
- VDOM 的出现和进一步优化:VDOM(Virtual DOM)的核心理念是合并高频次的 DOM 操作,以减少渲染时间。通过生成内存中的虚拟 DOM,我们可以对内存 DOM 进行合并操作,从而减少对真实 DOM 的修改次数。React 等库就是沿着这个发展路径不断优化的。
- 持续高频计算阻塞渲染优化:为了提升网页的渲染流畅度并优化在持续高频计算场景下的渲染阻塞问题,我们借鉴了操作系统中的作业管理技术。具体实施方案包括将复杂的代码逻辑划分为多个较小的作业单元,实现作业的分片调度。此外,通过引入作业恢复和优先级排序机制,确保渲染任务可以得到优先执行,从而避免长时间的渲染阻塞,确保用户体验的流畅性。
这个理念在React中指的是"时间切片"(Time Slicing)和"并发模式"(Concurrent Mode)。时间切片允许React在渲染过程中根据需要将任务分割成小块并暂停和恢复,这样就可以避免长任务阻塞浏览器渲染。并发模式则是一组新的特性,它允许React在渲染时进行任务的中断、优先级判断和恢复,从而提高应用的响应性。这些特性首次在React 16.x的alpha版本中作为实验性功能引入,而后在React 18中正式引入并发模式作为稳定功能。React 18在2021年3月发布了第一个公开的Alpha版本,并在2022年正式发布了稳定版本。因此,如果你所说的理念指的是时间切片和并发模式,那么它是在React 18中正式成为稳定特性的。所以软件发展到当前的程度,各种所谓新领域的新东西,其核心理念也是早就发明了的, 只是换了种说法。 |
---|
- 重复操作的防抖和节流,弃用部分请求
后端:
-
缓存层:以缓存层为例,可以将多个请求的缓存操作合并为一个,减少数据库查询的次数。同时,可以将缓存数据分片存储,提高缓存的可用性和扩展性。通过合并操作和分片,可以提高系统的性能和响应时间。
-
数据库读写分离:在数据库读写分离的情况下,可以将多个读请求的操作合并为一个,以减少数据库查询的次数。同时,可以将数据分片存储,提高数据库的可用性和扩展性。通过合并操作和分片,可以提高系统的性能和响应时间。
-
消息队列:以消息队列为例,当多个消息需要发送到同一个目的地时,可以将它们合并为一个消息,以减少网络传输次数。同时,可以将消息队列中的消息分片存储,以提高系统的可用性和扩展性。通过合并操作和分片,可以提高系统的性能和响应时间。
-
QPS限制等限流技术,弃用部分请求
在优化数据流性能时,我们通常会向核心层或基础设施层添加工具集或工具库,以实现对应的性能监控和数据流优化。
存储数据设计:
是对数据储存时的结构进行设计 |
---|
当前的存储数据结构类型
行列储存
- 关系型数据库,如 MySQL、Oracle 等,通常采用行列存储结构来存储数据。
- NoSQL 数据库,如 MongoDB、Cassandra 等,也可以采用行列存储结构来存储数据。
- 列式存储数据库,如 HBase、Cassandra 等,通常采用列式存储结构来存储数据。
图存储
- 图形数据库,如 Neo4j、GraphDB 等,通常采用图存储结构来存储数据。
- 知识图谱数据库,如 RDF 数据库、Ontology 数据库等,也可以采用图存储结构来存储数据。
对象存储
- 对象存储服务,如 Amazon S3、Google Cloud Storage 等,通常采用对象存储结构来存储数据。
- 文件系统,如 NTFS、EXT4 等,也可以采用对象存储结构来存储数据。
选择存储数据结构的因素
- 数据模型:不同的数据模型适合不同的存储数据结构。例如,关系型数据模型适合行列存储,而图形数据模型适合图存储。
- 数据访问方式:不同的存储数据结构支持不同的数据访问方式。例如,行列存储适合随机访问,而列式存储适合批量访问。
- 数据规模:不同的存储数据结构适用于不同的数据规模。例如,关系型数据库适合处理小型到中型的数据集,而分布式文件系统适合处理大型的数据集。
- 性能需求:不同的存储数据结构具有不同的性能特征。例如,列式存储适合处理分析型查询,而图形数据库适合处理复杂的关联查询。
- 成本:不同的存储数据结构具有不同的成本。例如,关系型数据库通常比 NoSQL 数据库更昂贵,而分布式文件系统通常比关系型数据库更便宜。
存储设计的原则
- 数据完整性:存储设计应该确保数据的完整性和一致性。
- 数据可用性:存储设计应该确保数据的可用性和可靠性。
- 数据安全性:存储设计应该确保数据的安全性和隐私性。
- 可扩展性:存储设计应该考虑到未来的数据增长和系统扩展。
- 性能优化:存储设计应该考虑到性能优化,以提高数据访问的效率。
存储设计的流程
- 需求分析:了解业务需求和数据特点,确定存储的数据类型和数据量。
- 数据模型设计:根据需求分析的结果,设计合适的数据模型。结合领域模型,设计数据表。
- 表设计:一般一个实体可以映射为一个数据库表。
对于泛化的不同实现方式:
泛化的数据库实现方式 | 优点 | 缺点 | 场景 |
---|---|---|---|
每个类一个表 | - 和领域设计对应 - 不会浪费空间 - 差异点的非空约束实现更合理 - 不易腐化 | - 大部分场景需要联表查询,性能慢 | 对性能没有什么强需求 |
每个子类一个表 | - 不会浪费空间 - 部分场景不需要联表,性能快 - 不易腐化 - 差异点的非空约束实现更合理 | - 全局场景,可能需要两个表一起去重,查两次表而损失性能 - 和领域设计的对应还差一点 | 查重场景少的情况 |
整个泛化一个表 | - 不需要联表查询,时间最快。 - 表少,简化 | - 容易混淆、腐化 - 子类的差异点会浪费空间 - 和领域设计对应不上(变成一坨了) | 对性能要求高 |
举例:
之前我们定义了实体,我们以这些实体进行数据库设计
数据库表设计:
- 这里对于用户的泛化,采用所有实体放到一张表,然后使用type 和 responsibilities
有了领域模型之后,领域模型可以比 ER 图表达更多的信息,因此就无需使用 ER 图了。可以说,领域模型是 ER 图的超集。
-
存储选型:根据数据模型和性能需求选择合适的存储技术和产品。
- 关系型数据库:如果数据之间存在复杂的关系,需要进行复杂的查询和分析,可以选择关系型数据库,如 MySQL、Oracle、SQL Server 等。
- NoSQL 数据库:如果数据量巨大,并且需要支持高并发读写和快速响应,可以选择 NoSQL 数据库,如 MongoDB、Cassandra、Redis 等。
- 分布式文件系统:如果需要存储大量的非结构化数据,如图片、视频、音频等,可以选择分布式文件系统,如 Hadoop HDFS、GlusterFS 等。
- 云存储:如果数据需要在云端进行存储和管理,可以选择云存储服务,如 Amazon S3、Google Cloud Storage、Microsoft Azure Blob Storage 等。
- **选择存储技术和产品时,需要考虑数据量、数据类型、读写性能、可靠性、安全性、成本等因素。同时,还需要考虑存储技术和产品的可扩展性和可维护性,以便随着业务的增长进行升级和扩展。
- 例如:储存数据巨大,并且需要支持精确搜索,可以选择分布式文件系统或 NoSQL 数据库。
-
存储架构设计:设计存储系统的架构,包括存储节点的布局、存储网络的拓扑结构等。
-
性能优化:根据存储架构和业务需求进行性能优化,包括缓存设计、索引设计、数据分片等。
-
安全设计:设计存储系统的安全机制,包括访问控制、数据加密、备份恢复等。
-
测试和验证:进行存储系统的测试和验证,以确保其满足业务需求和性能要求。
-
部署和运维:部署存储系统,并进行运维和监控,以确保其稳定运行。
存储设计的挑战
- 数据一致性:存储设计需要考虑到数据的一致性,确保多个节点上的数据是一致的。
- 性能优化:存储设计需要考虑到性能优化,以提高数据访问的效率。
- 可扩展性:存储设计需要考虑到可扩展性,以适应未来数据增长和系统扩展的需求。
- 数据安全性:存储设计需要考虑到数据的安全性,确保数据不被泄露或篡改。
- 成本控制:存储设计需要考虑到成本控制,以确保存储系统的建设和运维成本在可接受的范围内。
存储设计的发展趋势
-
云原生存储:随着云计算的发展,云原生存储成为存储设计的重要趋势。云原生存储具有弹性扩展、高可用性、自动化管理等优点,可以更好地适应云计算环境下的存储需求。
-
容器存储:随着容器技术的发展,容器存储成为存储设计的新趋势。容器存储可以更好地支持容器化应用的存储需求,提高存储的灵活性和可移植性。
-
人工智能存储:随着人工智能技术的发展,人工智能存储成为存储设计的新趋势。人工智能存储可以更好地支持人工智能应用的存储需求,提高存储的效率和性能。
-
边缘存储:随着物联网和边缘计算的发展,边缘存储成为存储设计的新趋势。边缘存储可以将数据存储在靠近数据源的地方,减少数据传输的延迟和成本,提高存储的效率和性能。
部署运行时设计:
是对系统运行的物理架构进行设计 |
---|
部署设计是对系统运行的物理架构进行设计,包括硬件选择、网络拓扑、基础设施、操作系统、应用程序和数据存储等方面。部署设计的目标是确保系统在不同场景下的可靠性、可用性、可扩展性和安全性,同时满足性能需求和成本效益。
在部署设计过程中,需要考虑以下几个方面:
- 硬件选择:根据系统的性能需求和成本效益,选择合适的服务器、存储设备、网络设备和安全设备等。同时,还需要考虑硬件的可扩展性和冗余性,以满足未来系统的扩展需求。
- 网络拓扑:设计合理的网络拓扑结构,确保系统的各个组件之间能够进行高效的数据传输和通信。同时,还需要考虑网络的可靠性和安全性,以防止数据泄露和网络攻击。
- 基础设施:搭建可靠的基础设施,包括电力供应、冷却系统、消防系统等,以确保系统的稳定运行。同时,还需要考虑基础设施的可维护性和可扩展性,以方便未来的升级和维护。
- 操作系统:选择适合系统的操作系统,确保系统的各个组件能够在操作系统上稳定运行。同时,还需要考虑操作系统的安全性和可维护性,以防止操作系统漏洞和故障对系统造成影响。
- 应用程序:设计合理的应用程序架构,确保系统的各个组件能够高效协同工作。同时,还需要考虑应用程序的可扩展性和可维护性,以方便未来的升级和维护。
- 数据存储:选择合适的数据存储方案,确保系统能够高效存储和管理数据。同时,还需要考虑数据存储的安全性和可扩展性,以满足未来系统的数据增长需求。
综上所述,部署设计是系统运行的重要基础,需要综合考虑各个方面的因素,以确保系统的可靠性、可用性、可扩展性和安全性。在部署设计过程中,需要不断优化和调整设计方案,以满足系统的实际需求和运行情况。
总结
架构设计的基础要素包括以下几个方面:
- 产品架构。在理解别人的架构时,我们首先需要了解其产品架构,即系统的整体结构和各个组成部分之间的关系。
- 功能模块架构。接下来,我们需要关注功能模块架构,以确保其满足产品的功能需求。这包括检查各个模块之间的交互方式、模块的划分是否合理、是否存在功能重复或缺失等问题。
- 代码模块分层。然后,我们需要查看代码模块的分层是否简洁合理。一个良好的分层结构可以提高代码的可读性、可维护性和可扩展性。我们需要关注模块之间的依赖关系、是否存在不必要的耦合以及层次是否清晰等问题。
- 数据流模型。数据流模型是指系统中数据的流动方式和处理逻辑。我们需要检查数据流模型是否存在各种性能、监控上的问题,例如数据瓶颈、数据一致性问题、数据处理延迟等。
- 存储设计。最后,我们需要关注存储设计是否能够满足系统的各方面要求,包括存储容量、性能、可靠性等。这包括选择合适的存储技术、设计合理的数据存储结构以及确保数据的安全性和一致性等问题。
综上所述,通过对产品架构、功能模块架构、代码模块分层、数据流模型和存储设计等方面的综合分析,我们可以全方位地设计或理解一个架构。