乐于分享
好东西不私藏

[022] 体系结构 vs LLR——软件设计到底在设计什么

[022] 体系结构 vs LLR——软件设计到底在设计什么

摘要:很多人以为软件设计就是画架构图、写设计文档。DO-178C只管两件事:体系结构和低层需求(LLR)。这两件事搞明白了,设计才算过关。

做过航空软件的都知道,设计阶段是最容易”自我感觉良好”的阶段。需求分析写完了,代码还没开始写,中间这段时间,很多团队干了什么?做的好的,可能画了一堆UML图、写了几十页设计文档、搞了个漂亮的架构分层图,然后信心满满地去参加设计评审。即是资深工程师,也容易在这个阶段产生”万事俱备”的错觉。做的不好的,可能照着高层需求和祖传代码,“硬编”软件设计说明或低层需求文档,一股脑搞出大几千页的LLR。

结果审查员翻了两页,问了一句:”你的体系结构和高层需求之间怎么对应的?LLR和架构怎么对应?”

答不上来。

这就是典型的”设计≠画图”。DO-178C对软件设计的定义,和大多数工程师直觉里的”设计”,差的不是一点半点。


一、DO-178C的设计:只管两件事

很多人以为DO-178C会规定你用什么建模工具、画什么图、用什么设计模式。

不是。

DO-178C对设计的定义非常简洁——软件设计包括两个元素:软件体系结构(往往也叫软件设计说明/软件架构)低层需求(LLR)

就这两个。

体系结构描述的是”怎么把软件组织在一起”,LLR描述的是”软件怎么执行想要的功能”。前者是骨架,后者是血肉。两者合在一起,就是设计的全部,也是程序猿能畅快码代码的基础。

DO-178C不管你用的是结构化方法还是面向对象方法,不管你画的是DFD还是UML,不管你用的是EA还是Visio还是纸笔。它只问两个问题:你的体系结构写清楚了吗?你的LLR写清楚了吗?

这种”目标导向、方法自由”的理念,贯穿了DO-178C的始终。设计阶段也不例外。


二、软件体系结构:不是画个分层图就完了

很多人对”体系结构”的理解停留在”画个分层图”——上面是应用层,中间是中间件,下面是驱动层,搞个三层架构就完事了。

但DO-178C对体系结构的定义是:”选择用于实现软件需求的软件结构“。关键词是”选择”和”实现需求”。体系结构不是凭空画出来的,它是你根据需求做出的工程决策。

具体来说,编写体系结构文档时需要考虑四个关键点。

第一,体系结构必须与需求兼容。 这不是说”大概对得上”就行了。你需要一些手段来确保兼容性——通常用可追踪性,或者一张需求与体系结构之间的映射表。审查员来了,你能指着这张表说”这条需求在体系结构里对应这个模块”,才算过关。

第二,体系结构要写得清晰一致。 你写的文档不是给自己看的,是给自家苦逼码农看的。拿着你的体系结构文档去写代码,如果看不懂,那这份文档就是废纸,不知道哪位码农又会因此丢掉几撮头发。更重要的,等当前这批码农完成历史使命后,新来的小朋友们将来维护软件,也要看这份文档——民机寿命动辄十几几十年,波音一个型号从七几年飞到现在,n年后有改型,需要再回来改设计,软件设计文档是否还能看懂吗?

第三,体系结构要支持迭代。 航空软件的开发很少是一次性完成的。需求会变、配置会改、安全分析会更新。你的体系结构文档要能跟着这些变化一起更新,而不是写完就锁死。

第四,体系结构有不同风格。 大多数体系结构都包含组件(component,也有叫模块、部件的)和接口(connector)。但在实时机载软件中,最常见的是功能性结构——组件代表功能,接口代表数据流转或控制形式。不是所有项目都适合用分层架构,也不是所有项目都适合用微服务、FACE架构。选什么体系结构,取决于具体软件的高层需求。

业内常见的做法是画三张图:环境图描述系统与外部的接口,流图描述模块间的信息流数据流,互连图描述模块间的物理(电子信号)连接。这三张图配合模块说明和通道说明,就构成了完整的体系结构描述。

但要记住,这些图只是工具,不是目的。目的是让你的体系结构”一步一个脚印、少出错、少踩坑、可理解、可追踪、可维护”。


三、LLR十个核心要素

如果说体系结构是骨架,那LLR就是血肉。DO-178C对LLR的定义是:”从高层需求、导出需求以及设计约束开发的软件需求,从它可以直接实现软件源代码而不需要更多信息。”

这句话信息量很大。它意味着——LLR写到什么程度,程序员拿着它就能直接写代码,不需要再回去猜”这个功能到底怎么实现”。

LLR是DO-178C设计阶段最容易引起争议的部分(ps 打个岔,刚接触178的时候,一直很疑惑为啥软件设计也要叫需求,和一搬软件工程的概念对不上。一次搜索资料的时候,偶然看到个帖子,说rtca编制组编标准时也讨论过,要不要把设计不作为需求,和软件工程领域概念保持一致,据说专家也没能达成一致,最终保留了。后面干验证了,才领会到,LLR之所以作为需求,从验证目标上看和HLR几乎一样,完全对应)。

围绕LLR,有十个关键概念值得记住。

概念一:LLR是设计细节,不是传统意义的需求。

很多人被”低层需求”这个名字误导了,以为LLR和HLR是一回事,只是粒度不同。其实LLR本质上是设计的一部分。把它想成”程序员要依从的实现步骤”更合适。有时候,LLR甚至被写成伪代码或模型。

概念二:LLR必须被唯一标识。

LLR向上要追踪到HLR,向下要追踪到代码。没有唯一标识,这条追踪链就断了。所以每条LLR都需要一个编号,就像每条HLR需要编号一样。这也是为什么有些组织喜欢在LLR里用”应当”——因为它让LLR看起来更像”正式的需求”,便于管理和追踪。

概念三:LLR聚焦于”怎么做”,不是”做什么”。

HLR说的是”做什么”(飞控软件应当对接收到的陀螺仪数据进行卡尔曼滤波),LLR说的是”怎么做”(从SPI接口读取原始数据,转换为角速度,送入滤波器,滤波器状态变量包括……)。如果LLR还在说”做什么”,说明分解不到位。

LLR也应该满足前面需求相关推文提到的质量属性——原子性、完整性、一致性、可验证性等等。[015] 写好需求是门手艺——绕开3个坑,练好9个技能。只是它的焦点从”做什么”转移到了”怎么做”。

概念四:LLR必须是可验证的。

这是”需求”这个词保留在DO-178C中的原因之一。对于A、B和C级软件,LLR需要被测试。你写LLR的时候就要想好——这条LLR怎么测?用什么输入?期望什么输出?如果写完了才发现没法测,那就麻烦了。

概念五:有时候LLR可能根本不需要。

这种情况存在,但不推荐。在一些项目中,HLR足够详细,程序员可以直接从HLR开始写代码。DO-178C的第五章确实允许单层软件需求——当这一层能同时满足高层和低层需求目标的时候。

这里有一个重要的澄清:如果源代码直接从HLR生成,那HLR本身也就被视为LLR。换句话说,不是说你”跳过了LLR”,而是你的HLR同时承担了LLR的角色。但这有个前提:你得在计划里解释清楚为什么单层就够了,并且证明DO-178C的目标能得到满足。而且,审定机构不一定接受。如果没有适当协调,可能导致项目推倒重来。[017] 需求合成一层省事?DO-178C说这是给自己挖坑

概念六:有时候LLR只在部分地方需要。

更微妙的情况是——大多数HLR足够详细可以直接编码,但有少数几条需要进一步分解。这时候你需要明确界定:哪些是HLR,哪些是LLR,程序员知道什么需求形成了编码工作的基础。这个方法很技巧,用的时候要格外小心。

概念七:越关键的软件,LLR需要越详细。

以作者的经验,软件关键等级越高(A级和B级),LLR需要越具体。必竟A级和B级软件有结构覆盖的要求——语句覆盖、决策覆盖、MC/DC覆盖。结构覆盖的准则越严格,LLR就需要写得越详细,才能支撑后续的测试和覆盖分析。C级软件只要求语句覆盖,LLR可以相对粗一些。D级和E级不要求结构覆盖,LLR的要求就更低了。

概念八:导出的LLR必须小心处理。

设计阶段可能会冒出一些新的需求——它们不能向上追踪到HLR,但可以向下追踪到代码。这些就是”导出需求”。

导出需求不是用来弥补HLR中的漏洞的,而是代表了在需求阶段尚未知的实现细节。每个导出的LLR都需要被安全组评价,确保它没有违反任何系统或安全性假设。而且,合理性证明的编写应当让不了解设计细节的人也能理解——因为安全组的人可能不懂你的设计,但他们需要评估这个需求对安全的影响。

概念九:LLR通常是文本的,但可以是模型。

大多数项目用文本格式表达LLR,需要时用表格和图形补充细节。但如果用模型表示LLR,就需要按照DO-331(基于模型的开发与验证)的指南来处理。模型作为LLR时,应当被分类为设计模型。

概念十:LLR可以是伪代码,但要谨慎。

伪代码作为LLR是一个有争议的做法。伪代码确实能提供更精确的算法描述——程序员拿着伪代码几乎可以直接翻译成代码。但问题在于:伪代码在语法上非常接近源代码,HLR和LLR之间的差距可能变得很大。结果是通过HLR/LLR审查,在低层需求级别上检测意外或缺失的功能会变得更困难。

而且,如果伪代码写得太接近实际代码,审查员可能会问一个尴尬的问题:”你这到底是LLR还是代码?”如果是LLR,它应该描述算法罗辑,不需要变量声明、系统特定的子例程这些实现细节。如果是代码,那它就不该出现在设计文档里。

不鼓励对所有或大多数LLR系统性地使用伪代码。DO-178C既不需要伪代码,也不推荐伪代码。如果要用,建议只在算法复杂、文本描述难以精确表达的地方使用,并且在计划中说明理由。


四、设计打包:怎么组织你的设计文档

体系结构和LLR写好了,怎么打包?

答案是:看项目。有些项目把体系结构和LLR集成在一份文档里,有些项目把它们放在完全独立的文档中,还有些项目在同一个文档里包含它们,但放在不同的章节。

DO-178C没有规定打包偏好。这取决于你用的方法和团队的习惯。但有一个原则是确定的——体系结构与需求之间的关系必须清楚。不管你怎么打包,审查员来了要能快速找到”这条需求在体系结构里怎么体现的”。

设计文档的最终使用者是程序员。编写设计文档的时候,脑子里要想着:程序员拿到这份文档,能不能直接开始写代码?

还有一点容易被忽视——设计阶段不是只输出设计文档。如果在设计过程中发现需求有漏洞、体系结构有缺陷、甚至计划本身有问题,这些信息要反馈回去。具体来说:发现需求不充分或不正确,反馈给系统生命周期过程或软件需求过程;发现计划或标准有问题,反馈给软件规划过程。设计阶段是整个开发流程的”质检站”——很多在需求阶段没发现的问题,到这里才浮出水面。


五、设计方法:结构化还是面向对象?

DO-178C对设计方法保持中立——你用结构化方法也行,用面向对象方法也行,混着用也行。

航空领域最常见的还是基于结构的设计——模块结构图、并发图、程序流程图、伪代码。这套方法在实时嵌入式软件中用了几十年,工具链成熟,工程师熟悉,审定机构也认可。

面向对象的方法(类图、状态图、序列图、活动图)在一些新项目中开始使用,特别是在航电显示系统等交互密集的领域。DO-178C第15章专门讨论了面向对象技术的相关考虑,并有额外的补充标准。

无论用哪种方法,设计的核心任务是一样的:把HLR分解为LLR,构建体系结构,确保两者与需求兼容。方法只是工具,目标才是关键。


结尾

DO-178C对软件设计的态度很务实——不管你用什么方法、画什么图,我只看两件事:体系结构和LLR。

体系结构回答的是”软件怎么组织”——哪些模块、怎么交互、用什么通道。LLR回答的是”软件怎么实现”——每条需求具体怎么落地,程序员拿到就能写代码。

这两件事写清楚了,设计评审就有底气。写不清楚,画再多的图也是白搭。以经有太多项目证明了这一点——设计阶段含糊过去的问题,到编码阶段全部暴露出来,到测试阶段更是加倍偿还。

设计不是画图,是把工程思维从”做什么”推进到”怎么做”。这个推进过程的质量,直接决定了代码的质量。

一个好的设计,能让编码变得相对直接——程序员拿着LLR就能写代码,不需要再回去猜”这个功能到底怎么实现”。而一个糟糕的设计,会让编码变成一场无尽的猜测游戏——需求不清楚、接口不明确、算法没定义,程序员每天都在问”这个到底什么意思”。

设计阶段投入的时间和精力,会在编码和测试阶段加倍回报。反之,设计阶段偷的懒,会在后面以十倍的代价讨回来。


*设计阶段是把模糊变成精确的过程。LLR写得越清楚,编码时的返工就越少。*