架构之道——人人都是架构师
阿里妹导读
一、前言
Improving the development team’s ability gives an architect much greater leverage than being the sole decision maker. —— Martin Fowler
二、什么是架构师
2.1 什么是架构师
软件架构师定义和设计软件的模块化,模块之间的交互,用户界面风格,对外接口方法,创新的设计特性,以及高层事物的对象操作、逻辑和流程。软件架构师与客户商谈概念上的事情,与经理商谈广泛的设计问题,与软件工程师商谈创新的结构特性,与程序员商谈实现技巧,外观和风格。
Within systems engineering, quality attributes are realized non-functional requirements used to evaluate the performance of a system.
挖掘/提炼需求描述的能力:客户/产品的prd往往描述得比较局部,会缺少一些上下文或者产品上认为不重要的部分 ,相信很多同学还经常收到一句话需求,更有甚者就简单地说和XXX产品一样。这时候就需要架构师将需求的迷雾层层拨开,了解需求详情、业务目标,了解不同的利益相关方动机、关注点;
架构设计能力:关键架构需求中,有一个点就是架构师及团队的经验和特长,比如我们用lindorm更多,那么我们在选型中就会更青睐于lindorm,因为这对项目的开发、运维成本以及进度风险会更低;但与此同时,我们也要承担这个选型带来的一些缺点;所以架构设计方法除了本文讲到的方法论,还很依赖于架构师的技术前沿性。就像一位前辈所说的,“每时每刻都在发生技术的升级和变革,只有持续不断的学习,才能对老的架构有新的认识,对于老的问题产生新的解法,要了解业界最近在发生什么变化,这个领域最关键的项目和人在做什么,学习他们的技术,学习他们的论文”;
代码落地能力:架构师要能够和开发工程师一起完成代码编写落地,这块本文不过多赘述;
2.2 方法论
三、纵向架构师思维
3.1 架构是什么
The fundamental organization of a system, embodied in its components, their relationships to each other and the environment, and the principles governing its design and evolution.
系统模块架构
模块:系统分层模块,应用模块,外部服务模块;
关系:模块依赖关系,模块数据流转方式;
原则:模块划分方案。
对象架构
模块:类,有哪些核心类,类职责是什么;
关系:类继承关系,类与类之间的依赖关系;
原则:类设计指导思想和关注点;
数据存储架构
模块:库表模块,描述有哪些数据库、有哪些表,如何做分库分表,数据库如何选型;
关系:表关联关系,不同数据表之间的一对多/一对一等依赖关系;
原则:数据存储选型/设计方案和关注点,设计考虑的核心质量属性;
3.2 架构师要解决什么问题
约束:给定的不可更改的设计决策;
质量属性:评估系统性能的非功能性需求,是利益相关方判断软件系统是否好用的一切外部可见特性,包括响应耗时、sla、可伸缩性、可用性、可维护性、可测试性等;其表征系统在特定环境下的运行情况,设计决策一般都会提高或者抑制一个质量属性,比如性能和可读性等。这点非常重要,在模式选择的时候,需要基于质量属性进行抉择;
影响较大的功能需求:需要特别注意的特性和功能;
其他影响因素:架构师/团队的经验、特长等;
3.3 怎么解决架构师要解决的问题
3.3.1 了解需求
3.3.1.1 利益相关方
3.3.1.2 关键架构需求
约束:约束是架构中不能被打破的原则性前提;如果描述的语法类似于“要尽可能地xxx”、“如果能xxx那就更好了”,这种就不属于约束,约束的语法应该是“必须xxx”。由于约束的不可变性,我们要尽可能减少约束性需求。可能是产品链路还没有思考清楚,可能是业务模型还没有理清,水平不高的产品往往并不知道新增约束会带来的后果,由于新增约束通常会带来架构设计上的便利,加速项目功能上线,这些产品在前期会给出很多并不恰当的约束,导致后续技术债难还,这就需要架构师慧眼识珠了。
质量属性:质量属性与功能性需求无关,所以很容易被忽略。但是在模式选择和方案设计的时候,通常需要基于质量属性做设计决策。因此要有一个质量属性优先级的排名,比如说数据一致性、事务性、可测性、数据准确性等,我们可以通过质量属性网格或图表,对利益相关方关注的事项和原始质量属性场景进行提取、分类、完善、排定优先级,可视化头脑风暴和与利益相关方的沟通结果。如下图,是我摘自《List of Quality Attributes for Grid Monitoring Tools》的一个示例。
影响较大的功能需求:需要特别注意的特性和功能;
其他影响因素:虽然它叫“其他影响因素”,但是它对最终架构选型至关重要;架构师和团队的经验会大大影响我们对技术框架的使用和对基础存储的选择,因为熟悉的事物对项目的进度风险,对后续的运维成本,都会小很多。所以架构师要不断学习新的知识,了解技术升级和变革。
3.3.1.3 用例集
3.3.2 架构描述
视点:从不同的视角或者专业领域来看待系统的方法。
C4模型
上下文层次(Context):描述了系统与外部实体(如用户、其他系统、硬件设备等)之间的关系,显示系统如何与其周围的环境交互以及其外部依赖关系。 容器层次(Container):系统内的软件被分解成多个容器,如应用程序、数据库、文件系统等,容器图描述了这些容器之间的关系及它们如何共同工作以实现系统的功能。 组件层次(Component):在容器的内部,每个容器被进一步拆分为组件,如类、模块、服务等,组件图描述了组件之间的关系和依赖关系,以及它们如何协同工作。 代码层次(Code):这是最低层次,描述了每个组件的内部实现细节,通常可以是类图、包图等,用于展示组件内的代码结构。 ——Simon Brown
4+1视图模型
场景视图:从外部视角,描述系统的参与者(用户)与系统功能用例的关系。反映的是系统的最终用户需求和交互设计。
逻辑视图:从结构化视角,描述该系统对用户提供的所需功能服务所具备的组件结构和数据结构,以及一些边界约束条件,清晰地描述给用户提供的功能需求服务是如何构建的。描述该系统内部所具备了哪些组织结构,以达到实现对外功能。 开发视图:从结构化视角和行为视角,去描述实现系统功能的各个组件和模块是如何实现的。 处理视图:从行为视角,描述系统各个组件和模块是如何进行通信的。 物理视图:从交互视角,描述系统可以部署到哪些物理环境(如服务器、PC端、移动端等)上和软件环境(如虚拟机、容器、进程等)上。 ——Phillipe Krutchen
3.3.3 业务模型
a、从准确的用例中剥离出名词; b、根据名词梳理领域模型和其属性; c、根据名词的修饰梳理出属性值; d、根据名词的定义完善属性值; e、从用例集合中剥离出动词&形容词; f、根据动词&形容词梳理出领域模型之间的关系;
3.3.4 软件系统模型
3.3.4.1 系统模块架构
3.3.4.2 对象架构
实体:由标识定义,而不依赖它的所有属性和职责,并在整个生命周期中有连续性。这句话在初看的时候非常晦涩,简单来说,就是一个标识没变的对象,在其他自身属性发生变化后,它依然是它,那么它就是实体;举个例子,一个订单的收货地址,收货人电话,都发生了变化,但是这个订单id没有变,那么这个订单依然是变化前的那个订单,只是它的一些属性发生了变化;通过这种方式来识别实体的目的,是因为领域中的关键对象,通常并不由它们的属性定义,而是由可见的/不可见的标识来定义,且有完整的生命周期,在这个周期内它如何变化,它都依然是它;通过这种方式识别出实体这种领域关键对象,也是领域驱动设计和数据驱动设计最大的差别,数据驱动设计是先识别出我们需要哪些数据表,然后将这些数据表映射为对象模型;而领域驱动设计是先通过业务模型识别出实体,再将实体映射为所需要的数据表。不过前面也提到,实体的标识可以是可见的,也可以是不可见的,因为有很多域内无持久化的系统,在它们的对象模型中,并不存在可见的唯一标识id,所以在我之前的另一篇文章,也提供过不一样的描述实体的思路:
对于更加关注"行为"而非"唯一性"的纯计算型应用,给出划分实体与值对象的另一种思路: 1、实体是会对自身属性做出强解释行为的类型。 2、值对象是轻属性解释,重属性设计的类型。 理由是,纯计算型应用,业务关注重点是行为,当一个类需要承载复杂的计算逻辑,即对自身属性需要进行强解释行为时,它往往就承载了系统中更重要的职责,能更加凸显领域业务概念。
值对象:用于描述领域的某个方面而本身没有唯一标识的对象。被实例化后用来表示一些设计元素,对于这些设计元素,我们只关心它们是什么,而不关心它们是谁。举个例子,一个订单的收货地址Address对象有省份、城市、街道、门牌号这几个属性,其中的门牌号从111修改成222后,它就已经不再是修改前的那个它了,因为门牌号222并不等于门牌号111的地址。即它是没有生命周期的,它的equals方法由它的属性值决定(实体的equals方法由唯一标识决定);
聚合:聚合是一组实体和值对象的组合;内部包含一个聚合根,和由聚合根关联起来的实体和值对象;比如说有商品、sku、库存三个实体,那么在商品模型中,商品就是聚合根,其内部通过sku id关联它的sku,通过库存id关联商品/sku的库存;聚合将这组关联关系建立,对外提供统一的操作,比如需要删除某个商品,那么这个聚合的内部可以在一个事务(或分布式事务)中,对库存进行清空,对sku进行删除,最终对商品进行删除。
服务:有一些对实体/聚合/值对象进行编排操作的概念并不适合被建模为对象,那么它应该被抽象为服务,化作一只上帝之手,做领域对象间流程操作的编排。服务很重要的特征,它的操作应该是无状态的。
工厂:当创建一个实体对象或聚合的操作很复杂,甚至有很多领域内部的结构需要暴露的时候,就可以用工厂进行封装。一种相对简单粗暴的判断方法是看这个类的构造方法实现是否复杂,并且看着这些逻辑不应该由这个类实现,那么不妨用工厂来构造这个对象吧!
仓库:仓库是可持久化的领域对象和真实物理存储操作之间的媒介,随意的数据库查询会破坏领域对象的封装,所以需要抽象出仓库这种类型,它定义领域对象的获取和持久化方法,具体实现不由领域层感知;至于具体用了什么存储,如何写入和查询,是否使用缓存,这些逻辑统一封装在仓库的实现层,对于后续迁移存储、增删缓存,都可以做到不侵蚀业务领域。
防腐层:防腐层并不是一个特定的对象类型,而是一种领域模型保护的思路;对于领域外界的变化,我们需要持悲观的态度,因为领域外部的模型不受我们控制,它们的变化轨迹难以捉摸,所以在系统与系统直接,上下文与上下文之间,要有一层放腐层进行领域内外的模型转换。
创建者 问题:谁负责产生类的实例 解决方案:如果符合下面的一个或者多个条件,则可以将创建类A实例的职责分配给类B B包含A B聚合A B拥有初始化A的数据并在创建类A的实例时将数据传递给类A B记录A的实例 B频繁使用A 信息专家 定义:如果某个类拥有完成某个职责所需要的所有信息,那么这个职责就应该分配给这个类来实现。这时,这个类就是相对于这个职责的信息专家。 解决方案:将职责分配给拥有履行一个职责所必须信息的类(域)。 低耦合 问题:怎么样支持低的依赖,减少变更带来的影响,提高重用性? 解决方案: 在类的划分上,尽量创建松耦合的类,修改一个类不会影响其他类。 在类的设计上,尽量降低类中成员和方法的访问权限,尽量将类设计为不变类。 在类的引用上,将一个对象对另一个对象的引用降低到最小。 高内聚 问题:如何使得复杂性可控? 解决方案:功能性紧密的相关职责应该放在同一个类中,并共同完成有限的功能。这样做更加有利于对类的理解和重用,也可以降低类的维护成本。 纯虚构 问题:当不想破坏高内聚和低耦合的设计原则时,但是有些职责又没地方放,如何处理 解决方案:将一组高内聚的职责分配给一个虚构的或者处理方便的类,它并不是问题域的概念,而是虚构的概念,以达到支持高内聚低耦合和重用的目的。 间接 问题:如何分配职责,以避免两个事物之间的直接耦合? 解决方案:当我们不知道将职责分配给何种模型的时候,可以看看是否可以将职责分配给中介模型。
3.3.4.3 存储架构
四、写在最后
参考:
《DDD中常提到的应用架构总结(六边形、洋葱、整洁、清晰)》:https://code84.com/730128.html
《List of Quality Attributes for Grid Monitoring Tools》:https://www.researchgate.net/publication/251818235_List_of_Quality_Attributes_for_Grid_Monitoring_Tools
https://c4model.com/
《架构师修炼之道》,作者:Michael Keeling,译者: 马永辉 / 顾昕,出版社:华中科技大学出版社
《领域驱动设计,软件核心复杂性应对之道》,作者:Eric Evans,译者:赵俐 / 盛海艳 / 刘霞,出版社:人民邮电出版社
实时可观测,即时应对风险
为了保障业务稳定性、提升客户满意度,运维监控告警与故障定位(运维)、检测与防范安全威胁(安全)、业务数据分析(运营)成为研发运维团队面临的难题。本方案使用日志服务(SLS),基于采集的日志数据实现对业务与 IT 系统的监控告警与问题排查,解决性能优化、安全保障、业务分析和用户体验提升等痛点。
快点击阅读原文查看详情吧~
微信扫码关注该文公众号作者