软件的结构模式及结构的扩展

软件是逻辑的结构艺术。


软件是由多个组件通过结构关联组成的,而组件本身也是由更小的组件通过结构关联构成的。这里的组件既指数据组件也指代码组件。

要做好软件系统设计,结构的探究必不可少。幸运的是,前人已经探索和总结了很多设计结构模式供参考和适用。熟悉并掌握这些设计结构模式,无疑会事半功倍。


结构模式

基本结构模式

基本结构模式是组合数据与逻辑、构建程序的基本要素。

  • 数据结构:标量、数组(字符串、位向量、布隆过滤器)、链表(单链表、双链表、跳表)、列表、栈、队列、堆、多维表(多维数组、路由表)、哈希表(对象映射、信号与处理器表)、树(二叉树、B+树、红黑树、前缀树)、图(DAG、加权图、二分图)、JSON、XML、倒排索引。
  • 指令结构:指令、函数(算法)、高阶函数(函数的组合)。统称“逻辑单元”。
  • 对象结构:属性(数据结构) 与 行为(指令)的结合体。

数据结构组合

  • 数据项: 数据的原子单位。
  • 平铺: 具有相同特性的数据项组成数据结构的方式。
  • 聚合: 具有逻辑关联的数据项构成对象结构。

可阅:

设计结构模式

每一种设计模式都代表一种基本设计结构。设计模式是关于对象职责与交互的结构,也是接口交互的结构。设计结构模式通常用于系统的扩展。

1. 单例:组件为应用提供全局唯一访问点。通常与工厂模式联用。

2. 外观:为复杂系统提供简洁易用的访问接口。通常与单例模式联用。

3. 生成器: 逐步构造复杂对象。可与组合模式联用。

4. 适配器:为已有实现定义新接口。可用于各种逻辑结构的适配。

5. 原型: 复制已有对象来快速生产新对象。可与工厂模式联用。

6. 工厂/抽象工厂: 创建和输出需要的对象或工厂对象。可与单例模式、原型模式联用。

7. 模板: 子类与父类共同完成完整逻辑。可与策略模式、工厂模式联用。

8. 组合: 简单对象组合成复合对象,统一接口处理。可与迭代器、装饰器模式联用。

9. 策略: 在多种实现中选择。通常与工厂模式、单例模式联用。

10. 装饰: 添加动态功能,叠加灵活功能。可与策略模式、组合模式联用。

11. 代理: 为目标对象行为添加代理行为(保护、延迟、功能增强、监控统计等)。可与装饰模式联用。

12. 命令:将命令封装成命令对象执行,执行者无需知道命令详情,只要调用命令接口。可与组合模式联用。

13. 观察者: 变化(及变化级联)的通知。可与命令模式、中介者模式联用。

14. 状态:状态行为迁移。

15. 访问者:访问复杂对象的操作扩展。可与解释器、迭代器模式联用。

16. 迭代器: 访问容器元素的易用接口。可与组合模式联用。

17. 中介者: 建立对象的多对多关联的集中管理。

18. 责任链: 请求与处理器解耦。可与命令模式联用。

19. 备忘录: 撤销回退。

20. 解释器: 定义一种语法,构造这种语法的解释器,从而能够解决这种语法所能描述的问题。可与组合模式、访问器模式联用。

21. 享元:共享大量原子对象。节省空间。可用于符号、策略和状态共享。

22. 桥接:在一套标准接口与多套实现间建立关联。

可阅:

流程结构模式

流程结构模式是指逻辑单元的组合方式。流程结构模式用于构建完整的业务流程。

1. 管道:建立处理流水线,输入从第一个程序进入,前一个程序的输出传递到后一个程序的输入,以此类推,直到所有程序都处理完成,输出。

2. 并发:大数据集处理和多任务协作,使用多个计算单元并行或并发处理,协作产生最终结果。

3. 批量:大数据集处理,将大数据集分解为多个子数据集,每个子数据集作为一个批次处理。

4. 异步: 耗时长(或不确定)的接口处理,分解成主从部分分别独立处理,主单元用于及时响应,从单元具体处理。

5. 回调: 底层函数调用高层函数。为函数添加灵活性和可定制性。

6. 迭代: 通过固定计算公式,将上一次的结果作为下一次输入,反复运算,逐步逼近精确。

7. 遍历: 遍历某个集合或容器的每个元素进行处理。

8. 闭包: 使函数变量作用域“逃逸于”函数之外,实现保护变量、封装、Curry(函数组合)、延迟计算。

9. 保存点与回滚/重续: 消除失败操作的负面影响,或者从某个保存点重续操作。

10. 事务:多个操作(逻辑单元)的原子化。

11. 重试:重新执行有限次操作(一般用于因偶然波动导致的失败,如果程序有bug,重试会加重系统负担)。

12. 定时: 周期性地执行。

13. 延迟:在指定一段时间或者满足某种条件后才执行。

14. 轮询:每隔一段时间执行一次。 多用于乐观锁、更新状态、多路IO复用。

15. 切面:业务方法的通用逻辑(日志、统计、监控等),业务逻辑的“外围钩子方法”(执行之前、执行之后、执行前后的钩子)。通常使用注解与反射机制实现。

16. 缓存: 减少重复查询数据源或第三方服务的操作。先查缓存,缓存命中则会直接返回缓存数据,否则查后端底层数据源或第三方服务,然后将返回的数据放入缓存。

可阅:

应用结构模式

应用结构模式涉及到功能、模块之间的交互。做好全局应用结构的设计,将能够在增加大量功能的情况下依然保持系统全局结构的可维护性。

1. IOC: 定义接口,底层方法引用接口,高层提供接口实现。

2. SPI: 定义接口,提供多个实现,在配置文件中指定实现。用于服务供应商选择。

3. 组件编排:定义组件及顺序,然后编排组件来完成完整流程。

4. DAG: 建立组件依赖关系,根据依赖关系处理业务。

5. Pipe line: 建立工序流水线,多个工序共同完成完整功能。

6. 分层模式:高层依赖底层的服务,避免反向依赖。

7. MVC 模式:“模型-视图-控制”, 经典的WebUI架构模式,控制处理请求从而更新模型和返回视图,模型更新驱动视图更新,视图请求控制处理。

8. 钩子模式:在框架和流程中定义钩子接口,增强框架和流程的灵活性和可扩展性。根据具体业务提供钩子实现,在流程中把这些钩子串起来。一套流程适配多种业务。

9. 插件模式:定义插件接口和插件标准格式,提供插件实现,根据需要加载插件。通常用于定制化。

10. 订阅-消费:订阅某种主题,当主题有消息来时,消费该消息。

11. 事件驱动:组件变化产生事件,驱动事件监听器处理,引发组件变化,如此循环。

12. Actor模式:基于事件驱动的分布式的、异步并发的、可伸缩的、有故障恢复能力的大型消息处理架构。

13. 规则-工作流模式:将系统业务逻辑分解成一系列的规则集合及工作流节点的组合,使用规则引擎来控制和运行,通过添加规则及规则流,实现可扩展性和可配置性。

14. ORM: 应用程序对象到关系型数据库表的映射。对象模型与数据存储的映射。

15. Restful: 从资源视角来组织 API 和服务语义。全局规范一致的资源逻辑命名和返回码定义,关注资源与数据;无状态、声明式;对客户端隐藏实现细节;具有层次感、可读易理解。

16. 动态脚本: 使用动态脚本和脚本执行引擎增强功能的可配置能力。

17. 持久化: 命令日志与数据日志。

18. 健康检查:服务提供健康检查接口,由某个管理者定期检查服务状态,自动下线或重启失败的实例。

19. 授权认证服务: 单点登录,加密、授权认证。

模块交互模式

模块交互的设计理念只有一句话:高内聚、低耦合。对高内聚大概不太容易理解,咱先说说低耦合。

低耦合是说模块A不应当直接访问模块B的内部结构和内部数据源,不应该知道模块B的内部细节。否则,模块B只要一改动,很容易不知不觉影响到模块A的原有功能。要实现低耦合,比较理想的方式是,模块之间仅通过 API 来交互,不要直接调用其内部方法(尽管这是极方便而有诱惑的)。

模块交互模式主要有引用和依赖。

  • 模块引用:模块A使用了模块B的能力,但并不强依赖B的能力,换句话说,模块B即使跪了,也不影响模块A的重要功能的运行。模块A 能够对模块B实现熔断降级机制。比如检测流程模块引用了自定义规则模块的规则,但即使移除自定义规则模块,也不影响检测流程模块,因为检测流程模块也使用了系统规则模块。
  • 模块依赖:模块A引用了模块B的能力,且有强依赖关系。如果模块B跪了,模块A也不可用了。比如订单模块不可用,那么下单也就不可用了。下单对订单是强依赖。

可阅:

部署结构模式

部署模式指应用程序组件在计算机资源(云资源)上的分布和交互结构。

服务模式

  • IP端口模式:通过IP和端口来唯一确定某台主机上的应用程序提供的服务。
  • P2P模式:每个节点是对等的,均可提供相同的服务。
  • 负载均衡: 将访问流量合理分配到多个服务实例上。负载均衡侧重流量分配。
  • 路由模式: 通过路由将信号和数据在多站点进行路由转发,以抵达最终目标主机。典型就是路由器。路由模式侧重功能分配。
  • 中台模式:由基础服务和中台组成,向各种终端提供可复用的能力。基础服务实现能力沉淀,中台实现能力聚合。

拓扑结构

  • 主主模式:都可以作为读和写节点。
  • 主从模式:写主读从,主管理从工作。主作为写和管理节点,从作为读和工作节点。
  • 主备模式:主同步从。主出问题切备,主好了再切回来。
  • CDN 模式:在临近用户的位置部署节点,更快抵达用户。
  • 总线模式: 读和写都通过一条总线传输。比如 CPU 与主存之间的通信。又如人体中的动脉。
  • 通道模式:组件的交互通过在通道中传递数据和消息来实现。通道可使用消息队列实现。

结构的扩展

软件需求的变更,往往涉及到对现有结构的扩展和修改。

数据结构的扩展

  • 在某个对象和表里添加一个新字段,复制字段的值。一般改动较小。但如果涉及到结构不易变更的关系型数据库,在变更部署上要仔细做好。

流程结构的扩展

  • 在现有流程上加一个分支路径。一般用if-else 就可以解决,不过如果此处变动较多,会形成难以维护的代码堆砌。可使用策略模式解耦。
  • 在现有单个流程上扩展一个子流程环节。找到合适的插入点。比如在恶意进程检测里添加脱壳检测步骤。
  • 在现有单个流程上扩展多个子流程环节。定义好子环节的衔接点和标准数据格式,尽量让子环节变成可复用的组件。比如在脚本内容检测流程增加引擎检测、黑名单检测、二进制检测。
  • 在现有单个流程上增加对一种新格式的支持。对核心数据和存储要定义清晰。比如上传文件增加对zip文件的上传。
  • 在现有多个关联流程上增加对一种新格式的支持。对核心数据和存储要定义清晰,否则会顾此失彼,bug 改到你吐血。比如 webshell cdc 检测增加对 zip 文件的检测、扫描、修复与验证。
  • 在服务组添加一个新实例。通过服务注册与发现、容器编排和负载均衡来实现。

结构的扩展通常会涉及到数据、流程和存储的改动。要先确定数据和存储,再来确定流程。流程是易变的,但数据和存储是相对稳定的。

框架与结构的突破

框架是一种相对固定的应用结构。Spring 就是一种流行的 Java 应用框架。在这种框架下,你通过注解来创建和获取对象,通过钩子方法来注入业务逻辑。

大多数时候,在框架的结构下做事就足够了。不过,有时出于性能或其它方面的考虑,需要突破结构。比如网络数据到应用程序的拷贝,通常是经历“网卡-套接字缓冲区-内核态缓冲区-用户态缓冲区-应用程序”的拷贝过程,涉及到用户态到内核态的切换和用户态到内核态的数据拷贝,这是正常而安全的。但零拷贝则是在用户态和内核态缓冲区建立映射和共享,减少了一次数据拷贝(当然,这样可能会带来安全问题)。

可阅:

结构与模式

软件是逻辑的层次化组合叠加,是逻辑的结构化艺术。

程序员,最为精通的技艺是“逻辑解剖”,而更高阶的目标是成为“结构解剖师”。一项软件技术原理或机制,也可以解剖为一系列结构的组合。万事万物,皆有结构。

你看到的不再只是代码和字符,而是千姿百态的结构。这些结构既在程序里,也在人类社会中。

可阅:

软件与建筑

软件与建筑同为结构的艺术,有诸多相似和可借鉴之处。就连设计模式也是借鉴经典的《建筑的永恒之道》一书,可见软件行业受建筑行业的启发不少。实际上,软件可比作一座虚拟建筑,在《大教堂与集市》一书中已有体现。我们也常将互联网比作一座软件大厦。

再引申开,软件与一切空间和结构艺术有关。甚至可从音乐、舞蹈等汲取灵感。所以说,程序员不单从技术书籍中汲取经验,更可从建筑乃至艺术书籍中汲取灵感。

posted @ 2023-05-25 00:29  琴水玉  阅读(78)  评论(0编辑  收藏  举报