当前位置: 首页 > news >正文

架构整洁之道-软件架构-测试边界、整洁的嵌入式架构、实现细节

6 软件架构

6.14 测试边界

  和程序代码一样,测试代码也是系统的一部分。甚至,测试代码有时在系统架构中的地位还要比其他部分更独特一些。

  测试也是一种系统组件。

  从架构的角度来讲,所有的测试都是一样的。不论它们是小型的TDD测试,还是大型的FitNess、Cucumber、SpecFlow或JBehave测试,对架构来说都是一样的。

  究其本质而言,测试组件也是要遵守依赖关系原则的。因为其中总是充满了各种细节信息,非常具体,所以它始终都是向内依赖于被测试部分的代码的。事实上,我们可以将测试组件视为系统架构中最外圈的程序,它们始终是向内依赖的,而且系统中没有其他组件依赖于它们。

  另外,测试组件是可以独立部署的,事实上,大部分测试组件都是被部署在测试环境中,而不是生产环境中的,所以,即使是在那些本身不需要独立部署的系统中,其测试代码也总是独立部署的。

  测试组件通常是一个系统中最独立的组件,系统的正常运行并不需要用到测试组件,用户也不依赖于测试组件。测试组件的存在是为了支持开发过程,而不是运行过程。然而,测试组件仍然是系统中不可或缺的一个组件。事实上,测试组件在许多方面都反映了系统中其他组件所应遵循的设计模型。

  由于测试代码的独立性,以及往往不会被部署到生产环境的特点,开发者常常会在系统设计中忽视测试的重要性,这种做法是极为错误的。测试如果没有被集成到系统设计中,往往是非常脆弱的,这种脆弱性会使得系统变得死板,非常难以更改。

  当然,这里的关键之处就是耦合。如果测试代码与系统是强耦合的,它就得随着系统变更而变更,哪怕只是系统中组件的一点小变化,都可能会导致许多与之相耦合的测试出现问题,需要做出相应的变更。这种修改一个通用的系统组件可能会导致成百上千个测试出现问题的情况,称为脆弱的测试问题(fragile tests problem)。

  脆弱的测试问题往往会让系统变得非常死板,当开发者意识到一些简单的修改就会导致大量的测试出错时,他们自然就会抵制修改。要想解决这个问题,就必须在设计中考虑到系统的可测试性,软件设计的第一原则,就是不要依赖于多变的东西。

  设计这样一个系统的方法之一就是专门为验证业务逻辑的测试创建一个API。这个API应该被授予超级用户权限,允许测试代码可以忽视安全限制,绕过那些成本高昂的资源(例如数据库),强制将系统设置到某种可测试的状态中,总而言之,该API应该成为用户界面所用到的交互器与接口适配器的一个超集。

  设置测试API是为了将测试部分从应用程序中分离出来。换句话说,这种解耦动作不只是为了分隔测试部分与UI部分,而是要将测试代码的结构与应用程序其他部分的代码结构分开。

  结构性耦合是测试代码所具有的耦合关系中最强大、最阴险的一种形式,测试专用API的作用就是将应用程序与测试代码解耦,这样,我们的产品代码就可以在不影响测试的情况下进行重构和演进。同样的,这种设计也允许测试代码在不影响生产代码的情况下进行重构和演进。

  这种对演进过程的隔离是很重要的,因为随着时间的推移,测试代码趋向于越来越具体和详细,产品代码则会趋向于越来越抽象和通用。结构性的强耦合可能会让这种必需的演进无法进行————至少会形成强烈的干扰。

  当然,这种具有超级权限的测试专用API如果被部署到我们的产品系统中,可能会是非常危险的。如果要避免这种情况发生,应该将测试专用API及其对应的具体实现放置在一个单独的、可独立部署的组件中。

  测试并不是独立于整个系统之外的,恰恰相反,它们是系统的一个重要组成部分。我们需要精心设计这些测试,才能让它们发挥验证系统稳定性和预防问题复发的作用。没有按系统组成部分来设计的测试代码,往往是非常脆弱且难以维护的,这种测试最后常常会被抛弃,因为它们终究会出问题。

6.15 整洁的嵌入式架构

  虽然软件质量本身并不会时间推移而损耗,但是未妥善管理的硬件依赖和固件依赖却是软件的头号杀手。

  也就是说,本可以长期使用的嵌入式软件可能会由于其中隐含的硬件依赖关系而无法继续使用,这种情况是很常见的。

  这里,软件(software)应该是一种使用周期很长的东西,而固件(firmware)则会随着硬件演进而淘汰过时,来看看固件的一些定义:

  (1) 固件通常被存储在非可变内存设备,例如ROM、EPROM或者闪存中;

  (2) 固件是直接编程在一个硬件设备上的一组指令或者一段程序;

  (3) 固件是嵌入在一个硬件中的软件程序;

  (4) 固件是被写入到只读内存设备中的(ROM)程序或数据;

  但实际上,大家普遍所认知的固件定义是错误的,或者至少是过时的,固件并不一定是指存储在ROM中的代码,也并不是依据其存储的位置来定义的,而是由其代码的依赖关系,及其随着硬件的演进在变更难度上的变化来定义的。硬件的演进是显而易见的,我们在架构嵌入式代码时要时刻记住这一点。

  那么,如果一个产品从头到尾都与具体技术、具体硬件息息相关、无法分割时,整个产品就已经成为事实上的固件了。

  为什么很多嵌入式软件最后都成为了固件呢?看起来,很可能是因为我们在做嵌入式设计时只关注代码能否顺利运行,并不太关心其结构能否撑起一个较长的有效生命周期,Kent Beck描述了软件构建过程中的三个阶段:

  (1) “先让代码工作起来”————如果代码不能工作,就不能产生价值;

  (2) “然后再试图将它变好”————通过对代码进行重构,让我们自己和其他人更好地理解代码,并能按照需求不断地修改代码;

  (3) “最后再试着让它运行得更好”————按照性能提升的“需求”来重构代码;

  而大部分“野生”的嵌入式代码,都只关注“先让它工作起来”这个目标————也许还有些团队会同时痴迷于“让它更快”这个目标,不放过任何一个机会加入各种微优化。在《人月神话》这本书中,Fred Brooks建议我们应该随时准备“抛弃一个设计”,即“在实践中学习正确的工作方法,然后再重写一个更好的版本”。

  这个建议对非嵌入式软件系统开发同样有用,毕竟目前大部分非嵌入式应用也仅仅停留在“可用”这个目标上,很少考虑为了长久使用而进行正确的设计。对于程序员来说,让他的程序工作这件事只能被称为“程序适用性(app-titude test)”,一个程序员,不论他写的是否是嵌入式程序,如果目标仅仅是让程序可以工作,恐怕对他的老板和这个程序本身而言都是一件坏事。

  嵌入式系统的程序员通常需要处理很多在写非嵌入式系统时不需要关心的事情————例如,有限的地址空间、实时性限制、运行截止时间、有限的I/O能力、非常规的用户接口、感应器,以及其他与物理世界的实际链接。大部分时候,这些系统的硬件是和它的软件、固件并行开发的,工程师在为这种系统编写代码的时候,往往没有任何地方可以运行。这就是目标硬件瓶颈(target-hardware bottleneck)是嵌入式开发所特有的一个问题,如果我们没有采用某种清晰的架构来设计嵌入式系统的代码结构,就经常会面临只能在目标系统平台上测试代码的难题。如果只能在特定的平台上测试代码,那么这一定会拖慢项目的开发进度。

  整洁的嵌入式架构就是可测试的嵌入式架构。

  我们来看一下具体应如何将架构设计的原则应用在嵌入式软件和固件上,以避免陷入目标硬件瓶颈。

  首先是分层,分层可以有很多种方式,先来看三层结构:
在这里插入图片描述
  首先,底层是硬件层,由于科技的进步与摩尔定律,硬件是一定会改变的。旧的硬件部件将会被淘汰,新的硬件部件可能耗电量更少,或者性能更好,或者价格更便宜,不管硬件更新的原因是什么,作为嵌入式工程师,我们都不会希望这些不可避免的硬件变动带来更多的工作量。

  硬件与系统其他部分的分隔是既定的————至少在硬件设计完成之后如此。这也是我们试图通过程序适用测试之时往往会发生问题的地方。因为没有什么东西可以真正阻碍硬件实现细节污染到应用代码。如果我们在构建代码的时候不够小心,没有小心安排哪些模块之间可以互相依赖,代码很快就非常难以更改了。请注意,这里所说的变更不仅仅是指来自硬件的变更,还包括用户的功能性变更、修复代码中的Bug。

  另外,软件与固件集成在一起也属于设计上的反模式(anti-pattern),符合这种反模式的代码修改起来都会很困难,同时,这种代码也很危险,容易造成意外事故,这导致它经历任何微小的改动都需要进行完整的回归测试,如果没有完善的测试流程,那么就会有无穷无尽的手工测试,同时还有纷沓而来的Bug报告。

  软件与固件之间的分割线往往没有代码与硬件之间的分割线那么清晰,所以,我们的工作之一就是将这个边界定义得更清晰一些,软件与固件之间的边界被称为硬件抽象层(HAL),这不是一个概念,它在PC上的存在甚至可以追溯到Windows诞生之前:
在这里插入图片描述
  HAL的存在是为了给它上层的软件提供服务,HAL的API应该按照这些软件的需要来量身定做。例如,固件可以直接将字节和字节组存入闪存中。相比之下,软件需要的是从某种持久化平台保存和读取name/value对信息,它不应该关心自己信息到底是被存储到闪存中、磁盘中、云端存储中,还是在内存中读取/存储这些信息。总之,HAL的作用是为软件部分提供一种服务,以便隐藏具体的实现细节。毕竟专门针对闪存的实现代码是一种细节信息,它应该与软件部分隔离。

  不要向HAL的用户暴露硬件细节 ,依照整洁的嵌入式架构所构建的软件应该是可以脱离目标硬件平台来进行测试的。因为设计合理的HAL可以为我们脱离硬件平台的测试提供相应的支撑。

  当我们的嵌入式应用依赖于某种特殊的工具链时,该工具链通常会。为我们提供一些“帮助”性质的头文件,这些编译器往往会自带一些基于C语言的扩展库,并添加一些用于访问特殊功能的关键词,这会导致这些程序的代码看起来仍然用的是C语言,但实际上它们已经不是C语言了。有时候,这些嵌入式应用的提供商所指定的C编译器还会提供类似于全局变量的功能,以便我们直接访问寄存器、I/O端口、时钟信息、I/O位、中断控制器以及其他处理器函数,这些函数会极大地方便我们对相关硬件的访问。但请注意,一旦你在代码中使用了这些函数,你写的就不再是C语言程序,它就不能用其他编译器来编译了,甚至可能连同一个处理器的不同编译器也不行。为了避免我们的代码在未来出现问题,我们就必须限制这些C扩展的使用范围。

  在整洁的嵌入式架构中,固件将这类底层函数隔离成处理器抽象层(PAL),这样一来,使用PAL的固件代码就可以在目标平台之外被测试了。

  除HAL和PAL之外,由于嵌入式系统可能使用某种实时操作系统(RTOS),或者某种嵌入式的Linux或Windows,因此我们必须将操作系统也定义为实现细节,让代码避免与操作系统层产生依赖。

  整洁的嵌入式架构会引入操作系统抽象层(OSAL),将软件与操作系统分隔开:
在这里插入图片描述
  除了在嵌入式系统的主要分层(指软件、操作系统、固件、硬件这四层)之中增加HAL和OSAL之外,我们还可以应用其他的设计原则,这些设计原则可以帮助我们按功能模块、接口编程以及可替代性来划分系统。

  分层架构的理念是基于接口编程的理念来设计的,当模块之间能以接口形式交互时,我们就可以将一个服务替换成另外一个服务。

  由整洁的嵌入式架构所构建的系统应该在每一个分层中都是可测试的,因为它的模块之间采用接口通信,每一个接口都为平台之外的测试提供了替换点。

7 实现细节

  数据库只是实现细节。

  从系统架构的角度来看,数据库并不重要————它只是一个实现细节,在系统架构中并不占据重要角色。如果就数据库与整个系统架构的关系打个比方,它们之间就好比是门把手和整个房屋架构的关系。请注意,这里讲的是“数据库”而非“数据模型”,为应用程序中的数据设计结构,对于系统架构来说当然是很重要的,但是数据库并不是数据模型。数据库只是一款软件,是用来存取数据的工具。从系统架构的角度来看,工具通常是无关紧要的————因为这只是一个底层的实现细节,一种达成目标的手段,一个优秀的架构师是不会让实现细节污染整个系统架构的。

  数据的组织结构,数据的模型,都是系统架构中的重要部分,但是从磁盘上存储/读取数据的机制和手段对于架构来说则不是那么重要,即使类似于读取/存储性能这样的指标,也应被封装在具体的数据库内部,而不是作为系统架构的一部分。

  Web是实现细节。

  这也是在前文不断被提及的概念。GUI只是一个实现细节,而Web则是GUI的一种,所以也是一个实现细节,作为一名软件架构师,我们需要将这类细节与核心业务逻辑隔离开来————即使不是Web,换一种GUI,也对业务核心逻辑无影响。

  应用程序框架是实现细节。

  应用程序框架现在非常流行,这在通常情况下是一件好事,这么多框架都非常有效,非常有用,而且是免费的,但框架并不等同于系统架构————尽管有些框架确实以此为目标。

  框架的作者通常会希望我们与其框架紧密结合,这意味着我们将与框架签订终身契约,而他们则不需要为我们遵守任何承诺,把风险全部交由我们自己承担,例如框架自身架构设计可能不正确而要求我们将代码引入到业务对象或业务实体中,或者框架可能会想要我们将框架耦合在最内圈代码中等,比如框架自身为了演进新增了很多我们不需要的功能等。

  因此我们应该将框架作为架构最外圈的一个实现细节来使用,而不是让它们进入内圈,不要让框架污染我们的核心代码,应该依据依赖关系原则,将它们当作核心代码的插件来进行管理。尽可能长时间地将框架留在架构边界之外,我们的业务逻辑才不会被框架影响,在我们需要替换框架时可以轻松替代。

相关文章:

架构整洁之道-软件架构-测试边界、整洁的嵌入式架构、实现细节

6 软件架构 6.14 测试边界 和程序代码一样,测试代码也是系统的一部分。甚至,测试代码有时在系统架构中的地位还要比其他部分更独特一些。 测试也是一种系统组件。 从架构的角度来讲,所有的测试都是一样的。不论它们是小型的TDD测试&#xff…...

nodejs学习计划--(十)会话控制及https补充

一、会话控制 1.介绍 所谓会话控制就是 对会话进行控制 HTTP 是一种无状态的协议,它没有办法区分多次的请求是否来自于同一个客户端, 无法区分用户 而产品中又大量存在的这样的需求,所以我们需要通过 会话控制 来解决该问题 常见的会话控制…...

fast.ai 机器学习笔记(四)

机器学习 1:第 11 课 原文:medium.com/hiromi_suenaga/machine-learning-1-lesson-11-7564c3c18bbb 译者:飞龙 协议:CC BY-NC-SA 4.0 来自机器学习课程的个人笔记。随着我继续复习课程以“真正”理解它,这些笔记将继续…...

LLM大模型常见问题解答(2)

对大模型基本原理和架构的理解 大型语言模型如GPT(Generative Pre-trained Transformer)系列是基于自注意力机制的深度学习模型,主要用于处理和生成人类语言。 基本原理 自然语言理解:模型通过对大量文本数据的预训练&#xff…...

这种学习单片机的顺序是否合理?

这种学习单片机的顺序是否合理? 在开始前我有一些资料,是我根据网友给的问题精心整理了一份「单片机的资料从专业入门到高级教程」, 点个关注在评论区回复“888”之后私信回复“888”,全部无偿共享给大家!&#xff01…...

13 年后,我如何用 Go 编写 HTTP 服务(译)

原文:Mat Ryer - 2024.02.09 大约六年前,我写了一篇博客文章,概述了我是如何用 Go 编写 HTTP 服务的,现在我再次告诉你,我是如何写 HTTP 服务的。 那篇原始的文章引发了一些热烈的讨论,这些讨论影响了我今…...

flask+python高校学生综合测评管理系统 phl8b

系统包括管理员、教师和学生三个角色; 。通过研究,以MySQL为后端数据库,以python为前端技术,以pycharm为开发平台,采用vue架构,建立一个提供个人中心、学生管理、教师管理、课程类型管理、课程信息管理、学…...

【GameFramework框架内置模块】1、全局配置(Config)

推荐阅读 CSDN主页GitHub开源地址Unity3D插件分享简书地址 大家好,我是佛系工程师☆恬静的小魔龙☆,不定时更新Unity开发技巧,觉得有用记得一键三连哦。 一、前言 【GameFramework框架】系列教程目录: https://blog.csdn.net/q7…...

PySpark(四)PySpark SQL、Catalyst优化器、Spark SQL的执行流程、Spark新特性

目录 PySpark SQL 基础 SparkSession对象 DataFrame入门 DataFrame构建 DataFrame代码风格 DSL SQL SparkSQL Shuffle 分区数目 DataFrame数据写出 Spark UDF Catalyst优化器 Spark SQL的执行流程 Spark新特性 自适应查询(SparkSQL) 动态合并 动态调整Join策略 …...

2024第六届中国济南国际福祉及残疾人用品展览会/失能护理展

龘龘龙年-第六届山东福祉展会-将于5月27-29日在济南黄河国际会展中心举办; 一、引言 2024年,中国龙年,龙象征着力量、繁荣与希望。在这个特殊的年份,一场备受瞩目的盛会即将拉开帷幕。2024年第六届中国(济南&#xf…...

SegmentAnything官网demo使用vue+python实现

一、效果&准备工作 1.效果 没啥好说的,低质量复刻SAM官网 https://segment-anything.com/ 需要提一点:所有生成embedding和mask的操作都是python后端做的,计算mask不是onnxruntime-web实现的,前端只负责了把rle编码的mask解…...

Java:字符集、IO流 --黑马笔记

一、字符集 1.1 字符集的来历 我们知道计算机是美国人发明的,由于计算机能够处理的数据只能是0和1组成的二进制数据,为了让计算机能够处理字符,于是美国人就把他们会用到的每一个字符进行了编码(所谓编码,就是为一个…...

RabbitMQ之五种消息模型

1、 环境准备 创建Virtual Hosts 虚拟主机:类似于mysql中的database。他们都是以“/”开头 设置权限 2. 五种消息模型 RabbitMQ提供了6种消息模型,但是第6种其实是RPC,并不是MQ,因此不予学习。那么也就剩下5种。 但是其实3、4…...

项目02《游戏-14-开发》Unity3D

基于 项目02《游戏-13-开发》Unity3D , 任务:战斗系统之击败怪物与怪物UI血条信息 using UnityEngine; public abstract class Living : MonoBehaviour{ protected float hp; protected float attack; protected float define; …...

【Java数据结构】单向 不带头 非循环 链表实现

模拟实现LinkedList:下一篇文章 LinkedList底层是双向、不带头结点、非循环的链表 /*** LinkedList的模拟实现*单向 不带头 非循环链表实现*/ class SingleLinkedList {class ListNode {public int val;public ListNode next;public ListNode(int val) {this.val …...

【ES6】模块化

nodejs遵循了CommonJs的模块化规范 导入 require() 导出 module.exports 模块化的好处: 模块化可以避免命名冲突的问题大家都遵循同样的模块化写代码,降低了沟通的成本,极大方便了各个模块之间的相互调用需要啥模块,调用就行 …...

腾讯云4核8G服务器可以用来干嘛?怎么收费?

腾讯云4核8G服务器适合做什么?搭建网站博客、企业官网、小程序、小游戏后端服务器、电商应用、云盘和图床等均可以,腾讯云4核8G服务器可以选择轻量应用服务器4核8G12M或云服务器CVM,轻量服务器和标准型CVM服务器性能是差不多的,轻…...

怎么在bash shell中操作复杂json对象

怎么在bash shell中操作复杂json对象 在bash shell中操作复杂JSON对象,jq可以帮助我们在bash环境下轻松地处理这类数据,本文将详细介绍如何使用jq在bash中操作复杂的JSON对象。 jq是一个轻量级且灵活的命令行JSON处理器,它允许你以非常高效的…...

11.div函数

文章目录 函数简介1.函数原型2.div_t结构体3.引用头文件 代码运行 函数简介 1.函数原型 div_t div(int numerator, int denominator);div函数把numerator除以denominator,产生商和余数,用一个div_t的结构体返回。 2.div_t结构体 typedef struct _div…...

windows11 MSYS2下载安装教程

MSYS2 可以理解为在windows平台上模拟linux编程环境的开源工具集 当前环境:windows11 1. 下载 官网地址可下载最新版本,需要科学上网 https://www.msys2.org/ 2. 安装 按照正常安装软件流程一路next就可以 打开 3. 配置环境 网上很多教程提到需…...

Spring Boot+Neo4j知识图谱实战:3步搭建智能关系网络!

一、引言 在数据驱动的背景下,知识图谱凭借其高效的信息组织能力,正逐步成为各行业应用的关键技术。本文聚焦 Spring Boot与Neo4j图数据库的技术结合,探讨知识图谱开发的实现细节,帮助读者掌握该技术栈在实际项目中的落地方法。 …...

Ascend NPU上适配Step-Audio模型

1 概述 1.1 简述 Step-Audio 是业界首个集语音理解与生成控制一体化的产品级开源实时语音对话系统,支持多语言对话(如 中文,英文,日语),语音情感(如 开心,悲伤)&#x…...

QT: `long long` 类型转换为 `QString` 2025.6.5

在 Qt 中,将 long long 类型转换为 QString 可以通过以下两种常用方法实现: 方法 1:使用 QString::number() 直接调用 QString 的静态方法 number(),将数值转换为字符串: long long value 1234567890123456789LL; …...

大语言模型(LLM)中的KV缓存压缩与动态稀疏注意力机制设计

随着大语言模型(LLM)参数规模的增长,推理阶段的内存占用和计算复杂度成为核心挑战。传统注意力机制的计算复杂度随序列长度呈二次方增长,而KV缓存的内存消耗可能高达数十GB(例如Llama2-7B处理100K token时需50GB内存&a…...

如何在网页里填写 PDF 表格?

有时候,你可能希望用户能在你的网站上填写 PDF 表单。然而,这件事并不简单,因为 PDF 并不是一种原生的网页格式。虽然浏览器可以显示 PDF 文件,但原生并不支持编辑或填写它们。更糟的是,如果你想收集表单数据&#xff…...

【WebSocket】SpringBoot项目中使用WebSocket

1. 导入坐标 如果springboot父工程没有加入websocket的起步依赖&#xff0c;添加它的坐标的时候需要带上版本号。 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-websocket</artifactId> </dep…...

若依登录用户名和密码加密

/*** 获取公钥&#xff1a;前端用来密码加密* return*/GetMapping("/getPublicKey")public RSAUtil.RSAKeyPair getPublicKey() {return RSAUtil.rsaKeyPair();}新建RSAUti.Java package com.ruoyi.common.utils;import org.apache.commons.codec.binary.Base64; im…...

图解JavaScript原型:原型链及其分析 | JavaScript图解

​​ 忽略该图的细节&#xff08;如内存地址值没有用二进制&#xff09; 以下是对该图进一步的理解和总结 1. JS 对象概念的辨析 对象是什么&#xff1a;保存在堆中一块区域&#xff0c;同时在栈中有一块区域保存其在堆中的地址&#xff08;也就是我们通常说的该变量指向谁&…...

【HarmonyOS 5】鸿蒙中Stage模型与FA模型详解

一、前言 在HarmonyOS 5的应用开发模型中&#xff0c;featureAbility是旧版FA模型&#xff08;Feature Ability&#xff09;的用法&#xff0c;Stage模型已采用全新的应用架构&#xff0c;推荐使用组件化的上下文获取方式&#xff0c;而非依赖featureAbility。 FA大概是API7之…...

Tauri2学习笔记

教程地址&#xff1a;https://www.bilibili.com/video/BV1Ca411N7mF?spm_id_from333.788.player.switch&vd_source707ec8983cc32e6e065d5496a7f79ee6 官方指引&#xff1a;https://tauri.app/zh-cn/start/ 目前Tauri2的教程视频不多&#xff0c;我按照Tauri1的教程来学习&…...