概述
作者:杨玉柱@模赛思软件技术(上海)有限公司
在基于模型的开发中,优质的模型架构是生成优质代码的必要前提。静态模型分析对于模型的质量保证有着至关重要的作用,同时建模规范已在业内有着广泛而成熟的应用。然而建模规范并非模型设计原则合规性的唯一考量标准,仍有许多方面,需要根据具体的模型属性加以改善。模型结构质量作为反映建模质量的重要方面,可通过一系列模型指标(Model metrics)对模型结构质量进行综合分析。本文我们将向您展示模型指标的概念、原理、方法、模型指标应用和模型重构的最佳实践,并向您展示这些操作具体如何提高模型结构质量。
什么是结构质量首先来看模型质量的概念。在基于模型的开发(MBD)中主要考虑模型软件质量保证问题。ISO26262中关于软件产品开发的参考阶段给出相应设计原则、软件设计和验证的规范要求。按照ISO26262 中软件开发的V模型(如图 1),各阶段涉及不同的主题活动,从安全需求到软件架构设计、软件单元设计和实现,以及软件单元验证、软件集成和验证,到嵌入式软件系统的验证。这个开发过程分为两个阶段:在V模型的左侧,按照软件需求,我们设计构建模型;在V模型的右侧,在相应阶段验证我们的模型或软件是否按预期的需求和架构设计工作。与此同时,此V模型同步涉及质量保证活动的各个阶段。一方面我们关注模型的结构质量,另外一方面考虑模型的功能质量。结构质量方面我们关注模型结构质量相关属性或设计的适用性,分析模型结构相应于设计的适用性,考虑设计能否适用于实现软件的需求。功能质量方面我们注重功能质量,验证软件的功能是否符合模型设计,并能够按照需求正确运行。
图1. 软件开发与软件质量活动流程
那究竟什么是结构质量呢? 首先让我们来谈谈模型的结构属性。结构属性反映软件设计在多大程度上适用于需求?设计属性的符合程度如何?根据ISO26262关于质量保证的典型设计特征和特性描述,设计或实现的目标要达到的目标包括一致性、简单性、可理解性和可读性、模块化和封装性、修改的适用性、设计的鲁棒性、可验证性、可测试性和可维护性。 因此,典型设计特征首先从软件架构看要实现软件单元接口的一致性。其次,设计要容易理解和审查。例如,设计的模块化程度如何?设计的修改方便吗?设计是否足够稳健?是否应用了行业最佳实践或范式进行建模?这样的设计方便测试吗?这样的设计后期方便维护吗?
为了实现这些属性,参考ISO26262-6在软件设计和实现的设计原则建议,事实上对应标准中的三个表格,这三个表格专注于设计建议和结构质量条款,分别是表一,表三和表六,包括建模和编码指南应该涵盖的主题,软件架构设计的原则,软件单元设计和实现的设计原则。这些原则主要通过静态分析和在基于模型的开发中应用建模指南来实现。与此同时,我们可以从软件架构设计原则中得到一些具体的操作建议,比如软件设计原则中提到的低复杂度执行,限制组件大小,组件的强内聚性,组件间的松散耦合等等。对于这些建议的实施,首先我们需要详细了解相关的模型属性,而对于这一点,模型指标可以为我们提供关于模型属性的详细信息。
结构质量相关模型指标
既然模型指标可以表示软件模型的相关质量属性,让我们先了解一下模型指标具体包含哪些指标,如何反映模型的结构质量。
通常,指标是软件模型具有某些属性的程度的度量(对于模型度量也是如此)。因此,我们通过度量来评估模型的某些属性,然后将这些属性映射到某些具体的数值,帮助我们建立对该属性客观的认识和理解。模型指标的示例包括模型的复杂度、组件大小、模型的非内聚度、功能组件的比例、接口大小以及克隆组的使用等等。
接下来,我们来详细地介绍这些具体的模型指标,并对各项模型指标给出合理的解释,各模型指标的影响因素,以及从这些模型指标如何反映结构质量。另外,针对各项指标给出行业最佳实践做法。
模型复杂度
首先介绍复杂度。当谈到复杂度时,我们首先谈到的是可读性的复杂度,这是对子系统级别建模应用风格的理解。因此,我们先来看子系统的局部复杂度。让我们从一个典型的子系统图 2开始。
图2. 子系统的局部复杂度
在此处可以看到,示例的子系统由一些输入端口和输出端口和另外两个子系统组成。在当前结构层,我们只关注两个子系统模块,而忽略关注子系统中包含的内容。即当前子系统具有输入端口和输出端口以及两个模块。这里要理解结构层次的概念,主要是因为在结构层面,我们需要了解信号流的来源、走向和信号的目标,以及它们之间是如何相互连接的,也就是我们在这里看到的当前层的结构布局。因此,为了从结构上理解这种局部复杂度,我们不需要知道设计中做了怎样的计算,我们暂忽略里面的内容,只考虑这些典型的模块。在当前模型层级评估这个系统的局部复杂度,可以计算得出一个具体数字,考虑可见的所有这些元素在内,我们得出好比这里给出的33的一个数字,以此来表征子系统的局部复杂度。
那么为什么要计算局部复杂度呢?事实上我们希望确保在特定的模型层只关注某些模型相关的重要特征信息,应用适当的层次结构,使模型表示的内容更容易阅读和理解,即增强模型的可读性。同时关于模型表达的所有其他信息,我们在当前层级省略并把它放在模型结构的不同层级。合理的复杂度的子系统布局可提升结构质量,提高模型软件的简单性、可读性,减少审查和维护方面的工作。现在,如果这个数字表示不能告诉我们关于复杂度的更多的信息,不用担心,让我们来看下面的示例如图 3。
图3. 复杂度: 低复杂度vs高复杂度
如图 3左侧部分,像这样的一个小规模的子系统,基本上只包含简单的几步计算,如果我们应用相同的局部复杂度计算方法计算得到一个数字,显示其复杂度数值为60。相比之下,对于图 3右侧一个更大的子系统,我们看到它具有更多的计算。因此,我们第一印象右侧这个子系统更复杂。现在的问题是,它究竟有多复杂?能否给出某种数字来比较它比这个数字大多少或复杂多少?因此,应用局部复杂度的运算,我们得到其复杂度数值为600。好的,那我们说这个右侧大的子系统大约比这里的这个小的子系统复杂十倍。
那么接下来我们研究一下这个具有相当高复杂度的大型子系统(参看图 3右和图 4),一般来说600显示其复杂度不是很高,但也是中高范围。对于这样的一个子系统,如何提高可读性?整体看,我们会发现这里包含某种并行计算,有些信号流不是很明确。我们可以尝试将局部复杂度限制在某阈值内,600已经有点过大了。于是我们查看此处的子系统并进行模型重构。
容易看到模型基本上分为了上下两个部分。我们在本层结构中上下部分分别创建一个子系统使部分计算包含在下面的模型层,重构后的局部(参看图 5)复杂度显著减小为40,更重要的是重新布局的子系统,对于理解信号流、数据流和计算更为清晰简单,增强了可理解性和可维护性。
图4. 复杂系统的布局与复杂度(局部复杂度~ 600)
图5. 复杂系统重构后的布局与复杂度(局部复杂度~ 40)
通过计算局部复杂度,我们可以查看模型组件的大小,或者子系统的复杂程度。从子系统重新开始,现在我们扩展到更大范围研究模型系统或子系统的全局复杂度,以了解模型系统的实现规模。因此,对于同样的模型系统或子系统如图 6, 子系统由输入端口, 输出端口和两个子系统模块。但现在我们考虑子系统中的所有内容,子系统内部所有实现,如一些计算或更深层级的子系统中包含的内容,如图 6在模型浏览器中我们所看到的那样。现在对模型子系统中包括的所有内容计算所有子系统的复杂度并加和,得到系统的全局复杂度为352。这个模型复杂度的计算考虑了我们在这里看到的所有内容。因此,我们看到当前结构层的全局复杂度不仅包括局部复杂度33,还包括其下各子系统层次结构贡献的局部复杂度。
图6. 全局复杂度
那么为什么要统计全局复杂度呢?原因在于通过全局复杂度的指标度量确保我们的单元或组件的不至于变得过于复杂。即确保单元和组件的可读性。同时我们还希望保持单元组件可度量,特别是对于审查和测试工作。因此,我们实际上可以将这个数字与审查和测试的工作量联系起来,这意味着数字越大,需要审查和测试的工作量就越大。
因此,全局复杂度大小等同于工作量多少,这正体现了对结构质量的影响。保持单元的小规模的同时提高可读性,模块化和可测试性,提升实施效率。如果对于某项功能需求,我们可以两种不同的方式实现它们,并且在功能上它们都可以完全正常地工作。但是当你计算它们的总实现的全局复杂度大小时,发现它们之间有很大的差异,然后你就知道全局复杂度较小的一个实际上是更高效的实现。因此全局复杂度不一定对功能质量产生影响,但它会对结构质量产生影响,表示我们的建模效率如何。
那么,全局复杂度在建模实践中怎样帮助我们改善和提高建模效率呢?我们以Matlab/Simulink中的一个典型示例模型为例加以说明。首先根据全局复杂度的指标,可以衡量并限制模型组件的整体大小,降低模型组件的复杂程度。其次,提供可测试性指标。例如通过采用库或模型引用(图 7)的方式,将文件分拆成单独的模型组件,提高组件的模块化和可测试性。最后,提供模块化指标。合理划分模块,达到模块复用或者单独测试的目的。模块化的设计与您的软件架构设计保持一致,关联重点需求和测试。模块化带来灵活地加载和编译,在不同的工程模型中重用,而不必在全局范围内重复的评审和测试某种结构类型的组件。
图7. 复杂模型系统的重构-库/模型引用
模型指标非相干度
前面我们已经讨论复杂度的概念,如何了解组件的大小和复杂度。现在,我们进一步了解此组件本身的协同工作情况,如计算或计算之间是否彼此关联。为此,我们研究组件内不同组成部分的关联程度,Matlab/Simulink/Stateflow中我们使用内聚度【1】和非相干度来表示。中心思想是子系统中的每个模块直接或间接地影响子系统中的其他模块,并且本身直接或间接地受到子系统中其他模块的影响。因此,对于每个模块b,计算受模块b 影响或影响模块 b 的所有模块的数量,包括模块b本身,即通过b的路径上的模块总数(简记为bop)。高内聚的子系统的 bop接近子系统的总模块计数;低内聚的子系统的bop(b)与总模块计数相比会很低。例如图 8中的模块,我们可以找到模块旁边标注每个模块的 bop 值。
图8. 子系统中通过每个模块的路径上的模块数示例
然后,汇总子系统中所有块的bop值,进行规范化,获得子系统的内聚值。因此,具有模块Bs的子系统S的内聚度的计算公式为:
内聚度:Coherences=b∈Bs|通过 模块b 的路径上的模块数|Bs2
进一步,我们定义非相干度为:
非相干度:
对于图 8中的子系统,内聚度和非相干度值代入相应计算公式得到:
内聚度:
非相干度:
子系统中的进行模块计算或结构的设计时,我们希望相关功能组合在一起,分组到一个子系统中。但是我们不希望对彼此不关联的功能进行分组,以此来提高可理解性。如果所有模块都只致力于一个功能或计算,那么就更容易理解。因此,我们希望确定并行组件的任何部分,或单独的组件,进而进行模型重构,实现模块化和封装性,提升可测试性和可维护性典型的质量指标。
那么,非相干度如何帮助我们重构操作呢?我们可以将非相干度解读为子系统中分离组件的粗略估计。例如图 9参与计算部分的所有模块都在一条路径上,这是最简单的情形,经过计算其非相干度为1。图 10类似前面的示例,如果子系统中有某种拆分路径,我们得到的非相干度是一个分数,在本例中是1.3。而如果在图 11这里进行并行计算会发生什么呢?此时多了一组单独并行的组件,实际上增加了不相干的组件数量,计算得到非相干度为2。由此看出,非相干度的数值的整数位可以让您大致了解子系统中有多少并行或分离的组件。
Incoherence = 1.0
图9. 非相干度示例1
Incoherence = 1.3
图10. 非相干度示例2
Incoherence = 2.0
图11. 非相干度示例3
让我们来看一个复杂的例子如图 12。我们现在可以使用非相干度来识别具有高复杂度和高度不相干的子系统。如图 12子系统的局部复杂度已经大到1104,导致可读性和可测试性方面非常大的困难。根据模型的非相干度指标值约为5,表明大约可划分为4-5个独立的组件。因此,我们对模型进行重构,可将模型重构为4个更大的分离组件。当然根据需要划分为5个也是完全可以的。像这样重构后,得到类似图 13的新结构,形成新的结构层,使整个模型更容易理解。首先,清晰地显示哪些输入信号对应于整个子系统单元的哪部分功能或运算,对相应功能信号的评估很重要。其次我们还可发现,非相干度大致保持不变,但是局部复杂度数值显著降低,提高了该层级模型的可理解性,当前模型层引入了结构层而不是功能和结构的混合。
Incoherence » 5, Local Complexity 1104
图12. 高复杂度和高度不相干的子系统示例
Incoherence » 4, Local Complexity 194
图13. 基于非相干度Simulink模型指标的模型重构
模型指标非相干度值除了帮助我们进行粗略的分离组件的估计,还能帮助我们避免隐式数据流的设计。我们来看看图 14这个例子。子系统由很多单独的图表组成,主要信号数据流被 goto/from模块隐藏,很难理解信号的属性和流向。统计显示子系统具有非常高的局部复杂度,可读性差。但是同时我们又注意到,非相干度数值只有3,意味着goto/from 模块的使用在观感上分隔了功能组件,但实际上它们可以简化为三个主要的功能组。而重构后的模型图 15表明,到结构层的子系统及数据信号流等方面得到更清晰的呈现,同时也显著降低了局部复杂度。
Incoherence » 3, Local Complexity 1673
图14. 具有隐式数据流和高度不相干子系统的示例
Incoherence » 3, Local Complexity 111
图15. 基于非相干度Stateflow模型指标的模型重构
结构和功能元素
如何理解实现结构和功能的分离?如图 16的Simulink的子系统为例,我们把输入端口和输出端口称为中性块,因为在大多数情况下都必须存在。在子系统内部,没有任何计算在这里进行,只有两个子系统。我们称这样只包含子结构模块的子系统为结构子系统。
图16. Simulink 演示模型 fuelsys 中的结构子系统示例
再如图 17这样的子系统,除了输入和输出端口,其余的模块都也是数学、逻辑或位运算或函数等功能性运算。我们称这样只包含功能运算模块的子系统为功能子系统。
图17. Simulink 演示模型 fuelsys 中的功能子系统示例
如何识别模型的功能和结构水平?为此,我们引入子系统级别上功能模块占比这一模型指标,指标功能模块占比表示子系统中功能模块与功能模块和结构模块之和的百分比。因此,度量值为0%意味着子系统除了中性块外,仅包含结构模块,子系统是0%的功能和100%的结构。指标值为100%表示纯功能子系统,中间值表示相应的混合子系统。
参看表 1中以对功能模块和结构模块的统计和指标功能模块占比,指标功能模块占比100%表示纯功能子系统, -表示纯结构子系统;还有可能是不中100% 的分数,但是,在建模实践中,我们强烈建议避免这些混合,这种混合子系统下同一子系统层上具有结构模块和功能计算模块,可能会影响可读性、可理解性,尤其是可测试性。因此,最好尽量让这些数字变大或尽可能小,而不是趋于中间傎。
表1. 模型指标:功能模块占比
按照ISO26262架构设计的原则的建议,创建子系统的适当层次结构。但什么是适当的呢?您可以在此处将适当定义为结构和功能层之间的专用差异化设计。将结构和功能元素分开,形成信号处理和结构、功能的层次结构架构设计的一致性,提高可读性,模块化,可测试性和修改的适用性等方面的结构质量。
这里我们给出推荐的模型架构设计的最佳实践。在软件开发过程的软件架构部分粗略地定义模型软件架构,同时根据以上我们谈到的相应的模型指标进行模型系统的设计和重构来确保模型系统或子系统符合行业最佳实践和初始的软件架构设计。在详细设计阶段,我们仍然需要注意避免在同一子系统中混合结构和功能模块元素,确保最终得到简易美观分层合理的模型结构。如形成类似图 18所示的适当分层的模型结构,由SimuLink根层作为顶层的系统结构布局,其下层如图中模型层1通过各级子系统实现信号分配和软件架构层,然后其下层模型层K的子系统作为软件架构设计的结构实现。然后,从某层如模型层m开始,作为软件架构的结构层,或者实际功能计算的实现层,直到功能计算的最后一层。即在分层结构中,将结构子系统层保持到最后一层。建模过程中通过合理使用库链接或模型引用简化系统复杂度,最终形成简单易读、易测试、易维护的模型架构。
图18. 模型分层设计的最佳实践
无效接口与接口大小
现在,在谈论子系统的层和结构以及模型分层架构时,我们当然还需要了解子系统之间和进入子系统的信号流和子系统的接口。
按照ISO26262关于软件设计的原则建议,接口大小应限制在合理范围,防止接口过大而导致系统变得难以理解。考虑接口大小限制时,一方面考虑子系统接口总数的限制和信号的总线化处理,另一方面我们需要更加注重的是接口信号的有效供给。也就是说,输入子系统的信号务必与子系统中的功能需求相关的有效输入,防止实质上无用信号的输入。为了更好的理解,让我们看下面图 19的例子。五个基本输入信号进入包含多级子系统和模型引用的子系统中。值得注意的是五个基本信号实际上只使用了两个,即信号a和信号b 用于积运算,而其他信号c,d, signal1是不必要的输入,因为它们事实上没有被后续用于某功能或运算过程。
图19. 未使用的基础信号示例
但在某些情况下,如果我们不了解实际的信号流,如我们难以查看子系统的模型或了解所有模型引用的细节,则很难理解这些信号是否被实际使用。此外,典型的用例如图 20中,我们常常把整个总线馈送到子系统中,但是子系统中本身的功能实现只需求其中部分数量的信号,旁路信号实际上并未被子系统所使用。所以我们希望限制接口的大小,确保单元和组件是可重用性的同时,还要避免出现虚拟耦合。为此我们通常采用强制实施显式信号流,以此提高模型的可读性、模块化、可测试性、可维护性和修改的适用性。
图20. 未使用的旁路信号示例
对于接口大小方面的限制和信号的处理,有什么最佳实践操作呢?首先我们要注意单纯的限定输入输出端口为具体的数值是不合理的。另外,作为接口操作的最佳实践如图 21,我们可以将需求的信号按功能需求分类分组到总线中,最好明确为子系统所需有效信号的基础上,使这些信号流显式表示,如图18中的信号分组与提取过程。因此,我们在子系统外部提取或修改必要的总线信号,在信号选择器上选择我们实际要使用的信号,并且在结构层上明确显示子系统必需的信号流。
图21. 接口操作的最佳实践
克隆组件
现在假设我们已经有了一个很好的模型设计,结构质量各指标都已得到充分优化,我想把某个功能或算法复用到其他地方,这时我们的操作可能是对模块、组件或子系统复制粘贴。当系统中多处形成类似结构,如包含相同属性的子系统部件组,我们也称其为克隆组。关于子系统克隆与克隆组的操作,假设模型中存在一个小子系统,包含一系列运算模块,一些常量,呈现一定布局。作为复用组件,我们复制粘贴它到模型另外一个地方,并进行了一些布局和参数的修改,如图 22所示。可以预见,如果我们重复进行类似操作,模型的复杂度会快速增长,相应也会带来测试和审查的工作量。因此,为了简化设计,降低模型系统的复杂度,可以把具有克隆组的模型作为单独的模型引用。如果某些子系统需要频繁多次在模型中使用,我们可以将其转换为库文件。转换为模型引用或模型库的方式,有助于减少模型的复杂度,减少模型测试和维护的工作量,最终也会减少代码量。
图22. 子系统克隆
因此,找到大量克隆和克隆组,并用库引用替换它们,是针对克隆组件的最佳实践。以便显着降低全局复杂度,这意味着大小和工作量。对一个不同类型大量模块构成的复杂模型系统,通常各模型模块分组分散开发,容易出现模型的复制粘贴和克隆组的情况,模型系统呈现相对较高的全局复杂度。以某项目在克隆组及复杂度方面的研究统计结果【2】为例(图 23),系统中包含了大量的克隆组件和重复的子系统。对克隆组及克隆子系统的优化,可实现10%左右的复杂度缩减。一般来讲,对于大型工程,10%的复杂度降低意味着10%的评审测试工作的减少,意味着大量的人工和成本的节省。
图23. 控制系统的克隆组检测结果统计
当我们研究一个典型的项目开发周期与模型全局复杂度之间的关联,从项目开始到功能完成再到缺陷修复到最后项目结束,我们可以看到全局复杂度在开始时增加很快,后期逐渐饱和,特别是在缺陷修复阶段。但是,如果我们应用模型指标分析并定期进行模型重构,那么模型的总体大小和复杂度会显著降低。也就是说,在整个模型开发周期内进行指标分析,克隆组检测并定期进行模型重构,可有效地降低全局复杂度,提高结构质量。
模型指标分析与模型重构的最佳实践
上面我们罗列了模型相关众多的结构性指标,如何获得具体的相应指标精确数值,为模型的设计和重构提供参考。为此,我们可以借助模型检查工具,比如市场上比较受欢迎的模型检查工具MES Model Examiner。MES Model Examiner不仅是一个模型规则检查工具,而且可以精确统计模型结构质量指标,例如全局/局部复杂度,分层深度和接口大小,克隆组,非相干度等指标等等作为检查实现,并将这些结构质量指标显示在专门视图(图 24)中。如图中显示有各子系统的局部复杂度,如显示红色的局部复杂度806,它高于默认设置的局部复杂度为750的上限,于是模型对局部复杂度规则一致性检查的结果给出失败的结果。
图24. MES Model Examiner模型指标视图
对于复杂模型系统的重构问题,也可以借助工具简化建模和重构的操作。比如MES 的专用模型重构工具MES Model Refactor通过自动化连续建模步骤使特定功能目的的建模变得简单快速,达到模型组件的快速重构。
综上所述,对模型结构属性相关质量如模型子系统的复杂度(局部/全局)、组件大小、非相干度、功能组件的占比、接口大小和克隆组的统计等结构质量的分析,在开发过程中中应用模型指标并定期重构的最佳实践,有助于在模型软件开发阶段增强模型的可读性、可理解性、可维护性和可测试性,有效降低软件复杂度,实施模型组件和功能建模的设计和验证原则,提高模型的结构质量,从而总体上提高模型软件系统的质量。
参考文献:
- Mäurer et al. (2014), On Bringing Object-Oriented Software Metrics into the Model-Based World – Verifying ISO 26262 Compliance in Simulink, 8th International Conference, SAM 2014, Valencia, Spain
- Salecker et al. (2016), JUST SIMPLIFY: Clone Detection for Simulink Controller Models, SAE World Congress 2016, Detroit, MI, USA
最后
以上就是风趣红牛为你收集整理的Simulink模型指标分析与模型重构的最佳实践 - 软件模型质量保证不可忽视的一环的全部内容,希望文章能够帮你解决Simulink模型指标分析与模型重构的最佳实践 - 软件模型质量保证不可忽视的一环所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复