软件
- 数据-模型
- 算法-流程
- 封装-集成
- 实现-连接
架构总览
目标
- 识别出问题的关键复杂性,建立稳定有效的领域模型。
- 大幅降低开发与维护成本。 可以落定到人力与时间。
- 系统脉络更清晰。可以绘制出系统模块的依赖图,通过依赖图的节点与节点链接数量来判断。
- 更稳定。操作失败、出现各种问题的比率。
- 更容易操作。 操作的步骤数 乘以 操作失败的比率。
- 弹性和灵活性。 支持新业务的难度,以新增代码量和修改代码量来衡量。
具体目标
-
是不是有针对性地解决了问题固有的复杂性? 通过提供渐进稳定的领域模型,让问题理解、描述和求解都更清晰自然了;
-
是不是令需求更快地实现,大幅降低了开发成本? 比如原来要花时间修改代码、测试、发布系统,后续只要新增配置,测试并刷新配置就搞定了;原来需要5人日,现在只需要 2人时;
-
是不是让系统的依赖更清晰了?比如原来你调我我调你,乱成一团,现在能够清晰地看到数据流在各个模块的流通和流向、脉络;
-
是不是更稳定了?比如原来磕磕碰碰,做百次操作就有一次失败,现在做十万次操作才有一次失败;
-
是不是更容易操作了?比如原来需要ABCDE五步操作,现在一键无忧完成;
-
是不是足够有弹性和灵活性,可以支撑数年的业务发展? 比如原来遇到新业务就要做许多兼容,现在只要配置一些规则、流程就能适应新业务的发展。对新增开放,对修改关闭。
根本原则
- Separation of Concerns (SoC) – 关注点分离:使用者不用关心它的内部实现细节
- 标准化
- 抽象与包装
- Keep It Simple, Stupid (KISS)
工程实用原则
- High Cohesion & Low/Loose coupling – 高内聚, 低耦合
- Program to an interface, not an implementation - 基于接口编程
- Command-Query Separation (CQS) – 命令-查询分离原则
- Convention over Configuration(CoC)– 约定优于配置原则
- Common Closure Principle(CCP)– 共同封闭原则 :一起修改的类,应该组合在一起
- Common Reuse Principle (CRP) – 共同重用原则: 没有被一起重用的类不应该被组合在一起
- Hollywood Principle – 好莱坞原则
- 由容器控制程序之间的关系,而非传统实现中,由程序代码直接操控。这也就是所谓“控制反转”的概念所在
- 不创建对象,而是描述创建对象的方式。在代码中,对象与服务没有直接联系,而是容器负责将这些联系在一起。控制权由应用代码中转到了外部容器,控制权的转移,是所谓反转。
- Design by Contract (DbC) – 契约式设计:对软件系统中的元素之间相互合作以及“责任”与“义务”的比喻
- 通过基类的接口调用一个对象时,用户只知道基类前提条件以及后续条件。因此继承类不得要求用户提供比基类方法要求的更强的前提条件,亦即,继承类方法必须接受任何基类方法能接受的任何条件(参数)。同样,继承类必须顺从基类的所有后续条件,亦即,继承类方法的行为和输出不得违反由基类建立起来的任何约束,不能让用户对继承类方法的输出感到困惑。
- Acyclic Dependencies Principle (ADP) – 无环依赖原则:依赖结构中不允许出现环
- 第一种方法是创建新的包,如果A、B、C形成环路依赖,那么把这些共同类抽出来放在一个新的包D里。这样就把C依赖A变成了C依赖D以及A依赖D,从而打破了循环依赖关系。
- 第二种方法是使用DIP(依赖倒置原则)和ISP(接口分隔原则)设计原则。
准则
- 自然清晰
- 容错处理
- 减少了错误发生的可能性。
- 错误发生时,更安全友好地处理。
- 不会因为次要局部影响整体。
- 减少了故障可能性,或可能故障级别。
- 故障发生时,能够更快更安全地处理和恢复正常。
- 性能与稳定
- 扩展能力
- 底层模型统一。
- 核心简洁而稳定,外围可扩展。
- 适当地分离关注点,组合和组织关注点。
- 组件化、配置化,通过增减插件来支持需求。
- 弹性扩展
- 可复用
- 配置化
- 依赖弱化
- 最小复杂
- 整合能力
目标细则
- 功能诉求
- 稳定性优化
- 性能提升
- 维护成本
- 弹性
- 数据
- 体验
- 及时
- 安全
- 扩展
- 重构
设计细则
-
文档说明: 当设计变更涉及较大变动时,建立文档说明改动点及缘由。
-
API :继承不可超过两层,避免嵌套;避免将不相关的东西混杂在 API 参数中;避免将底层实现细节暴漏在API 参数与传参中。
-
分层: 提炼出一系列关注点,分离到不同的语义层次,分离到多个类的单一职责中。
-
组件化: 将工程里的代码与功能实现抽象为组件接口与实现。
-
策略模式: 使用策略模式分离同一个接口的不同实现,并根据场景选择适宜的实现。
-
插件流程: 如果流程是可变的,那么将单个流程节点变成可配置的插件,并进行编排。
-
启动检查: 当应用启动时,加载所有必要组件,任一不满足时及时报错退出,避免错上加错。
-
使用切面: 当多个功能要复用同一个前置或后置逻辑时,使用切面来实现这些前置或后置逻辑。
-
受控线程池: 切忌在应用里动态创建单个线程或线程池;使用全局受控的线程池来执行任务。
-
重载函数: 使用重载函数建立适合的工具类。
-
无状态: 除非必要,不要在实例间共享状态;不要让请求的处理结果依赖于某个状态。
-
快速失败: 当前置要件不满足时,快速失败胜于自以为的智能容错处理。
-
事务: 多个关联操作的原子性和一致性保障。
-
幂等: 处理多个完全相同的请求时,与处理一次的效果相同。
-
范式: 在关系型数据设计中,要遵循基本的规范范式。
-日志: 在开发时,打印合适级别的必要的日志(关键路径和关键状态),方便快速排查错误。
- 来源监控: 如果有多个来源或类型,建立监控了解每个来源或类型的业务量及占比。
架构落地
领域建模
- 从产品、业务、客服、运营、线上线下等多角度去看待事物
- 一个设计优雅的系统,必定含有一组核心概念集合,这些概念之间存在紧密的联系。
系统划分
- 单个应用。单个应用内的组件化,主要是将单一关注点的逻辑功能组件化,能够灵活组合和配置。组件化之后,亦容易实现可定制化。
- 多个应用构成了更大粒度的领域服务组件
- 多个特定领域服务组件构成更大粒度的行业服务
应用模型
应用模型涉及到数据处理与响应的全局流程和交互方式。
- 请求-响应模型
- 流处理模型:海量数据
- 分布式模型
- 人机交互模型
架构模式
- 分层模式:
- RESTFUL模式
- mvc
- 插件:
- 订阅消费:订阅-消费模式需要高稳定可用的消息中间件,并仔细评估消息延迟对用户活动造成的影响。新品消息推送、商品消费订阅、发货提醒等,
- Pipeline模式:管道-过滤器-处理器链:一个请求沿着管道连接的处理链,依次由链上的过滤器、拦截器、处理器进行处理并返回
- 事件驱动: 在系统内定义一系列的组件、事件及监听器,组件发生变化时触发事件,通知相应的监听器处理事件更新组件,进而触发新的事件,如此循环直至手动终止系统或系统崩溃。适用于GUI应用开发
- Actor模式:基于事件驱动的分布式的、异步并发的、可伸缩的、有故障恢复能力的大型消息处理架构
- 规则-工作流模式:将系统分析成一系列的工作流节点以及规则的解析匹配,使用规则引擎来控制和运行,通过添加规则及规则流,实现可扩展性和可配置性。
存储设计
-
存储设计是领域建模的设计细化,决定数据的结构以及领域对象的表达。
-
存储设计通常要考虑读写操作的性能、
容量、并发、事务、搜索。 -
关系型
-
数据库设计很大程度上是领域模型的表达和呈现;因此领域模型的质量决定了数据库设计完成后是否需要做频繁的变更;
-
尽可能采用规范化的关系范式,除非有足够理由打破范式。
-
仔细设计实体映射关系,尤其是一对多关系时;仔细设计外键引用。通常是多的方引用一的方的主键作为外键。
-
仔细设计字段名称、类型。字段具有单一含义,名称尽量贴切而具有描述性;主键类型通常采用自增的 bigint;字符串尽可能采用VARCHAR;枚举采用字符串更易理解;日期采用Date;增加极少量的扩展字段;加上字段注释COMMENT。熟悉和使用字段设计套路可提升设计效率。
-
根据业务查询和更新场景,仔细设计好索引。设计组合索引时,区分度高的字段放前面。索引应少而实用,覆盖性高。熟悉和使用索引设计套路可提升设计效率。
-
考虑运维和排查问题的需要。比如日期采用字符串比时间戳更直观可读。
-
寻找经验丰富的高级开发者和DBA给出中肯的Review意见并进行完善。
-
-
大数据
- ES(Elasticsearch)是水平可扩展的可靠的分布式文档存储/查询/分析系统,通常用于海量数据的准实时联合搜索与分析,提供了RestFul接口风格的API。相比关系型数据库,ES的优点在于可以存储文档和对象的完整体,适合多字段的灵活的联合搜索和全文搜索;ES不适合于事务型应用以及对数据丢失零容忍的应用。通常使用DB+ES的组合,DB用于主存储,ES用于准实时联合搜索。
- Hbase适用于海量数据的存储,设计合适的 rowkey 非常重要,通常关乎性能、吞吐量和负载均衡。比如订单详情通常是获取单个或少量订单的详情,Rowkey设计更注重负载均衡,高位作为散列字段;
- Storm可用于实时数据同步和计算; Hive用于离线统计,提供了类SQL语句; Spark 用于离线计算。
模式系统
- 设计模式:实现特定需求的接近代码层面的设计套路,通常用于梳理和表达对象之间较为复杂的依赖和交互关系,将错综复杂的容易膨胀的难以理解和扩展的条件判断逻辑解开成一系列对象的清晰可理解的易扩展的交互结
- 业务模式: 业务模式是对业务规则和流程的常见相似性、以及特定业务的数据处理能力的提炼**,是业务应用系统中的常见套路
- 参数检测模式:调用身份检测、权限校验、空检测、时间参数检测、业务约束检测、存量检测;
- 健壮服务调用模式: 健壮地调用服务接口。若调用成功,则抽取数据部分;若调用失败,适当地打印错误日志和返回错误信息;
- 多源数据组装模式: 从多个服务接口源获取数据后进行组装,进行下一步健壮服务调用。
- 数据解析与转换模式:从一个对象转换到另一个对象;在对象、Map、JSON之间互转;属性拷贝。
- CRUD模式: 增删查改,经典的数据库访问模式。
- 接口正交模式:API接口是正交的无重叠的可灵活组合的,比如先搜索关键词列表再根据关键词列表获取详情,而不是在搜索接口中把详情数据一并返回。
- 幂等处理模式: 识别重复请求并进行相应处理(直接返回最近一次处理的结果、忽略、报错等)。涉及金额和数据一致性的地方尤其要细心。
- 事务处理模式: 多个数据存储操作作为一个原子性操作要么全部成功要么全部失败,不存在未完成的不完整的中间数据存储状态。
- 资源互斥同步模式: 多个线程同时写操作相同的资源时,进行资源互斥同步。
- 多任务模式: 将任务分解为多个子任务,启动多个线程或进程来执行子任务,然后汇总子任务的结果集得到最终的结果。
- 上下文模式: 对于长链路复杂请求处理,创建一个上下文对象,在该上下文对象里传递处理请求所需要的必要信息。最好仅将该对象作为函数或方法参数而非实例变量,避免引起不必要的并发问题。
- 架构模式: 没说
- 分析模式: 没说
技术设施
各种技术栈
api设计
api
实际权衡(实战经验)
当需求难以满足时,就是发现设计束缚的最佳时机
- 顺序结构简单可控,乱序呢?
- 针对一对一的实体关联的设计,简单易用易理解,一对多,多对多呢
- 一个字段的同步通常来源于一个表,可是有的字段来源于多个表,需要根据不同业务场景进行覆写
关注点-接口-组件-流-应用
。设计软件有时像设计游乐园,先构思各种微小景致(微组件),然后设计四通发达曲径通幽的小路(流)串联起所有的微小景致。
- 代码层面,使用设计模式进行对象交互结构的重构,使功能和服务实现更具柔性,容易修改和扩展;
- 模型层面,使用深化和显化领域概念,优化存储设计,使得领域模型更加清晰。
设计需要大量取舍,需要优秀的判断力
- 为了更优的体验而把事情弄复杂:
- 现代产品设计强调用户体验,有时用力过猛,反倒容易忽视简单性带来的隐形良好体验。人容易犯错。复杂而完美的事情,尽管往往蕴含了很多“贴心的思考”,可这些“贴心的思考”在真实场景未必成立或只是偶尔出现,或者受到更多限制而无法施展,甚至起到反制作用,令人困扰和沮丧。相反,简单的事物,尽管不完美,却预留了很多空间,人们总能想到“奇妙方法”应对现实的阻挠因素并广为传播,而这些“奇妙方法”反而成了事实上的解决方案。因此,产品设计应尽量简单而预留余地,流程尽量直线式单一化且短小,消减分支和重试,减少诱发出问题的潜在因素。而对于系统设计而言,要尽量做到关注点正交分离解耦,更好地支持产品的灵活性;而即使能够做到产品的灵活性,也要仔细斟酌是否必须做到如此。
- 过度设计可扩展性。预先思考过多,想做的更灵活,结果实际发展方向并非所料,增加了系统设计复杂
- 设计不可偷懒。尽管设计要提倡节制,可是也不能偷懒,在必要的地方设计不足。一旦设计不足,很快就会受到“惩罚”
数据结构
- 位图用来表示映射;
- 元组用来表示有意义的数据对;
- 数组、链表、集合、映射,是为了批量处理数据;
- 栈、队列是为了匹配特定的操作;
- 二叉树是为了处理层次性的组织结构;
- 图是为了处理复杂网状结构;
- 多维数组用来处理科学计算的矩阵;
- 正则表达式,是为了实现文本的模式匹配;模板,是为了描述可复用的文本;JSON 和 XML 主要考虑数据交换的标准化 。
数据建模
数据建模是根据需要对采集到的大量数据项赋予意义和有序化组织,从而更好地批量存储和处理数据。建模的主要依据:
- 数据项之间的属性关联。比如人的健康信息,身高、体重、心跳等;
- 数据项之间的映射关系。比如 1:1, 1:N, N:N 。 一个人有多个地址,一个地址也可能对于多个人。
控制结构
函数、组件、框架、模块、微服务、云服务是在各个层次上的可复用实体。在每个层次上,互补、连接、交换关系都是大规模复用的基本形式关系,在组合术语上略有侧重和不同。
- 比如函数级别上,侧重调用、委托和级联;组件级别上,侧重装配、重组和替换。函数级别更多通过内部实现优化功能;
- 组件级别更多的是重组和替换,而不是修改组件内部实现。交换是这些可复用的实体之间相互交换数据和信息的关系。
构建单元
-
组件是具有
接口和行为的高层次的功能单元,是高级别的复用和构建形式。组件需要仔细设计接口,其基本准则与函数相同,同时在组件行为上定义标准和规范,便于装配、重组和替换。 -
框架是通过
组件装配和交互而构建的高层次控制结构模板和半成品。框架使用者只要遵循框架的规范和约定,在模板里面填充有针对性的业务实体和逻辑即可(特化)。在使用框架时,应当定义系统与依赖的边界及接口,在系统与框架依赖之间建立适配层,在需要替换过时框架时,只需要更改适配层而不需要大动筋骨。这一准则适用于处理任何外部依赖。 -
模块与微服务是接近应用级别的高层次的构建单元,往往是
基于框架和组件而构建。其复用性相对较低,更适合于构建完整的应用服务。模块和微服务,更多侧重能够并发执行和分布式的部署与通信,且要高稳定性,因此,侧重在行为的定义和规范上要更加严格,避免频繁变更。
良好的架构实践
-
领域实体。 实体是针对领域内的事物的建模。领域实体具有属性和行为。
-
业务点。 业务点是针对业务判断的原子逻辑块。比如判断订单是否已支付、订单打标等。业务点通常只涉及极有限的业务变量,不依赖外部系统。
-
业务组件。业务组件是在业务点的基础上,构建起的具有一定业务功能的组件。比如关闭订单、对订单商品退款、扣减库存、交易信息落库。业务组件通常要符合一定的约束,采用一定的技术手段。比如调用外部服务获取数据,对订单的操作加锁,满足操作事务。
-
组件编排。有了业务组件之后,需要一个组件编排系统,定义若干阶段,在每个阶段能够执行相应的组件序列。通过执行组件序列,来完成整体业务流程。组件编排应当支持扩展点。
-
事件编排。如果多个行为具有级联特征,则可采用事件编排,组件变化触发事件,产生事件消息,监听事件消息实现业务组件处理,业务组件处理又触发新的事件,如此循环。
-
交互接口。当涉及多个系统的交互时,需要定义系统之间的交互接口。交互接口往往反映了系统所在领域的最简洁最核心的概念。
技术的实质是实现特定约束而设计的形式结构。比如事务、幂等、限流、并发、异步、缓存、降级、切面等。