什么是架构及架构师
写在前面
2024年笔者正式步入到架构设计的领域,因此有必要记录自己在架构设计方面学习的点滴。由于每个业务系统都要求从业务架构的视角进行代码开发和迭代,因此架构师属性不仅仅是作为架构师所必备的技能,更是作为每个开发者应当具备的属性,这种架构师可称为纵向架构师,而目标在于提高整个团队水平,确定多业务边界的,笔者称之为横向架构师。
【纵向架构师】主要考虑业务和系统,做问题和结果的定义,做系统、模块、代码设计;
【横向架构师】解决跨域问题,确定跨团队边界,定义规范、统一语言。定义纵向架构能力基础/标准的成长路径和方法论,让人人都成为架构师。即它的核心过程是探讨如何完成架构设计,如何培养和拥有架构思维。
什么是架构师
定义
关于架构师的定义,wiki百科是这样定义的:
软件架构师定义和设计软件的模块化,模块之间的交互,用户界面风格,对外接口方法,创新的设计特性,以及高层事物的对象操作、逻辑和流程。软件架构师与客户商谈概念上的事情,与经理商谈广泛的设计问题,与软件工程师商谈创新的结构特性,与程序员商谈实现技巧,外观和风格。
笔者认为,架构师主要有如下三个职责:
(1)跟需求方,提炼关于项目的概念并和需求方有统一的沟通语言;
(2)基于利益相关方,总结关键质量属性,并给出满足质量属性的软件系统架构设计;
(3)与开发工程师,共同进行软件系统的落地。
【注意】这里的质量属性,是指评估系统性能的非功能性需求,如可用性,可扩展性,一致性等。
其实从上面的图中可以看出,想要成为架构师,需要具备三方面的能力:
(1)挖掘/提炼需求描述的能力。一般来说,客户或者产品对于需求的描述都比较简单,会缺少一些上下文或者产品上认为不重要的部分,此时就需要架构师来拨开云雾,了解需求详情、业务目标,确定不同方的关注点。
(2)架构设计能力。在架构设计方面,有一个非常重要的点就是架构师及团队成员的能力。举个例子,如果我们在选型非关系型数据库的时候,团队成员对Redis非常熟悉,而对MongoDB不太熟悉,那么在选型中会更加青睐于Redis。因为这对于项目的开发,测试和运维成本以及进度风险会更低。与此同时,我们也需要承担这个选型带来的一些缺点。所以架构设计不仅依赖方法论,还依赖于架构师的技术前沿性。
(3)代码落地能力。架构师要能够和开发人员一起,完成代码的落地编写,因此一个好的架构师也是一个优秀的开发者。
也就是说,架构师是需要在日常工作中,从多个维度去坚持提升,包括但不限于业务抽象能力、代码编写能力、复杂问题解决能力、与人沟通能力、项目管理能力等。
方法论
之前笔者以为方法论非常虚,没必要去学习,后面随着实际经验的累计,发现“学习方法论 > 实践 > 基于方法论对实践进行反思”这一循环,正是我们自身能力螺旋上升的典型模式。
举一个笔者印象非常深刻的例子,余弦定理。初看公式的时候,我们会觉得很精妙,之后自己花一定时间去推导出来后,发现太完美了。但是推导它需要花费一定的时间和精力,所以我们都是将该公式铭记于心,然后在解决相关问题时,就直接套用公式,而不是再去重新推导,这其实就是一种方法论,对于架构设计也是如此。
什么是架构
架构,是由组成模块、特定上下文环境下模块间的关系、指导模块和关系发展的原则。
有点类似于编程语言中的基类,越顶层的抽象描述事物的范围越广。对于软件系统架构,我们可以从一个软件系统最终落地结果来看,进而得到软件系统架构关于架构的具体实现。
它分为系统模块架构、对象架构和数据存储架构这三种,具体如下:
(1)系统模块架构:
- 模块:系统分层模块,应用模块和外部服务模块;
- 关系:模块依赖关系,模块数据流转方式;
- 原则:模块划分方案。
(2)对象架构:
- 模块:类,包含哪些核心类,类的职责是什么;
- 关系:类的继承关系、依赖关系;
- 原则:类设计指导思想和关注点。
(3)数据存储架构:
- 模块:数据库和表,有哪些数据库、数据表;
- 关系:表的关联关系,不同数据表之间的一对一或者一对多关系;
- 原则:数据存储选型/设计方案和关注点,以及设计考虑的核心质量属性。
架构师要解决的问题
前面我们提到了软件系统架构的定义,实际上这些就是架构师的职责和要落地的内容。对于业务系统来说,我们的输入往往就是业务需求或者技术需求的描述集合,输出则是软件的系统架构,中间的则是架构师要解决的问题,即如何将业务需求或者技术需求加工成落地的软件系统架构。
实际上我们提到的数据存储架构、对象架构、系统模块架构,这些底层都需要有业务领域模型,这样才能设计出对应的软件系统领域模型。要想得到业务领域模型,我们需要对需求充分了解,充分挖掘,并评估各个利益方关注点的核心质量属性以及业务目标,因此架构师要解决的问题如下:
分析过程
从上往下看,首先是了解需求,包括利益相关方的业务目标、关键架构需求和用例集。
利益相关方的业务目标会影响质量属性的优先级和架构的设计选型。而关键架构需求包含四类:
(1)约束:给定的不可更改的设计决策;
(2)质量属性:评估系统的非功能性需求,是利益相关方判断软件系统是否好用的一切外部可见特性。如响应耗时,可扩展、可用性、可维护性等,它表征系统在特定环境下的运行情况。通常设计系统时会提高或者拟制一个质量属性,如性能和可读性。因此在模式选择的时候,需要基于质量属性进行抉择。
(3)影响较大的功能需求:一些需要特别注意的特性和功能;
(4)其他影响因素:团队成员的经验和擅长。
用例集,它是一组相关的成功和失败的场景集合,用于描述参与者如何使用系统来实现目标。举个例子,一个优秀的产品文档,应当无限接近于标准的格式化用例描述集合,它是原始产品方案经过多次沟通得到的标准化语言。一个用例应该要包含参与者(可以是系统或者用户)、前置条件(使用场景)、如何使用系统、经过什么规则、产生什么结果。只有这样的用例集合,才能描述出产品系统的功能全貌。
设计过程
当从业务目标、关键架构需求和用例集将需求描述清楚之后,接下来就可以使用业务模型来描述项目产品。这一层设计的是问题空间中的领域模型,它是对客观物理世界中概念、规则、关系的分析和描述,与软件系统无关。
这个层次上的实体,我们称之为概念实体,这一部分内容用在需求和业务分析上。讨论业务概念模型时,完全不需要考虑软件的实现,这是一个分析过程,即使不做软件研发,做其他的研发,类似的分析过程也应该有。
实际上,笔者翻阅《实现领域驱动模型》一书,里面对问题空间也做了一个定义,即问题空间是顶级域和其他域的组合,以及域之间的关联关系,即使没有软件的存在,这些域还是存在的,域之间的关系也还是存在的。
在设计过程,我们需要将问题空间的领域模型映射到解决问题空间,而《实现领域驱动模型》一书中对解决问题空间的定义“一组特定的软件模型,它通过软件的方式来实现解决方案”。实际上,这一组特定的软件模型就是我们要产出的软件系统架构,如系统模块架构、对象架构、存储架构。
笔者看过一些文章,发现现在很多讲架构的文章,讲的都是这一层的故事,甚至有些只讲了系统模块架构,如MVC架构、六边形架构、洋葱架构和整洁架构等。
《领域驱动设计,软件核心复杂性应对之道》 一文,则花了大量的篇幅,来介绍实体、值对象、服务、聚合根、工厂、仓库、界限上下文等,这是一套将问题空间领域模型映射到解决空间对象架构的设计模式。
如何解决架构师要解决的问题
了解需求
只有了解需求、了解利益相关方关注点、了解业务目标,我们才能制定对应的设计策略。
利益相关方关注点
首先我们需要梳理需求的利益相关方关注点图表,如下所示:
这里我们对利益相关方进行了排序,其中优先级越高的,它的关注点越能影响后续决策。与利益相关方沟通之后,接下来我们要与他们明确业务目标(背景+主体+结果)。业务目标是架构的主要驱动因素,多个利益相关方发生冲突时,要以业务目标为核心进行排序选择。
关键架构需求
关键架构需求是指能够显著影响架构中的结构选择的需求,一般为四类:约束、质量属性、影响较大的功能需求和其他影响因素。
- 约束:约束是架构中不能被打破的原则性前提。约束的语法应当是“必须怎样”,而不是“尽量怎样”。由于约束的不可变性,因此我们要尽可能的减少约束性需求。但是新增约束通常会带来架构设计上的便利,加速项目功能上线,只是需要架构师来甄别哪些约束恰当。
- 质量属性:质量属性与功能性需求无关,因此很容易被忽视,但是在进行模式选择和方案设计的时候,通常又要基于质量属性做设计决策。可通过一个质量属性优先级的排名来对利益相关方关注的事项和原始质量属性场景进行提取、分类、完善、排定优先级,并可视化头脑风暴与利益相关方的沟通结果:
- 影响较大的功能需求:需求特别注意的特性和功能。
- 其他影响因素:请注意,这里虽然叫做其他影响因素,但是它对最终架构选型至关重要。架构师和团队的经验极大程度上影响着,我们使用框架和存储选择。
用例集
用例集,它是一组相关的成功和失败的场景集合,用于描述参与者如何使用系统来实现目标。单独一个用例,应该要能完整表达一次或者一类业务行为。
一个用例应该要包含参与者(可以是系统或者用户)、前置条件(使用场景)、如何使用系统、经过什么规则、产生什么结果。只有这样的用例集合,才能描述出产品系统的功能全貌。
请注意,这里的用例集合通常所说的tc(测试用例)集是不一样的,用例相比tc更强调过程规则,强调对功能的描述,而tc则更多关注的是输入和输出,强调覆盖场景,强调对业务功能、业务模型、技术模型的功能性或者非功能性验证。
架构描述
前面提到的业务模型还是软件系统模型,它们对于架构的描述,都不可避免的涉及到通过图来描述全貌。复杂的系统架构,用单一的模型图描述的话,只会非常粗略,因此提出了“视点”这一概念。
”视点“,即从不同的角度或者专业领域来看待系统的方法。常用的视点方法论有”C4模型“和”4+1模型“。
C4模型
所谓C4模型,实际上就是4个层次,下面分别进行介绍:
(1)上下文层次(Context):描述系统与外部实体(如用户、其他系统、软硬件设备等)之间的关系,用于展示系统如何与周围环境交互以及其外部依赖关系。
(2)容器层次(Container):系统内的软件被分解成多个容器,如应用程序、数据库、文件系统等,容器图描述了这些容器之间的关系及它们如何共同工作以实现系统的功能。
(3)组件层次(Component):在容器的内部,每个容器被进一步拆分为组件,如类、模块、服务等,组件图描述了组件之间的关系和依赖关系,以及它们如何协同工作。
(4)代码层次(Code):这是最低层次,描述了每个组件的内部实现细节,可以是类图、包图等,用于展示组件内的代码结构。
4+1模型
所谓4+1模型,实际上就是5个视图,下面分别进行介绍:
(1)场景视图:从外部视角,描述系统的参与者(用户)与系统功能用例的关系。反映的是系统的最终用户需求和交互设计。
(2)逻辑视图:从结构化视角,描述该系统对用户提供的所需功能服务所具备的组件结构和数据结构,以及一些边界约束条件,清晰的描述给用户提供的功能需求服务是如何构建的。描述该系统内部所具备了那些组织结构,以达到实现对外功能。
(3)开发视图:从结构化视角和行为视角,去描述实现系统功能的各个组件和模块是如何实现的。
(4)处理视图:从行为视角,描述系统各个组件和模块是如何进行通信的。
(5)物理视图:从交互视角,描述系统可以部署到哪些物理环境(如服务器、PC端、移动端等)上和软件环境(如虚拟机、容器、进程等)上。
详细说明
下面分别从问题空间和解决方案空间模型出发,来对架构进行描述:
(1)业务领域模型图:描述需求的分析结果,突出业务领域概念和业务模型关系,统一产品需求方、领域专家、开发人员的概念语言;
(2)系统模块图:描述系统内的分层模式和模块/领域依赖关系,描述系统间的依赖关系和数据交互方式;
(3)对象模型图:描述核心类职责,类与类继承关系,类与类的依赖关系;
(4)数据存储架构图:描述库表结构,分库分表策略,存储选型,以及不同数据表之间的一对多/一对一等依赖关系。
实际上,这里也是一个视点想要表达的内容,而不是只用一张图来表达内容。我们按需绘制精细视图,用于呈现系统细节,描述某个局部的系统细节。也可以增加质量属性视图,用来表达设计原则,描述对于某个属性质量的设计,如可用性、数据准确性、一致性等。
业务模型
业务模型是问题空间的领域模型,描述业务和产品,与软件系统无关,具体描述的是客观物理世界的概念、规则、关系。
要想对业务领域建模,首先要对用例进行分析,步骤如下:
(1)从准确的用例中剥离出名词;
(2)根据名词梳理领域模型和其属性;
(3)根据名词的修饰梳理出属性值;
(4)根据名词的定义完善属性值;
(5)从用例集合中剥离出动词&形容词;
(6)根据动词&形容词梳理出领域模型之间的关系。
但如果只是对用例集进行提取,会遗漏很多隐藏概念。举个例子,关于“一个店铺拥有多个子账户,而每个子账号对店铺的操作权限又不相同”这一描述,我们很容易将操作视作为子账号的一个属性,子账号则挂靠在店铺实体上:
这里例子应该很常见了,一般我们都会将权限单拎出来,作为单独的实体,然后再将权限关联到账号上,这样也方便权限后期的维护和继承:
实际上这里就涉及到了架构元模型(元模型定义了模型中使用的概念和使用规则)中隐藏概念的建立,从提问开始,建立模型,检验模型,分离概念,如此循环:
请注意,业务人员与技术人员之间需要使用统一的、通用的语言,这样才能极大地降低了沟通成本。
软件系统模型中的系统模块架构
系统模块架构用于描述系统内的分层模式和模块/领域依赖关系,即系统间的依赖关系和数据交互方式。
关于系统内的分层模式和模块/领域的依赖关系方法论,《DDD中常提到的应用架构总结(六边形、洋葱、整洁、清晰)》一文中介绍的非常详细,这里简单摘录一下。
传统MVC分层架构
最开始的传统MVC分层架构,对系统进行了简单分层,描述了从数据层到业务逻辑层,再到数据出口层的关系。由于MVC中,业务逻辑对数据出入口的依赖是确定的,因此如果这个依赖发生变化,那么改动成本非常大,无法突出领域模型的独立性:
六边形架构
于是六边形架构出现,它将领域层独立出来,不依赖任何确定的外部服务,以端口/适配器的方式定义外部服务交互协议。只要能实现这套交互协议,数据出入口的依赖变化,对领域层是没有侵入的。这里体现的是领域驱动设计的思想,将领域知识立在最重要的地位,不为任何模块影响。
清晰架构
最后是清晰架构,它的最内层是领域层,包含领域模型和领域服务,实现相关的领域知识和概念模型。实际上清晰架构中间还存在洋葱架构和整洁架构这两个架构。
清晰架构向外是应用服务,可依赖领域层,做业务用例的编排实现,如操作某个领域服务后,再操作某个实体进行某项行为,最后发送某个领域事件等。和应用服务层同级的,还有CQRS和事件/消息处理器,接受不同类型的命令执行类似应用服务的事情。
以上这些即应用核心,应用核心与外界的交互分为两类:图中的左半边为主动适配器,做类似于Controller或是HSF服务的系统最外层请求实现;图中的右半边为被动适配器,定义消息出口、数据持久化接口、搜索引擎接口等,由外部具体的基础设施实现。可见,清晰架构属于集前人所长,提供的一份以领域知识为核心的分层架构指南:
除了系统内部的分层结构,我们还需要描述系统间、应用间的关系,以及数据流转的方式:
软件系统模型中的对象架构
按照前面抽象出来的业务领域模型编写的代码,能够很好的表达设计含义,且模型与实际系统相契合。
面向对象编程之所以强大,是因为它为架构概念提供了实现方式,能描述现实物理世界中的关系(操作、继承、组合)和模型(定义、属性、职责)。
《领域驱动设计,软件核心复杂性应对之道》一书中,用了大量的篇幅来讲解如何使用面向对象的思路,来对类的类型进行划分,并将业务模型映射到对象模型中的模式。下面分别介绍实体、值对象、聚合、服务、工厂、仓库和防腐层等7个概念。
实体
【实体】实体由标识定义,而不依赖它的所有属性和职责,并在整个生命周期中有连续性。说白了,就是一个标识不变的对象。如果某个对象在它自身属性发生变化后,它依旧不变,那么它就是一个实体。举个例子,某条工单,它的处理人和状态发生了变化,但是它的工单编号没有变化,那么这个工单依旧是之前的工单,只是它的一些属性发生了变化。
我们可以通过这种方式来识别实体,因为在领域中的关键对象,通常并不由它们的属性定义, 而是由可见的/不可见的标识来定义,且有完整的生命周期,在这个周期内它如何变化,它都依然是它。
通过这种方式识别出实体这种领域关键对象,这也是领域驱动设计和数据驱动设计最大的差别。数据驱动设计是先识别出我们需要哪些数据表,然后将这些数据表映射为对象模型。而领域驱动设计是先通过业务模型识别出实体,再将实体映射为所需要的数据表。
请注意,实体的标识可以是可见的,也可以是不可见的,因为有很多域内无持久化的系统,在它们的对象模型中,并不存在可见的唯一标识ID。
对于更加关注”行为”而非”唯一性”的纯计算型应用,笔者这里给出划分实体与值对象的另一种思路,如下所示:
(1)实体是会对自身属性做出强解释行为的类型;
(2)值对象是轻属性解释,重属性设计的类型。
因为对于纯计算型应用而言,业务关注重点是行为,当一个类需要承载复杂的计算逻辑,即对自身属性需要进行强解释行为时,它往往就承载了系统中更重要的职责,能更加凸显领域业务概念。
值对象
【值对象】值对象用于描述领域的某个方面而本身没有唯一标识的对象。被实例化后用来表示一些设计元素,对于这些设计元素,我们只关心它们是什么,而不关心它们是谁。
举个例子,某条工单的发生地址Address有城市、街道、道路这三个属性,其中的道路从长江路修改到了黄河路,它已经不再是修改前的那个了,因为道路名称已经发生了变化。即它是没有生命周期的,它的equals方法由它的属性值决定(实体的equals方法由唯一标识决定)。
聚合
【聚合】聚合是一组实体和值对象的组合,其内部包含一个聚合根,由聚合根关联实体和值对象。
举个例子,商品、sku、库存三个实体,那么在商品模型中,商品就是聚合根,其内部通过sku_id来关联它的sku,库存id关联商品/sku的库存。聚合将这组关联关系建立,对外提供统一的操作。如果需要删除某个商品,那么这个聚合的内部需要在一个事务(或分布式事务)中,对库存进行清空,对sku进行删除,最终才能对商品进行删除。
服务
【服务】有一些对实体/聚合/值对象进行编排操作的概念并不适合被建模为对象,那么它应该被抽象为服务,化作一只上帝之手,做领域对象间流程操作的编排。服务很重要的特征,它的操作应该是无状态的。
工厂
【工厂】当创建一个实体对象或聚合的操作很复杂,甚至有很多领域内部的结构需要暴露时,就可以用工厂进行封装。对于工厂的判断,这里提供一种相对简单粗暴的判断方法,即看这个类的构造方法实现是否复杂,且这些逻辑不应该由这个类实现那么此时就可以使用工厂来构造该对象。
仓库
【仓库】仓库是可持久化的领域对象和真实物理存储操作之间的媒介,随意的数据库查询会破坏领域对象的封装,因此需要抽象出仓库这种类型。仓库用于定义领域对象的获取和持久化方法,具体实现领域层无法感知。
至于具体用了什么存储,如何写入和查询,是否使用缓存,这些逻辑统一封装在仓库的实现层,对于后续迁移存储、增删缓存,可以做到不入侵业务领域。
防腐层
【防腐层】防腐层并不是一个特定的对象类型,而是一种领域模型保护的思路。对于领域外界的变化,我们需要持悲观的态度,因为领域外部的模型不受我们控制,它们的变化轨迹难以捉摸。因此在系统与系统之间,上下文与上下文之间,要有一层放腐层进行领域内外的模型转换。
职责划分
有了对象类型的划分之后,接下来将对对象职责进去确定,可以使用GRASP给出的判断标准:
(1)创建者。
问题:谁负责产生类的实例?
解决方案:如果符合下面的一个或者多个条件,则可以将创建类A实例的职责分配给类B:
B包含A
B聚合A
B拥有初始化A的数据并在创建类A的实例时将数据传递给类A
B记录A的实例
B频繁使用A
(2)信息专家。
定义:如果某个类拥有完成某个职责所需要的所有信息,那么这个职责就应该分配给这个类来实现。此时,这个类就是相对于这个职责的信息专家。
解决方案:将职责分配给拥有履行一个职责所必须信息的类或者域。
(3)低耦合。
问题:怎么样支持低的依赖,减少变更带来的影响,提高重用性?
解决方案:
在类的划分上,尽量创建松耦合的类,修改一个类不会影响其他类。
在类的设计上,尽量降低类中成员和方法的访问权限,尽量将类设计为不变类。
在类的引用上,将一个对象对另一个对象的引用降低到最小。
(4)高内聚。
问题:如何使得复杂性可控?
解决方案:功能性紧密的相关职责应该放在同一个类中,并共同完成有限的功能。这样做更加有利于对类的理解和重用,也可以降低类的维护成本。
(5)纯虚构。
当不想破坏高内聚和低耦合的设计原则时,但是有些职责又没地方放,如何处理
解决方案:将一组高内聚的职责分配给一个虚构的或者处理方便的类,它并不是问题域的概念,而是虚构的概念,以达到支持高内聚低耦合和重用的目的。
(6)间接。
问题:如何分配职责,以避免两个事物之间的直接耦合?
解决方案:当我们不知道将职责分配给何种模型的时候,可以看看是否可以将职责分配给中介模型。
软件系统模型中的存储架构
存储架构,用于描述数据库和表结构,包含分库分表策略,存储选型,以及不同数据表之间的一对多/一对一等依赖关系。
常见的手段有E-R图描述表模型关系,用图例来描述分库分表策略以及存储选型策略。
小结
本文学习了架构设计中的一些方法及软件系统模型,对于后续架构设计有很大帮助。由于横向架构师本质上是站在多业务视角上的纵向架构师,单业务领域对它而言其实就是子域,因此没有花篇幅去介绍横向架构师。
【参考资料】
(1)《领域驱动设计,软件核心复杂性应对之道》,作者:Eric Evans,译者:赵俐 / 盛海艳 / 刘霞,出版社:人民邮电出版社;
(2)《DDD中常提到的应用架构总结(六边形、洋葱、整洁、清晰)》;
(3)架构之道:人人都是架构师