利用非功能信息浏览组件库
1. 组件实现示例
以下是一个组件实现示例,包含了非功能行为(NF - behaviour)模块,为非功能属性(NF - attributes)赋予值:
package body NETWORK is
--I implemented with ADJACENCY_MATRIX
--I behaviour
--I
ErrorRecovery;
FullyPortable;
Test = 3
--I
-- this implies Reliability = Medium
--I
space(Network) = pow(NbItems, 2)
procedure ShortestPath
...
--I
time(ShortestPath) : pow(NbItems, 2)
--I
space(ShortestPath) = NbItems
end NETWORK;
在这个示例中,可靠性(Reliability)NF - 属性的值没有明确给出,因为它可以从其他属性计算得出,我们称其为派生属性。
implemented with
构造用于标记包体,以便后续进行包选择。
2. 查询相关概念
在软件组件选择中,查询是基本的检索操作。其目的是从非功能角度选择更适合新系统的组件实现,并确定选择正确所需满足的条件。查询过程依赖于实现的 NF - 行为,因此后续讨论主要围绕实现而非组件本身。
对于实现范围内的每个 NF - 属性,NF - 行为会明确或隐含地给出其值,这取决于该属性是基本属性还是派生属性。行为模块中出现的 NF - 约束用于确定额外条件。其中,效率是最重要的基本属性之一,可借助工具进行计算,后续还计划扩展此类基本 NF - 属性的集合。
为了后续组合查询,假设选择是从一组初始的候选实现中进行的,因此查询可视为绑定实现集合的映射。具体而言,给定查询
qM
和接口
M
的实现集合
S
(对于
S
中的每个
s
,
<M, s>
从我们的角度来看是一个组件),
qM
在
S
上的评估
qM(S)
会产生以下结果:
- 一个集合
T ⊆ S
,其中
T
中的实现满足
qM
中出现的条件。
- 一个以
T
为索引的查询族
QT = (qt)t∈T
,其中
qt
又是一个以
V
为索引的查询族,
qt = (qt,v(Rv))v∈V
,
V
是
t
导入的组件集合,
Rv
是组件
v
的所有实现集合。查询
qt,v(Rv)
表示为了使
t
被原始查询选中,导入的实现
v
必须满足的条件。
我们将
QT
中的查询称为从属查询,而
qM
则成为主查询。当提及
qM
的评估结果
eval(qM)
时,用
eval(qM).T
表示集合
T
,用
eval(qM).QT
表示查询族
QT
。
从语法角度看,查询被定义为原子查询的列表,即
qM(S) = aqM,1(S1) ⊕ ... ⊕ aqM,k(Sk)
。列表中的项按重要性递减顺序表示条件,因此初始集合
S
可以逐步受限,直到得到最终结果。原子查询与 NF - 约束非常接近,但要求它们以合取范式(CNF)形式出现,并且可以使用一对有用的运算符来选择最大化或最小化给定 NF - 属性值的实现。运算符
⊕
称为限制运算符,其定义将在后续介绍。
3. 原子查询
原子查询
aqM(S)
被定义为可能取反的 CNF 表达式,即
aqM(S) = A
或
aqM(S) = ¬A
,其中
A = A1 ∧ ... ∧ Ar
。应用德摩根定律消除析取,因此
Ai
也可能取反,即
Ai = Bi
或
Ai = ¬Bi
。逻辑连接词实际上被解释为集合运算符。每个
Bi
可以是以下两种形式之一:
- 关系表达式,用于比较可测量属性域的表达式。给定输入集合
S
,关系表达式
E1 < E2
(对于任何定义的排序
<
)的评估
eval(E1 < E2)
定义为:
eval(E1 < E2) = { R∈S / E1[R] < E2[R] }
这里
E[R]
表示使用绑定到
R
的行为模块中的值对
E
进行评估。
- 形式为
max
或
min
的量化表达式,用于选择
S
中最大化或最小化给定表达式
E
的实现子集,定义如下:
eval(max(E)) = { R∈S / (∀T∈S: E[T] ≤ E[R]) }
eval(min(E)) = { R∈S / (∀T∈S: E[T] ≥ E[R]) }
原子查询
aqM(S)
的评估
eval(aqM(S))
如下:
- 计算集合
T
需要对所有
Bi
进行评估
eval(Bi)
,结果为集合
Si ⊆ S
。如果
Ai = Bi
,则
Ai
的评估
eval(Ai)
等于
Si
;如果
Ai = ¬Bi
,则等于
S - Si
。然后,定义
A
的评估为
eval(A) = eval(A1) ∩ ... ∩ eval(Ar)
。最后,如果
aqM(S) = A
,则
eval(aqM(S)) = eval(A)
;如果
aqM(S) = ¬A
,则
eval(aqM(S)) = S - eval(A)
。
- 以
T
为索引的查询族
QT = (qt)t∈T
会产生一个以
V
为索引的查询族
qt = (qt,v(Rv))v∈V
,其中
V
是
t
中所有导入组件的集合,
Rv
是组件
v
的所有实现集合,
qt,v
是
t
内部对
v
规定的 NF - 约束,如果不存在此类 NF - 约束,则假定其为真。
4. 原子查询的组合
限制运算符
⊕
用于组合原子查询以得到主查询的结果,根据不同情况有两种不同的定义。
4.1 开放情况
开放查询计算的主要思想是按原子查询的出现顺序进行评估,直到为要复用的抽象数据类型找到单一实现,从而唯一确定组件。一个原子查询的结果将作为下一个原子查询的输入。然而,存在两种不符合此方案的情况:
- 处理完所有原子查询后,仍可能有多个实现。此时,所有这些实现都被视为查询结果。
- 某个原子查询没有被前一个查询得到的任何实现满足。这种情况下,将前一个原子查询得到的实现作为查询结果。
在这两种情况下,都需要用户进行交互以选择其中一个实现。
评估分两步进行。首先,通过连接两个连续原子查询的输入和输出集合来定义它们之间的联系:
S1 = S.
Si = eval(aqM,i - 1(Si - 1)).T, 1 < i ≤ k.
现在,查询
qM(S)
的评估定义为:
eval(qM(S)) = eval(aqM,i(Si)), 1 ≤ i ≤ n, 使得:
| eval(aqM,i(Si)).T | = 1 ∧ (i > 1 ⇒ | eval(aqM,i - 1(Si - 1)).T | > 1
∨
| eval(aqM,i(Si)).T | > 1 ∧ (i < k ⇒ | eval(aqM,i + 1(Si + 1)).T | = 0).
查询族的计算可直接从集合得出。作为查询评估的正确性条件,必须满足
eval(aqM,1(S1)).T ≠ Ø
。
4.2 封闭情况
如果希望使用现有实现解决所有查询,可能会出现查询处理过程中过度限制实现集合导致从属查询无法解决的情况。因此,重新定义查询
qM(S)
的评估以避免这种情况。定义使用谓词
solvable
来检查是否存在无法解决的从属查询,由于从属查询可能会激活其他查询,该谓词采用递归形式:
eval(qM(S)) = eval(aqM,i(Si)), 1 ≤ i ≤ n, 使得:
∀q: q∈eval(aqM,i(Si)).QT: solvable(q)
∧
{ | eval(aqM,i(Si)).T | = 1 ∧ (i > 1 ⇒ | eval(aqM,i - 1(Si - 1)).T | > 1
∨
| eval(aqM,i(Si)).T | > 1 ∧ (i < k ⇒ | eval(aqM,i + 1(Si + 1)).T | = 0 ∨
∃q: q∈eval(aqM,i + 1(Si + 1)).QT: ¬solvable(q) }.
其中:
solvable(q) ≡ eval(q).T ≠ Ø ∧ ∀q’: q’∈eval(q).QT: solvable(q’).
作为查询评估的正确性条件,必须同时满足
eval(aqM,1(S1)).T ≠ Ø
和
∀q: q∈eval(aqM,1(S1)).QT: solvable(q)
。
5. 选择树
由于查询的评估是以递归形式定义的,因此自然地使用实现树来表示其结果,我们称之为选择树。虽然也可以考虑使用有向图,但这是不正确的,因为在两个不同查询中选择的实现可能对其一个或多个导入组件使用不同的实现。
选择树由以下元素组成:
-
节点
:分为两种类型,接口节点用椭圆表示,实现节点用矩形表示。还有一个特殊的实现节点,称为空节点,当没有实现满足特定查询时会出现。
-
分支
:有两种类型的分支,导入分支从实现节点指向接口节点,用箭头表示;选择分支从接口指向实现,用无向线表示。
实际上,组件节点并非严格必要,但为了清晰起见以及支持某些用户交互,将其包含在内。
6. 示例
假设有一个包
NETWORK_USER_IMPL
使用
NETWORK
组件,该新组件主要用于通过
ShortestPath
操作计算网络中的最短路径,并且假设网络几乎是完全连接的。
NETWORK
存在不同的实现,主要在两个方面有所不同:表示底层图的策略(重点是邻接表和邻接矩阵)以及实现
ShortestPath
操作的算法。对于表示策略,还需考虑数据结构如何使用项(整数)进行索引,有三种情况:项预先已知、项未知但项的数量有界、对项没有任何信息,这里假设是第二种情况。
在这种假设下,实现将使用通用
MAPPING
组件的实例通过项访问数据结构。在邻接表的情况下,映射将列表与项关联;在邻接矩阵的情况下,返回 1 到
N
之间的整数以访问矩阵。
映射上的操作包括插入、删除和使用项作为键进行检索。因此,标准的映射实现(如哈希、AVL 树、有序列表等)将很有用。在哈希的情况下,链式策略将依次使用通用
LIST
组件来链接同义词键。
此外,这些实现还会使用
LIST
组件来构建邻接表和
ShortestPath
操作的结果。
为了使示例规模合理,限制了上述组件的实现集合:列表有三种实现(有序、带指针的无序列表和带数组的无序列表),映射有两种实现(链式哈希和 AVL 树),优先队列也有两种实现(堆和可访问最小元素的 AVL 树),这些优先队列用于某些版本的
ShortestPath
实现(改进的 Dijkstra 算法)。
现在处理以下查询:
min(time(ShortestPath)) and Reliability >= medium ;
min(space(Network)) ; max(Reliability)
该查询可理解为:首先,在保证合理可靠性的前提下最小化查找最短路径的时间(如果只是构建应用程序的第一个原型,可靠性可以放宽);其次,最小化类型表示空间;最后,最大化可靠性。查询的初始集合为上述所有
NETWORK
实现。由于网络的完全连接特性,存在渐近关系
NbConns = power(NbItems, 2)
(
NbConns
和
NbItems
是 NoFun 语句中连接数和项数的表示,用于说明效率)。
查询的选择树如图 5 左侧所示,其生成基于组件的非功能特性。出现了两个歧义,对应于存在两对兄弟节点。为了更清晰地显示,绘制了一个歧义球连接从同一节点发出的所有选择分支。需要与用户进行明确交互,要么提供额外的查询来解决每个歧义,要么直接按名称选择一个实现。在这种情况下,
UNORDERED_POINTERS
在两种情况下都有效,似乎是更优的选择。
以下是部分组件实现的非功能行为(NF - 行为)总结:
| 组件 | 实现 | 空间复杂度 | 时间复杂度 | 可靠性 | 额外要求 |
| ---- | ---- | ---- | ---- | ---- | ---- |
| NETWORK | ADJ LISTS 1 |
NbItems + NbConns
|
(NbItems + NbConns) * log(NbItems)
| 中等 | 对
MAPPING
要求
min(time(Insert, Delete, Get))
;对
LIST
要求
min(time(Put))
和动态存储;对
PRIORITY QUEUE
要求
time(Put, First, RemFirst) <= log(NbElems)
|
| NETWORK | ADJ LISTS 2 |
NbItems + NbConns
|
power(NbItems, 2)
| 中等 | 对
MAPPING
要求
min(time(Insert, Delete, Get))
;对
LIST
要求
min(time(Put))
和动态存储 |
| NETWORK | ADJ MATRIX |
power(NbItems, 2)
|
power(NbItems, 2)
| 高 | 对
MAPPING
要求
min(time(Insert, Delete, Get))
;对
LIST
要求
min(time(Put))
|
| LIST | ORDERED |
NbElems
| 动态存储 | - | - |
| LIST | UNORDERED POINTERS |
1
| 动态存储 | - | - |
| LIST | UNORDERED ARRAY |
1
| 非动态存储 | - | - |
| HASHING | CHAINED HASHING |
1
| 非动态存储 | - | 对
LIST
要求动态存储 |
| HASHING | AVL |
log(NbElems)
| 动态存储 | - | - |
| PRIORITY QUEUE | HEAP |
log(NbElems)
| 非动态存储 | - | - |
| PRIORITY QUEUE | AVL WITH MIN |
log(NbElems)
| 动态存储 | - | - |
为了说明不同效率参数之间的关系在选择过程中的重要性,在另一种情况下重新表述相同的查询,考虑网络连接较少的情况。这种情况可以用渐近关系
NbConns = NbItems
来建模,此时查询会选择第一个网络实现,因为 Dijkstra 算法利用了优先队列的优势。由于对列表使用动态存储的额外约束,列表的实现被唯一确定,而映射的实现不变。对于优先队列,现有的两种实现都满足 NF - 约束,因此都出现在最终的选择树中(图 5 右侧)。
7. 用户交互
为了使组件选择方法更实用,为其添加了引导过程并最终影响结果的能力,主要基于以下原因:
- 如示例所示,查询评估可能会产生模糊的选择树,用户需要从现有选项中进行选择,或者针对涉及的组件制定额外的查询。
- 有些用户可能不想完全依赖查询处理算法,而是希望逐步引导过程并处理原子查询,可能会提前停止过程或切换查询类型以比较结果。
- 主查询或从属查询的正确性条件可能会被违反。在主查询的情况下,需要放宽查询并重新开始过程,不允许选择违反查询的实现。
为了实现用户交互,将选择树显示在屏幕上。可以通过点击候选实现之一来解决歧义,也可以点击歧义球并在新窗口中输入查询。点击空节点时也可以进行此操作,以编辑未解决的查询并使其限制更小。
8. 效率的自动计算
效率的计算结合了三个不同的元素:模式、语法中的综合属性和程序注释。
首先,定义了一个综合属性,用于根据代码的语法布局计算效率。这解决了一些情况,但显然该属性无法处理循环计算。
对于循环,可以进行注释,即程序员可以在 Ada 代码中包含 NoFun 表达式来声明循环的成本,可能以效率参数的形式表示。然而,注释处理起来不太方便,需要程序员额外付出努力,并且使该方案无法应用于现有程序。
因此,定义了模式来识别决定某些效率结果的程序方案。模式可以是全局的,即独立于组件,如用于计算递归过程效率和递增控制变量的循环的模式;也可以是特定的,当识别出组件上的某些程序方案时,例如图遍历的模式。后一种模式不仅有助于避免注释,还能获得更准确的结果。
获得渐近表达式后,可以应用简化演算来得到简化表达式,这些表达式将作为最终结果分配给组件的 NF - 行为。
9. 总结
提出了一种考虑非功能问题的软件复用方案,包括用于描述非功能属性、行为和约束的语言,以相同符号编写的查询形式的复用方法,以及用于计算某些质量因素(目前主要是效率)的框架。查询结果是一个实现树,用户可以对其进行修改,该树遵循组件之间的导入关系。
复用研究考虑了两种不同的情况,即是否强制使用库中的实现来完成最终的树。选择方法的效率不取决于库的大小,而是取决于以下因素:
1. 组件实现的平均数量。
2. 选择树的平均高度。
3. 非原子查询中出现的原子查询数量。
这些因素通常足够适中,以确保良好的响应时间。
与相关的方面分类方案相比,该方案具有一定的优势。相关方案主要用于根据组件提供的功能进行选择,而本方案将方面视为枚举的 NF - 属性,允许其他类型的域,从而推广了方面的概念。此外,相关方案引入了组件之间的相似性概念,而本方案未采用该概念,因为语言的其他特性(如有序域)允许对组件进行排名并检索最大化或最小化一个或多个 NF - 属性值的组件。最后,相关方案只能检索单个组件,而本方案可以检索组件树。不过,相关方案中某些 NF - 属性的模糊性是本方案目前未提供的有趣特性。
利用非功能信息浏览组件库
10. 操作步骤总结
为了更清晰地展示整个组件选择过程,下面总结了从查询到最终选择组件的操作步骤:
1.
定义查询
:根据需求编写查询语句,例如
min(time(ShortestPath)) and Reliability >= medium ; min(space(Network)) ; max(Reliability)
。
2.
确定初始集合
:明确查询的初始实现集合,如示例中的所有
NETWORK
实现。
3.
分解查询为原子查询
:将主查询分解为原子查询,如
min(time(ShortestPath))
、
Reliability >= medium
等。
4.
选择计算情况
:根据实际情况选择开放情况或封闭情况进行原子查询的组合计算。
-
开放情况
:按原子查询顺序评估,处理可能出现的多实现或无满足实现的情况,必要时与用户交互。
-
封闭情况
:使用
solvable
谓词避免从属查询无法解决的问题。
5.
生成选择树
:根据查询评估结果生成选择树,展示组件之间的关系和可选实现。
6.
处理歧义
:如果选择树中出现歧义,通过用户交互解决,如点击候选实现或输入额外查询。
7.
计算效率
:结合模式、综合属性和程序注释计算组件的效率,并进行简化。
8.
最终选择
:根据用户交互和效率计算结果,确定最终的组件实现。
11. 流程图展示
下面是整个组件选择过程的 mermaid 流程图:
graph TD;
A[定义查询] --> B[确定初始集合];
B --> C[分解查询为原子查询];
C --> D{选择计算情况};
D -->|开放情况| E[开放情况计算];
D -->|封闭情况| F[封闭情况计算];
E --> G[生成选择树];
F --> G[生成选择树];
G --> H{是否有歧义};
H -->|是| I[用户交互解决歧义];
H -->|否| J[计算效率];
I --> J[计算效率];
J --> K[最终选择];
12. 不同组件实现的比较
为了更直观地比较不同组件实现的性能,下面进一步分析各组件实现的特点:
| 组件 | 实现 | 空间复杂度 | 时间复杂度 | 可靠性 | 额外要求 | 适用场景 |
| ---- | ---- | ---- | ---- | ---- | ---- | ---- |
| NETWORK | ADJ LISTS 1 |
NbItems + NbConns
|
(NbItems + NbConns) * log(NbItems)
| 中等 | 对
MAPPING
要求
min(time(Insert, Delete, Get))
;对
LIST
要求
min(time(Put))
和动态存储;对
PRIORITY QUEUE
要求
time(Put, First, RemFirst) <= log(NbElems)
| 网络连接数和节点数适中,对时间和空间复杂度有一定要求的场景 |
| NETWORK | ADJ LISTS 2 |
NbItems + NbConns
|
power(NbItems, 2)
| 中等 | 对
MAPPING
要求
min(time(Insert, Delete, Get))
;对
LIST
要求
min(time(Put))
和动态存储 | 网络连接数和节点数相对较少,对时间复杂度要求不高的场景 |
| NETWORK | ADJ MATRIX |
power(NbItems, 2)
|
power(NbItems, 2)
| 高 | 对
MAPPING
要求
min(time(Insert, Delete, Get))
;对
LIST
要求
min(time(Put))
| 网络连接数较多,对可靠性要求较高的场景 |
| LIST | ORDERED |
NbElems
| 动态存储 | - | - | 需要有序存储元素的场景 |
| LIST | UNORDERED POINTERS |
1
| 动态存储 | - | - | 对插入和删除操作效率要求较高,不要求元素有序的场景 |
| LIST | UNORDERED ARRAY |
1
| 非动态存储 | - | - | 元素数量相对固定,对空间利用效率要求较高的场景 |
| HASHING | CHAINED HASHING |
1
| 非动态存储 | - | 对
LIST
要求动态存储 | 快速查找元素,允许一定哈希冲突的场景 |
| HASHING | AVL |
log(NbElems)
| 动态存储 | - | - | 对查找、插入和删除操作都有较高效率要求的场景 |
| PRIORITY QUEUE | HEAP |
log(NbElems)
| 非动态存储 | - | - | 需要高效处理优先级的场景,对空间利用要求不高 |
| PRIORITY QUEUE | AVL WITH MIN |
log(NbElems)
| 动态存储 | - | - | 需要高效处理优先级,并且对动态存储有要求的场景 |
13. 实际应用中的注意事项
在实际应用中,使用该组件选择方法时需要注意以下几点:
1.
查询的准确性
:编写查询时要准确表达需求,考虑各非功能属性之间的平衡,避免过于严格或宽松的查询条件。
2.
组件库的完整性
:确保组件库中包含足够的实现选项,否则可能无法得到理想的结果。
3.
用户交互的时机
:及时与用户进行交互,特别是在出现歧义或正确性条件被违反时,避免选择不合理的实现。
4.
效率计算的准确性
:合理使用模式、综合属性和程序注释来计算效率,尽量减少误差。
5.
可扩展性
:考虑方案的可扩展性,以便在未来能够轻松添加新的组件和实现。
14. 未来展望
随着软件系统的不断发展,对非功能属性的关注将越来越重要。未来可以在以下方面对该方案进行进一步改进和扩展:
1.
支持更多非功能属性
:除了效率和可靠性,还可以考虑其他非功能属性,如安全性、可维护性等。
2.
优化查询处理算法
:提高查询处理的效率,特别是在处理大规模组件库时。
3.
增强用户交互体验
:提供更友好的用户界面和交互方式,方便用户进行选择和调整。
4.
集成更多工具和技术
:与其他软件开发工具和技术进行集成,提高整个开发过程的效率。
5.
研究模糊性处理
:借鉴相关方案中 NF - 属性的模糊性处理方法,使组件选择更加灵活。
通过不断改进和完善该方案,可以更好地满足软件复用的需求,提高软件开发的质量和效率。