从公众号转载,关注微信公众号掌握更多技术动态
---------------------------------------------------------------
一、架构设计的目的
1.什么是复杂的软件项目
复杂的软件项目通常有两个特点:
需求不确定
技术复杂
技术的复杂性主要体现在四个方面:
需求让技术变复杂:软件要能不断响应新的需求
人员让技术变复杂:团队成员水平不一,擅长的技术方向也不一样
技术本身复杂:技术本身的使用门槛较高
保证软件稳定运行是复杂的:运行时的不确定性
2.软件复杂性
(1)复杂性的表现形式
①症状1-变更放大
变更放大(Change amplification)指得是看似简单的变更需要在许多不同地方进行代码修改。比较典型的代表是Ctrl-CV式代码开发,领域模型缺少内聚与收拢,当需要对某段业务进行调整时,需要改动多个模块以适应业务的发展。
②症状2-认知负荷
认知负荷(Cognitive load)是指开发人员需要多少知识才能完成一项任务。使用功能性框架时,我们希望它操作简单,部署复杂系统时,我们希望它架构清晰,其实都是降低一项任务所需的成本。盲目的追求高端技术,设计复杂系统,增加学习与理解成本都属于本末倒置的一种。
③症状3-未知的未知
未知的未知(Unknown unknowns)是指必须修改哪些代码才能完成任务,或者说开发人员必须获得哪些信息才能成功地执行任务。这一项也是John Ousterhout教授认为复杂性中最糟糕的一个表现形式。
当你维护一个有20年历史的项目时,这种问题的出来相对而言就没那么意外。由于代码的混乱与文档的缺失,导致你无法掌控一个500万行代码的应用,并且代码本身也没有明显表现出它们应该要阐述的内容。这时“未知的未知”出现了,你不知道改动的这行代码是否能让程序正常运转,也不知道这行代码的改动是否又会引发新的问题。这时候我们发现,那些“上帝类”真的就只有上帝能拯救了。
(2) 永远追求最优雅
业务简单的系统不应用DDD架构,弱交互场景也无需进行前后端分离。不要盲从一些教条的观念,选择适合自己的,控制在可控制范围内,既不过度也不缺失。毕竟没有绝对的优雅,甚至没有绝对的正确。
3.架构设计如何解决“复杂”
因为技术的复杂性,会导致软件开发变得很复杂,开发成本高。而架构设计恰恰可以在这些方面很好地解决技术复杂的问题。
主要从四个方面来:
架构设计可以降低满足需求和需求变化的开发成本:通过对系统抽象和分解,把复杂系统拆分成若干简单的;对需求的变化,已经有一些成熟的架构实践。
架构可以帮助组织人员一起高效协作:通过抽象再拆分,可以把复杂的系统拆分成开发人员可以各自独立完成的模块。
架构设计可以帮助组织好各种技术:如分层架构
架构设计可以保障服务稳定运行:如分布式架构、异地多活等
业务与技术的隔离。以业务为核心,分离业务复杂度和技术复杂度。
内部系统与外部依赖的隔离
系统中常变部分与不常变部分的隔离
隔离复杂性(把复杂性的部分隔离在一个模块,尽量不与其他模块互动)
4.什么是架构设计
架构设计的方法都是基于工程领域分而治之的策略,本质上就是将系统分拆,将人员分拆。但是光拆还不够,拆完了还能拼回来,所以咬清楚架构设计的“道”。
架构设计的道,就是组织人员和技术把系统和团队拆分,并安排好切分后的排列关系,让拆分后的部分能通过约定好的协议互相通信,共同实现最终的结果。
5.如何做好架构设计
业界已经有了很多成熟的架构设计模式,不需要闭门造车,可以在理解清楚业务需求后,找到相近的架构设计,然后基于成熟的架构设计方案,进行改造,变成适合自己业务需求的架构。可以按以下步骤进行。
先模型,再接口,最后 是实现
(1)分析需求
需要对产品需求进一步进行抽象。一个常用的分析方法就是分析用例,也就是了解主要用户校色和其使用的场景。
(2)选择相似的成熟的架构设计方案
在了解清楚需求后,就可以从业界成熟的架构设计模式中选取一个或几个。具体选择 哪些架构设计模式,需要根据平时的学习积累来做判断。
在选好架构方案后,还需要考虑选择什么语言和开发框架。这部分选择需要根据团队情况和项目情况来综合评定。
(3)自顶向下层层细化
从整体到局部,不要过早陷入技术细节中。
①部署架构
②分层和分模块
③模块之间的交互关系
比较常见的系统之间的交互方式有两种,一种是同步接口调用,另一种是利用消息中间件异 步调用。第一种方式简单直接,第二种方式的解耦效果更好。
比如,用户下订单成功之后,订单系统推送一条消息到消息中间件,营销系统订阅订单成功 消息,触发执行相应的积分兑换逻辑。这样订单系统就跟营销系统完全解耦,订单系统不需 要知道任何跟积分相关的逻辑,而营销系统也不需要直接跟订单系统交互。
除此之外,上下层系统之间的调用倾向于通过同步接口,同层之间的调用倾向于异步消息调 用。比如,营销系统和积分系统是上下层关系,它们之间就比较推荐使用同步接口调用。
④API设计、数据库设计、模块设计(业务逻辑)
数据库和接口的设计非常重要,一旦设计好并投入使用之后,这两部分都不能轻易改动。改动数据库表结构,需要涉及数据的迁移和适配;改动接口,需要推动接口的使用者作相应的 代码修改。这两种情况,即便是微小的改动,执行起来都会非常麻烦。因此,在设计接 口和数据库的时候,一定要多花点心思和时间,切不可过于随意。相反,业务逻辑代码侧重 内部实现,不涉及被外部依赖的接口,也不包含持久化的数据,所以对改动的容忍性更大。
(4)验证和优化架构设计方案
二、设计文档
1.概要设计
在概要设计阶段,一般以子系统为维度来阐述系统各个角色之间的关系。对于关键的子 系统,还会进一步分解它,甚至详细到把该子系统的所有模块的职责和接口都确定下 来。
这个阶段的核心意图并不是确定系统完整的模块列表,焦点是整个系统如何被有 效地串联起来。如果某个子系统不做进一步的分解也不会在项目上有什么风险,那么并 不需要在这个阶段对其细化。
为了降低风险,概要设计阶段也应该有代码产出。 这样做的好处是,一上来我们就关注了全局系统性风险的消除,并且给了每个子系统或模块 的负责人一个更具象且确定性的认知。 代码即文档。代码是理解一致性更强的文档。 经过系统的概要设计,整个系统的概貌就了然于胸了。
2.详细设计
详细设计阶段,是需要各个子系统或 模块的负责人,对他负责的部分进行进一步的细化。详细设计关注的是子系统或模块的全貌。概要设计不一定会把子系统或模块的完整接口都列出来, 实际上它只关注最核心的部分。但是从详细设计角度来说,接口描述的完备性是必需的。 详细设计并不是只谈实现就完事,更不是一个架构图。它包括以下这些内容。
(1)现状与需求
现在在哪里,遇到了什么问题,要做何改进。从逻辑自洽的角度,任何一篇文档,首先关注的都应该是要解决的问题与目标。 现状与需求的陈述,要简明扼要。 现状更多的是陈述与我们要做的改变相关的重要事实,侧 重点在于强调这些事实的存在性和重要性。 假设要对某个模块重构。那么,现状就是要谈清楚现在的业务架构是怎样的?它 到底有什么样的问题。
需求陈述是对痛点和改进方向的一次共识确认。痛点只要够痛,大家都知道,所以同样不需 要长篇累牍。 每个子系统或模块,都有自己的角色分工与用户故事。不用重新做一遍需求分析,但对 需求分析的核心结论,在详细设计开始之前需要明确。 这很重要。它是我们详细设计所要满足的业务目标。
(2)需求满足方式
要做成啥样?交付物的规格,或者说使用界面(接口)。规格,或者说使用界面,体现的是别人要怎么使用。 使用界面(接口)应该自然体现业务需求,就是强调程序是为用户需 求服务的。而架构设计,在需求分析与后续的概要设计、详细设计等过程之间也要有 自然的延续性。
使用界面这一部分要详细写,它是团队共识确认的关键。 我们的交付物有哪些可执行文件,有哪些包(package)?如果可执行文件,那么它是一个 界面程序,还是服务?如果是服务,网络协议是什么样的?如果是包,它又包含哪些公开的 类或函数。
更需要强调的是,使用界面的稳定是至关重要的。对使用界面的不兼容调整,可能出现严重的后果。技术上,可能会导致客户异常,出现编译
失败需要重写代码,或者更严重的是,可能导致他们的系统崩溃。商业上,则可能导致大量 的客户流失。接口的变更需谨慎!
没有页面写接口得变更
(3)程序 = 数据结构 + 算法
①数据结构
数据结构从大的层面分,可分为基于内存的数据结构,和基于外存(比如 SSD 盘)的数据 结构。在服务端我们谈数据结构,谈的不是内存数据结构,往往谈的是数据库的表结构设计。
不管我们用的是哪种数据库,出于惯例我们往往还是以 “定义表结 构” 一词来表达想干什么。其实定义表结构和定义内存数据结构本质是完全一致的。定义内存中的一个类 (或结构体),我们也关心字段名(成员变量名)和类型,也关心字段的含义,以及它是否 指向另一个类(或结构体)的某个字段(成员变量)。
②算法
在架构过程中,需求分析阶段,我们关注用户需求的精确表述,会引入 角色,也就是系统的各类参与方,以及角色间的交互方式,也就是用户故 事。 到了详细设计阶段,角色和用户故事就变成了子系统、模块、类或者函数的 使用界面(接口)。使用界面(接口)应该自然体现 业务需求,就是强调程序是为用户需求服务的。而我们的架构设计,在需求 分析与后续的概要设计、详细设计等过程之间也有自然的延续性。 所以算法,最直白的含义,指的是用户故事背后的实现机制。
那么,怎么描述一个用户故事对应的算法?
基于 UML 时序图(Sequence Diagram)。
基于伪代码(Pseudo Code)。在逻辑较为复杂时,伪代码往往有更好的呈
现效果。
三、如何做好技术选型
1.项目决策
(1)问题定义
问题定义阶段需要明确两个问题:
为什么需要技术选型
技术选型目标是什么
只有明确了技术选型的目标,才有一个标准来评判该选择哪一个方案。
(2)调研
在明确技术选型的目标后,需要进行调研看有哪些技术选型可以满足目标,可以从这几个方面去分析:
是否满足技术选型目标
是否满足时间、范围和成本的约束
是否可行
有什么样的风险?是否可控
优缺点是什么
(3)验证
可以通过一个快速原型项目,用候选技术方案快速做一个原型出来,做的过程中才知道,所做的技术选型是否真的满足技术选型的目标。
(4)决策
在调研和验证完成后,需要召集所有利益相关人一起,就选择的方案做一个调研结果评审的会议,做出最终的决策。
2.架构思维
架构设计是要控制技术的复杂性。对于架构师来说,要控制技术复杂性,有几种有效的方式:抽象、分治、复用和迭代。架构师思维其实就是这几种思维的集合。
抽象思维:对需求进行抽象建模后,可以帮助我们隐藏很多无关紧要的细节,在高层次的架构设计时,可以关注在几个主要的模型上,而不必关心模型内的细节实现。
分治思维:架构设计的一个重点,就是要对复杂系统分而治之。
复用思维:通过对相同内容的抽象,让其能复用于不同的场景,是一种非常简单的提升开发效率的方法。
迭代思维:好的架构通常不是一步到位,而是先满足好当前业务需求,然后随着业务的变化而逐步演进。
3.架构选型这注意点
(1)产品选型要服从于项目整体目标
局部最优的选择拼装在一起未必是全局最优的方案。如果你的目标是要对整个应用系统做彻底重构,例如把单体架构改为微服务架构,那么要解决原来某些局部的问题,可能会有 更多选择。这时候要从整体上评估技术复杂度、工程实施等因素,而不是仅选择局部最合 理的方案。
(2)先进的产品可能会延长项目交付时间
最先进的产品不一定是完美的选择。尤其是有进度要求时,往往会选择更稳妥、快速的办 法。但是,这本质上是在短期利益和长期利益之间做权衡,没有绝对的对错,搞清楚你想要的是什么就行。
(3)当产品选型可能导致业务流程变更时,请慎重对待
对任何项目来说,协作范围的扩大一定会增加实施难度。当技术部门对业务流程变更没有决定权时,我认为这是多数情况,通过技术手段避免这种变更往往是更好的选择。
(4)评估技术潮流对选型影响
跟随潮流并不是人云亦云,你必须能够独立对技术发展趋势做出研判。太过小众的技术往 往不能与工程化要求兼容。但同时,保持对新技术的敏感度和掌控力,也是非常必要的。
四、技术债务
1.什么是技术债务
范围不减、成本不加,还想节约时间,就会影响到质量。技术债务就是软件项目中对架构质量和代码质量的透支。
技术债务具有以下特点:
有利息:后期对软件做修改的时候,需要额外的时间成本。
不一定都是坏的:如快速原型模型,就是通过技术债务的方式快速开发快速验证。验证不可行,则无需偿还债务。
2.如何管理
技术债务有利息也有收益,如何管理才能保证软件项目中的收益大于支付的利息。
(1)识别债务
软件项目中有很多指标来发现存在的技术债务:
开发速度降低
单元测试覆盖率低
代码规范检查的错误率高
Bug数量越来越多
(2)处理技术债务策略
在识别之后,解决技术债务有三种策略:
重写:推翻重来,一次还清
维持:修修补补,只还利息。适用于不需要增加新功能的系统
重构:新旧交替,分期付款
(3)实施策略
重写-正式项目来立项
重构-将任务拆分并进行跟踪
维持-制定计划
(4)预防
最好的方法是预防技术债务的产生:
预先投资:好的架构,高质量的代码是一种技术投资
不走捷径:做好代码审查、保障单元测试代码覆盖率等
及时还债:记下欠的债务,及时还债。