用例图及用例描述_kun_lun_jian的博客-CSDN博客_什么是用例描述
ID | 用例的标识 |
---|---|
名称 | 对用例的内容的精确描述,体现了用例所描述的任务 |
参与者 | 描述系统的参与者和每个参与者的目标 |
触发条件 | 标识启动用例的事件 |
前置条件 | 用例能够正常启动和工作的需要的系统状态 |
后置条件 | 用例执行完成后的系统状态 |
正常流程 | 在常见的符合预期的条件下,系统与外界的行为交互序列 |
扩展流程 | 用例中可能发生的其他场景 |
特殊需求 | 和用例相关的其他特殊需求,尤其是非功能性需求 |
黑盒测试与白盒测试
黑盒测试(基于规格)
黑盒测试(black-box testing)是把测试对象看做一个黑盒子,完全基于输入和输出数据来判定测试对象的正确性。测试使用测试对象的规格说明来设计输入和输出数据。
早期的黑盒测试主要使用等价类划分
、边界值分析
等简单方法[Myers1979],后来人们为形式化模型、UML等各种规格手段都建立了相应的测试方法[Bochmann1994,Bertolino2003]。
1. 等价类划分
等价类划分是把所有可能的输入数据,即程序的输入域划分成若干部分(子集),然后从每一个子集中选取少数具有代表性的数据作为测试用例。该方法是一种重要的、常用的黑盒测试用例设计方法。
如图19-8所示,等价类是指某个输入域的子集合。在该子集合中,各个输入数据对于揭露程序中的错误都是等效的,并合理地假定:测试某等价类的代表值就等于对这一类其他值的测试。因此,可以把全部输入数据合理划分为若干等价类,在每一个等价类中取一个数据作为测试的输入条件,就可以用少量有代表性的测试数据取得较好的测试结果。
等价类划分可以有两种不同的情况:
● 有效等价类:是指对于程序的规格说明来说是
合理的
、有意义的
输入数据构成的集合。
利用有效等价类可检验程序是否实现了规格说明中所规定的功能和性能。
● 无效等价类:与有效等价类的定义恰好相反。
设计测试用例时,要同时考虑这两种等价类。因为软件不仅要能接收合理的数据,也要能经受意外的考验。这样的测试才能确保软件具有更高的可靠性。
例如,在测试MSCS的Sales类getChange(double payment)
方法时,依据如表17-1所示的规格说明,可以将其输入数据划分为三类:①有效数据;②无效数据,payment<=0;③无效数据,payment<total。这样就可以依据等价类划分方法设计Sales类getChange(double payment)
方法的测试用例如表19-3所示。
2. 边界值分析
边界值分析方法是对等价类划分方法的补充。因为经验表明,错误最容易发生在各等价类的边界上,而不是发生等价类内部。因此针对边界情况设计测试用例,可以发现更多的缺陷。
例如,在Sales类getChange(double payment)
方法所划分的三个等价类中:
根据等价类①可以得到边界值payment=total,payment=total+1,payment=total-1;
根据等价类②可以得到边界值payment=-1,payment=0,payment=1;
根据等价类③可以得到边界值payment=total,payment=total-1。
这样,建立的测试用例如表19-4所示。
3. 决策表
决策表是为复杂逻辑判断设计测试用例的技术。决策表是由
条件声明
、行动声明
、规则选项
和行动选项
四个象限组成的表格。
决策表是一种决策逻辑的表示方法,用于描述复杂决策逻辑,其基本结构如表18-2所示。
条件声明是进行决策时需要参考的变量列表。条件选项是那些变量可能的取值。动作声明是决策后可能采取的动作。动作选项表明那些动作会在怎样的条件下发生。
/**
* 礼品赠送事件触发决策
* 各个不同级别的赠送亨件可以同时触发,例如新会员一次性购
* 买产生了6000积分,就同时触发1级、2级与3级三个事件
*/
//prePoint是增加之前的积分额度;
//postPoint是增加之后的积分额度;
//如果首次积分超过100o,触发1级礼品赠送事件
if((prePoint<1000)&&(postPoint>=1000)) {
triggerGiftEvent(1);
}
//如果首次积分超过200o,触发⒉级礼品赠送事件
if((prePoint<2000)&&(postPoint>=2000)){
triggerGiftEvent(2);
}
//如果首次积分超过5000,触发3级礼品赠送事件
if((prePoint<5000)&&(postPoint>=5000)){
triggerGiftEvent(3);
}
例如,"礼品赠送事件触发决策"所描述的复杂决策可以建立正式的决策表(如表18-3)。
使用决策表描述复杂决策能够保证决策分析的完备性。决策表列举了所有可能出现的决策规则和行动,基于决策表的描述通常很少会发生规则遗漏和考虑不周的情况。
使用决策表,还能方便表驱动编程的使用,以将复杂决策代码简单化处理,就像图18-13所示的那样。
如果一个测试对象的规格是复杂逻辑判断,那么就可以为其建立决策表,并依据决策表设计测试用例:每一列规则选项为一个测试用例的输入,相应的条件选项为测试用例的预期输出。
例如,在测试MSCS的礼品赠送事件时,依据其规则建立的决策表为表18-3。从表18-3中可以发现三种规则选项及条件选项:
1 ) prePoint<1000&&postPoint>=1000→GiftLevel=1;
2 ) prePoint<2000&&postPoint>=2000→GiftLevel=2;
3 ) prePoint<5000&&postPoint>=5000→GiftLevel=3。
据此可以设计测试用例,如表19-5所示。
4. 状态转换
状态转换测试是专门针对复杂测试对象的测试技术。该类复杂测试对象对输入数据的反应是多样的,还需要依赖自身的状态才能决定。如果测试对象的状态不同,那么即使输入数据是一样的,输出也会有所不同。
使用状态转换测试技术时,通常要先为测试对象建立状态图,描述测试对象的状态集合、输入集合和输入导致的状态转换集合。以状态图为基础,可以建立测试对象的状态转换表。状态转换表的每一行都应该被设计为测试用例。
例如,在18.5.2节测试MSCS的Sales类时,就使用了状态转换测试,按照上述步骤设计了测试用例。
状态转换包括有效转换,也包括无效转换。在很多情况下,只需要为有效转换设计测试用例即可。在复杂情况或者可靠性要求较高的情况下,也会要求为无效转换设计测试用例。
白盒测试(基于代码)
与黑盒测试将测试对象看做黑盒进行测试不同,白盒测试将测试对象看做透明的,不关心测试对象的规格,而是按照测试对象内部的程序结构来设计测试用例进行测试工作。在20世纪70年代后期和80年代,白盒测试方法是被研究最多的主流方法[Bertolino2004]。
最常用的白盒测试方法是语句覆盖
、路径覆盖
和分支覆盖
[Huang1975]。
1. 语句覆盖
语句覆盖设计测试用例的标准是确保被测试对象的每一行程序代码都至少执行一次。
例如,对如下所示的程序代码,可以建立如图19-10所示的程序流程图。
/**
* Customer.getBonus()示例代码
*/
public class Customer {
int bonus;//积分额度
...
//预计算消费行为后的积分额
int getBonus(boolean cashPayment,int consumption,boolean vip) {
//已有的bonus需要调整getBonus,不能直接使用属性bonus
int preBonus=this.getBonus();
if(cashPayment){
preBonus+=consumption ;
}
if(vip){
preBonus*=1.5;
} else {
preBonus*=1.2;
}
return preBonus;
}
}
为了清晰地解释对语句的覆盖,图19-10为各条语句都进行了编号。可以发现如果按照a,b,c,d,e,f,h和a,b,c,d,e,g,h两条路径执行,就能够覆盖所有的语句。将测试用例设计为表19-6所示,就能执行上述两条路径。
相比于条件覆盖与路径覆盖,语句覆盖是一种比较弱的代码覆盖技术,不能覆盖所有的执行路径。例如,在Customer.getBonus()
的示例中,cashPayment为false的情况就没有得到测试。
2. 条件覆盖
条件覆盖设计测试用例的标准是确保程序中每个判断的每个结果都至少满足一次。条件覆盖保证判断中的每个条件都被覆盖了,这样就可以避免测试
Customer.getBonus()
时cashPayment为false没有得到测试的情况。
使用条件覆盖测试Customer.getBonus()
时,可以发现它有两个判断if (cashPayment)
和if(vip)
,需要让cashPayment和vip取true与false各一次,设计测试用例如表19-7所示。
条件覆盖的覆盖程度比语句覆盖强,但是仍然不能保证覆盖所有的执行路径。例如,cashPayment=true&&vip=false
和cashPayment=false&&vip=true
这两个条件下的路径就没有得到执行。
3. 路径覆盖
路径覆盖设计测试用例的标准是确保程序中每条独立的执行路径都至少执行一次。例如,使用路径覆盖设计
Customer.getBonus()
的测试用例如表19-8所示。
集成测试
集成的策略
大爆炸集成
自顶向下集成
优点:
● 按深度优先可以首先实现和验证一个完整的功能需求;
● 只需最顶端一个驱动( driver );
● 利于故障定位。
缺点:
● 桩的开发量大;
● 底层验证被推迟,且底层组件测试不充分。
因此,自顶向下集成适用于
控制结构比较清晰和稳定
、高层接口变化较小
、底层接口未定义或经常可能被修改
、控制组件具有较大的技术风险
的软件系统。
自底向上集成
优点:
● 对底层组件行为较早验证;
● 底层组件开发可以并行;
● 桩的工作量少;
● 利于故障定位。
缺点:
● 驱动的开发工作量大;
● 对高层的验证被推迟,设计上的高层错误不能被及时发现。
因此,自底向上集成适合应用于
底层接口比较稳定
、高层接口变化比较频繁
、底层组件较早被完成
的软件系统。
持续集成
桩、驱动与集成测试用例
类间关系
依赖
语义
一个类A使用到了另一个类B,但是这种使用关系是具有偶然性的、临时性的、非常弱的,但是类B的变化会影响到类A
语法
类B作为类A的方法的参数(或者局部变量)存在
UML图示
由类A指向类B的带箭头虚线表示
关联
语义
比依赖关系强,必然的,长期的,强烈的;
分为单向关联(只是班级中增加了学生)、双向关联(在学生中也添加班级属性)
分为一对一(学生和学生证)、一对多(班级和学生)、多对多关联(学生和课程)
有两个类的关联(客户和订单、订单和商品)、还有一个类和自身关联(领导也是员工)
语法
类B作为成员变量形成存在于类A中
UML图示
由类A指向类B的带箭头虚线表示;双向关联可以取消两个箭头
在关联的两端可以标注关联双方的角色和多重性标记
聚合
语义
关联关系的一种特例
整体和部分的关系
整体部分可分离,整体的生命周期和部分的生命周期不同,has-a的关系
计算机与CPU、公司与员工的关系、班级和学生的关系
语法
同关联关系
UML图示
空心菱形加实线箭头
组合
语义
关联关系的一种特例
整体和部分关系、整体部分不可分离、比聚合更强 ,contains-a的关系
整体的生命周期和部分的生命周期相同
人和四肢的关系
语法
同关联关系
UML图示
实心菱形加实线箭头
继承
语义
类和子类的关系,接口和子接口的关系;
一个类(称为子类、子接口)继承另外的一个类(称为父类、父接口)的功能,并可以增加它自己的新功能
语法
extends
UML图示
一条带空心三角箭头的实线,从子类指向父类,或者子接口指向父接口。
实现
语义
类和接口之间的关系
一个类可以实现多个接口,实现所有接口的功能;体现了规范和实现分离的原则
语法
implements
UML图示
实现用一条带空心三角箭头的虚线表示,从类指向实现的接口
总结
- 组合>聚合>关联>依赖
- 关联和依赖的区别
关联关系强、长期
关联关系是通过属性来实现;依赖关联是通过方法形参或者局部变量实现
- 关联、组合/聚合的异同
都是关联,都是做类的属性
组合 /聚合表示的是整体和部分的关系,关联可以表示所有关系
- 组合和聚合的异同
都是关联的特例,都是表示的整体和部分的关系
整体部分的生命周期是否相同?组合更强
内聚
内聚(Cohesion)是一个模块内部各成分之间相关联程度的度量。
1. 偶然内聚
如果一个模块的各成分之间毫无关系,则称为偶然内聚。
模块的各成分之间没有关联,只是把分散的功能合并在一起。
例:A模块中有三条语句(一条赋值,一条求和,一条传参),表面上看不出任何联系,但是B、C模块中都用到了这三条语句,于是将这三条语句合并成了模块A。模块A中就是偶然内聚。
public class Rous {
//模式匹配,全局搜索
public static int findPattern(String text, String pattern){...}
//求平均值
public static int average (Vector numbers){...}
//打开文件
public static OutputStream openFile(String fileName){...}
}
public class TextUtils {
public static int findPattern(String text, String pattern){...}
}
public class MathUtils {
public static int average (Vector numbers){...}
}
public class FileUtils {
public static OutputStream openFile(String fileName){...}
}
总结
判别特征:模块执行多个
完全不相关
的操作。
修改方式:将操作按相关性封装进不同的模块中。
在判别偶然内聚的时候,需要从各个操作的语义入手,分析语义之间的相关性。
2. 逻辑内聚
几个逻辑上相关的功能被放在同一模块中,则称为逻辑内聚。如一个模块读取各种不同类型外设的输入。尽管逻辑内聚比偶然内聚合理一些,但逻辑内聚的模块各成分在功能上并无关系,即使局部功能的修改有时也会影响全局,因此这类模块的修改也比较困难。
例:A模块实现的是将对应的人员信息发送给技术部,人事部和财政部,决定发送给哪个部门是输入的控制标志决定的。模块A中就是逻辑内聚。
public void sample(String flag){
switch (flag){
case "ON" :
...
break;
case "0FF":
...
break;
case "CLOSE":
...
break;
}
}
public void sampleOn() {
...
}
public void sampleOff() {
...
}
public void sampleClose() {
...
}
判别特征:
if-else
语句,且每个分支后的操作具有并立性;
switch-case
语句,且每个分支后的操作具有并立性。
修改方式:将每个分支后的操作单独封装成模块。
3. 时间内聚
如果一个模块完成的功能
必须在同一时间内执行(如系统初始化)
,但这些功能只是因为时间因素关联在一起,则称为时间内聚。
例:编程开始时,程序员把对所有全局变量的初始化操作放在模块A中。模块A中就是时间内聚。
public class A {
private:
FinancialReport fr;
WeatherData w;
int totalCount;
public:
A();//构造器
void init();//初始化
~A();//析构
}
void init(){
//初始化财务报告
fr=new( FinancialReport);
fr.setRatio(5);
fr.setYear("2010");
//初始化当前天气
w=new(WeatherData) ;
W.setCity("NanJing");
W.setCode(“210093") ;
//初始化计数器
totalCount=0;
}
void initFinancialReport {
//初始化财务报告
fr=new( Financia lReport);
fr.setRatio(5);
fr.setYear("2010");
}
void initWeatherData {
//初始化当前天气
w=new(WeatherData) ;
W.setCity("NanJing");
W.setCode("210093") ;
}
void initTotalCount {
//初始化计数器
totalCount=0;
}
判别特征:初始化方法
修改方式:将每个初始化操作单独封装
4. 过程内聚
如果一个模块内部的处理成分是相关的,而且这些处理
必须以特定的次序执行
,则称为过程内聚。
例:用户登陆了某某网站,A模块负责依次读取用户的用户名、邮箱和联系方式,这个次序是事先规定的,不能改变。模块A中就是过程内聚。
public void renewCar(){
洗车();
消除凹痕();
打磨();
上蜡();
}
public class TestCase3 {
public void MonthEnd(){
Report ExR=InitExpenseReport();//1
Report r=InitRevenueReport();
Report EmR=InitEmployeeReport();
EmR.Init();//2
r.init();
ExR. SetEmployees(true);
if(ExR.GetReportParams()){//3
EmR.GetReportParams();
}
SendToPrinter(r);//4
SendToPrinter(ExR);
SendToPrinter(EmR);
}
}
判别特征:模块内部多个操作之间的顺序不可颠倒
5. 通信内聚
如果一个模块的所有成分都
操作同一数据集或生成同一数据集
,则称为通信内聚。
例:模块A实现将传入的Date类型数据转换成String类型,以及将Date类型数据插入数据库,这两个操作都是对“Date类型数据”而言的。模块A中就是通信内聚。
public class Calculate {
public int product;
public void produce(int a, int b){
product= a*b;
...
save (product) ;
...
}
public void save(int product){
//code to store value into database
...
}
}
public class TestCase4 {
public void validate_checkout_request(input_form i){
if(!valid_string(i.name)){//1
error_message ("Invalid name"); .
}
if(!valid_string(i.book)){//2
error_message("InvaLid book name");
}
if(!valid_month(i.date)){//3
error_message("InvaLid month");
}
}
}
判别特征:模块内部多个操作使用同一个数据结构
6. 功能内聚
模块的所有成分
对于完成单一的功能都是必须的
,则称为功能内聚。
例:模块A实现将新注册的用户信息(用户名,密码,个性签名)全部转换成String类型并插入数据库。模块A中就是功能内聚。
public int commission(int sale,long percentage) {
int com;
//calculate commission
return com;
}
判别特征:单一操作或单一目的
7. 信息内聚
信息内聚:一个模块如果它执行每个动作都有它自己的入口点,每个动作都包含独立代码,所有的动作都作用于在相同的数据结构,那么这个模块具有信息内聚性。
信息内聚模块可以看成是多个功能内聚模块的组合,并能达到信息的隐蔽。即把某个数据结构、资源或设备隐蔽在一个模块内,不为别的模块所知晓。当把程序某些方面细节隐藏在一个模块中时,就增加了模块的独立性。
public interface Address {
public abstract String getName();
public abstract String getAddress();
...
}
public class Employee impLements Address{
public String getName() {
return null;
}
public String getAddress() {
return nulL;
}
}
判别特征:接口 抽象类
耦合
耦合(Coupling)是模块之间依赖程度的度量。
1. 内容耦合
当一个模块直接修改或操作另一个模块的数据,或者直接转入另一个模块时,就发生了内容耦合。此时,被修改的模块完全依赖于修改它的模块。
例:模块A中定义了变量a,在模块B中直接使用了。这种情况下模块A和模块B就是内容耦合。
public class Vector3D {
public int x,y,z;
...
}
public class Arch{
private Vector3D baseLine;
...
void slant(int newY){
baseLine.x=10;
baseLine.y=13;
}
}
public class Vector3D {
private int x,y,Z;
public int getX() {
return x;
}
public void setX(int x) {
this.x = x;
}
public int getY() {
return y;
}
public void setY(int y) {
this.y = y;
}
public int getZ() {
return z;
}
public void setZ(int z) {
this.Z = Z;
}
}
cLass Arch {
private Vector3D baseLine;
public void slant() {
baseLine.setX(10);
baseLine.setY(13);
}
}
public void validate_checkout_request(input_form i){
if(!valid_string(i.name)){
i.string="Invalid name" ;//i.setString
error_message();
}
if(!valid_string(i.book)){
i.book="Invalid book name";//i.setString
error_message();
}
valid_month(i.date);
}
public int valid_month(date d){
if(d.month<1){
d.month=1;//d.setMonth
}
if(d.month>12){
d.month=12;//d.setMonth
}
return 1;
}
public class Grade {
public double averageGradeForTop50(ArrayList<Student> allStudent){
ArrayList<Student> sortedStudent = allStudent;
double totalGrade=0;
for(int i=0;i<50;i++){
totalGrade+=sortedStudent.get(i).getGrade();
}
double averageGrade=totalGrade/50.0;
return averageGrade;
}
}
public class Grade {
public double averageGradeForTop50(ArrayList<Student> allStudent){
ArrayList<Student> sortedStudent = allStudent;
double totalGrade=0;
//for(int i=0;i<50;i++){
// totalGrade+=sortedStudent.get(i).getGrade();
//}
Iterator<Student> it =sortedStudent.iterator();
for(int i=0;i<50;i++){
Student stu=it.next();
totalGrade+=stu.getGrade();
}
double averageGrade=totalGrade/50.0;
return averageGrade;
}
}
判别特征:1.未通过set方式修改其他模块的属性
2.未通过迭代器遍历集合
3.alter、goto关键字
修改方式:1.通过set方式修改其他模块的属性
2.通过迭代器遍历集合
3.在不改变原有代码功能的基础上,去除alter、goto关键字
2. 公共耦合
两个以上的模块共同引用一个全局数据项就称为公共耦合。
例:定义了一个全局变量a,在A、B、C模块中均调用了a,这种情况下模块A、模块B、模块C就是公共耦合。
int x;
public class myValue {
public void addValue(int a){
x=x+a;
}
public void substractValue(int a){
x=x-a;
}
}
public class myValue {
public int addValue(int a,int x){
return x+a;
}
public int substractValue(int a,int x){
return x-a;
}
}
String patron_name, book_name;
Date checkout_date;
void validate_checkout_request(input_form i){
patron_name=i.name;
if(!valid_string()){
error_message("Invalid name");
}
book_name=i.book;
if(!valid_string()){
error_message("Invalid book name");
}
checkout_date =i.date;
if(!valid_month()){
error_message("Invalid month");
}
}
int valid_month() {
return checkout_ date.month> =1 && checkout_ date.month<= 12;
}
String patron_name, book_name;
Date checkout_date;
void validate_checkout_request(input_form i){
patron_name=i.name;
if(!valid_string()){
error_message("Invalid name");
}
book_name=i.book;
if(!valid_string()){
error_message("Invalid book name");
}
checkout_date =i.date;
if(!valid_month(checkout_date.month)){
error_message("Invalid month");
}
}
int valid_month(int month) {
return month> =1 && month<= 12;
}
判别特征:模块间共享全局变量
修改方式:将共享的全局变量以形参的方式传入
3. 重复耦合
模块之间有同样逻辑的重复代码
void validate_checkout_request(input_form i){
int len=0;
boolean valid_string=false;
len=i.name.length();
char arr1[] = new char[len] ;
for(char c : arrl){
if (c是小写字母) {
valid_string=true;
}
}
if (!valid_string) {
error_message("Invalid name");
}
len=i.book.length();
char arr2[]=new char[len];
for (char c: arr2) {
if (C是小写字母) {
valid_string=true;
}
}
if(!valid_string) {
error_message ( "Invalid book name");
}
if(!valid_month(i.date)){
error_message ("Invalid month");
}
}
void validate_checkout_request(input_form i){
if (!validString(i.name)) {
error_message("Invalid name");
}
if(!validString(i.book)) {
error_message ( "Invalid book name");
}
if(!valid_month(i.date)){
error_message ("Invalid month");
}
}
public boolean validString(String info) {
int valid_string=false;
int len=info.length();
char arr[] = new char[len] ;
for(char c : arr){
if (c是小写字母) {
valid_string=true;
}
}
return valid_string;
}
private Double getTotalSum(List amounts){
Double totalToPay =0.00;
Iterator amountsIterator = amounts.iterator();
while (amountsIterator.hasNext()){
Amount amount = (Amount) amountsIterator.next() ;
if(!cancelStatuses.contains ( amount.getStatus())){
totalToPay+=amount.doubleValue();
}
}
return totalToPay;
}
private Double getTotalSumExcludeCancelAmount(List amounts){
Double totalToPay=0.00;
Iterator amountsIterator = amounts.iterator();
while (amountsIterator.hasNext( ) ){
Amount amount = (Amount)amountsIterator.next() ;
if(!amounts.getIsToCancel()) {
// Additional condition comparing to the first method
if (!cancelStatuses.contains (amount.getStatus())) {
totalToPay += amount.doubLeValue();
}
}
}
return totalToPay;
}
private Double getTotalSumExcludeCancelAmount(List amounts,boolean flag){
Double totalToPay=0.00;
Double totalToPayEx=0.00;
Iterator amountsIterator = amounts.iterator();
while (amountsIterator.hasNext()){
Amount amount = (Amount)amountsIterator.next() ;
if (!cancelStatuses.contains (amount.getStatus())) {
totalToPay += amount.doubLeValue();
if(!amounts.getIsToCancel()) {
totalToPayEx += amount.doubLeValue();
}
}
}
if(flag) return totalToPayEx;
return totalToPay;
}
判别特征:模块间有相同逻辑的重复代码
修改方式:抽象封装重复代码,然后复用;过滤器
4. 控制耦合
一个模块在界面上传递一个信号(如开关值、标志量等)控制另一个模块,接收信号的模块的动作根据信号值进行调整,称为控制耦合。
例:模块A获取用户类型(普通用户、高级用户)传递给模块B,模块B根据不同类型的用户提供不同的服务。这种情况下模块A和模块B就是控制耦合。
public routineX(String command){
if( command.equals ("drawCircle")){
drawCircle();
}else{
drawRectangle();
}
}
void validate_checkout_request(input_ form i){
if( !valid(i.name, STRING)){
error_message("Invalid name");
}
if( !valid(i.book, STRING)){
error_message ("Invalid book name" ) ;
}
if( !valid(i.date, DATE)){
error_message("Invalid month");
}
}
int valid(String s, int type){
switch(type){
case STRING:
return strlen(s)<MAX_STRING_SIZE;
case DATE:
Date d= parse_date(s);
return d.month >= 1 && d.month <= 12;
}
}
5. 印记耦合
模块间通过参数传递复杂的内部数据结构,称为标记耦合。此数据结构的变化将使相关的模块发生变化。
例:模块A向模块B传递Object类型的数据。这种情况下模块A和模块B就是标记耦合。
void validate_checkout_request(input_form i){
if(!valid_string(i)){
error_message ( "Invalid name");
}
if(!valid_string(i)){
error_message ("Invalid book name") ;
}
if(!valid_month(i)){
error_message ("Invalid month");
}
}
int valid_month(input_form i){
return i.date.month >=1 && i.date.month <=12;
}
public class Employee{
public String name,emailId;
}
public class Emailer{
public void sendEmail(Employee e,String text){
...
}
...
}
6. 数据耦合
模块间通过参数传递基本类型的数据,称为数据耦合。
例:模块A实现两个数的加法操作,模块B实现两个加数的初始化,模块B将两个加数传给模块A,模块A进行相加。这种情况下模块A和模块B就是数据耦合。
public class Receiver{
public void message (MyType X){
...
X.doSomethingForMe(0bject data);
...
}
}
软件体系结构风格
一、主程序/子程序风格
main program/subroutine style
1. 简介
如图9-4所示,主程序/子程序风格将系统组织成层次结构,包括一个主程序和一系列子程序。主程序是系统的控制器,负责调度各子程序的执行。各子程序又是一个局部的控制器,负责调度其子子程序的执行。
2. 设计决策与约束
主程序/子程序风格的重要设计决策与约束有:
● 基于声明——使用(程序调用)关系
建立连接件,以层次分解的方式
建立系统部件,共同组成层次结构。
● 每一个上层部件可以“使用”下层部件,但下层部件不能“使用”上层部件,即不允许逆方向调用
。
● 系统应该是单线程执行
。主程序部件拥有最初的执行控制权,并在“使用”中将控制权转移给下层子程序。
● 子程序只能够通过上层转移来获得控制权,可以在执行中将控制权转交给下层的子子程序,并在自身执行完成之后将控制权交还给上层部件。
3. 实现
主程序/子程序风格的主要实现机制是模块实现,它将每个子程序都实现为一个模块, 主程序实现为整个系统的起始模块(在很多语言中,即为包含main函数的模块)。依照抽象规格的层次关系,实现模块也被组织为相应的层次机构,通过导入/导出关系相连接。
需要强调的是,虽然主程序/子程序风格非常类似于结构化程序的结构,但是主程序/子程序风格是基于部件与连接件建立的高层结构。
它的部件不同于程序,而是更加粗粒度的模块。而且,在部件的实现模块内部,可以使用结构化分析方法,也可以使用面向对象方法,这并不妨碍整个系统的高层结构符合主程序/子程序风格的约定。
4. 优点
● 流程清晰,易于理解。
严格的层次分解
使得整个系统的结构组织非常符合功能分解
和分而治之
的思维方式,从而能够清晰地描述整个系统的执行流程,易于理解。
● 强控制性。严格的层次分解
和严格的控制权转移
使得主程序/子程序风格对程序的实际执行过程具备很强的控制能力,这使得如果一个子程序所连接的子子程序是正确的,那么就很容易保证该子程序的“正确性"。所以,主程序/子程序风格比其他常见风格更能控制程序的“正确性"。
5. 缺点
● 程序调用是一种强耦合的连接方式,非常依赖交互方的接口规格,这会使得系统难以修改和复用。
● 程序调用的连接方式限制了各部件之间的数据交互,可能会使得不同部件使用隐含的共享数据交流,产生不必要的公共耦合,进而破坏它的“正确性”控制能力。
6. 应用
主程序/子程序风格主要用于能够将系统功能依层次分解为多个顺序执行步骤的系统。
[Shaw1996]发现,在很多受到限制的编程语言环境下(这些编程语言没有模块化支持),系统通常也会使用主程序/子程序风格,这时主程序/子程序风格的实现是程序实现,即主程序和子程序都被实现为单独的程序。
一些使用结构化分析方法(自顶向下或自底向上)建立的软件系统也属于主程序/子程序风格。
二、面向对象式风格
object-oriented style
1. 简介
如图9-5所示,面向对象式风格借鉴面向对象的思想组织整个系统的高层结构。
面向对象式风格将系统组织为多个独立的对象,每个对象封装其内部的数据,并基于数据对外提供服务。
不同对象之间通过协作机制共同完成系统任务。
2. 设计决策与约束
面向对象式风格的重要设计决策与约束有:
● 依照对数据的使用情况,用信息内聚的标准为系统建立对象部件。每个对象部件基于内部数据提供对外服务接口,并隐藏内部数据的表示。
● 基于方法调用( method invocation )机制
建立连接件,将对象部件连接起来。
● 每个对象负责维护其自身数据的一致性与完整性,并以此为基础对外提供“正确”的服务。
● 每个对象都是一个自治单位,不同对象之间是平级的,没有主次、从属、层次、分解等关系。
3. 实现
关于面向对象式风格的实现,需要强调说明的是它的“对象”是部件,属于高层结构的元素,虽然名称相同,但它并不是面向对象分析方法中所述的“对象”实体。
“面向对象式”风格的命名是因为它借鉴了面向对象分析方法的思想,而不是因为它使用面向对象分析方法实现体系结构,这也是在该风格名称中有一个“式”字的原因。
面向对象式风格的主要实现机制是模块实现,它将每个对象部件实例都实现为一个模块。
存在连接的对象部件实例之间会存在模块的导入/导出关系。
每个模块内部可以是基于面向对象分析方法的实现,也可以是基于结构化方法的实现。
4. 优点
● 内部实现的可修改性。因为面向对象式风格要求封装内部数据,隐藏内部实现,所以它可以在不影响外界的情况下,变更其内部实现。
● 易开发、易理解、易复用的结构组织。面向对象式风格将系统组织为一系列平等、自治的单位,每个单位负责自身的“正确性”,不同单位之间仅仅是通过方法调用相连接,这非常契合模块化思想,能够建立一个易开发、易理解、易复用的实现结构。
5. 缺点
● 接口的耦合性。虽然面向对象式风格有利于对象修改自己的内部实现,但是其所用的方法调用连接机制使得它无法消除接口的耦合性。
● 标识(identity)的耦合性。除了接口的耦合性之外,方法调用机制带来的还有标识的耦合性,即一个对象要与其他对象交互,就必须知道其他对象的标识。
● 副作用。面向对象式风格借鉴了面向对象的思想,也引入了面向对象的副作用,因此更难实现程序的“正确性”。
例如,如果A和B都使用对象C,那么B对C的修改可能会对A产生未预期的影响。
再例如,对象的重入(reentry) 问题:
如果A的方法f()调用了B的方法p(),而p()又调用了A的另一方法q(),那么就可能使得q()失败,因在q()开始执行时,A正处于f()留下的执行现场,这个现场可能是数据不一致的。
6. 应用
面向对象式风格适用于那些能够基于数据信息分解和组织的软件系统,这些系统主要是标识和保护相关的数据信息;能够将数据信息和相关操作联系起来,进行封装。
实践中,基于抽象数据类型建立的软件系统大多属于面向对象式风格。
三、分层风格
layered style
1. 简介
分层风格根据不同的抽象层次,将系统组织为层次式结构。
每个层次被建立为一个部件,不同部件之间通常用程序调用方式进行连接,因此连接件被建立为程序调用机制。
2. 设计决策与约束
分层风格的重要设计决策与约束有:
● 从最底层到最高层,部件的抽象层次逐渐提升。每个下层为邻接上层提供服务,每个上层将邻接下层作为基础设施使用。也就是说,在程序调用机制中上层调用下层。
● 两个层次之间的连接要遵守特定的交互协议,该交互协议应该是成熟、稳定和标准化的。也就是说,只要遵守交互协议,不同部件实例之间是可以互相替换的。
●跨层次的连接是禁止的
,不允许第I层直接调用I+N(N>1 )层的服务。
●逆向的连接是禁止的
,不允许第I层调用第J (J<I)层的服务。
3. 实现
分层风格通常是不限粒度的模块实现。
每个层次部件可以被实现为一个模块,也可以被实现为包含多个细粒度模块的组合模块,还可能是一个粗粒度的子系统。
所以,每个层次部件可以表示为一个包,包内部含有它的所有实现模块。
模块之间使用导入/导出关系连接。子系统之间使用<<use>>
关系连接,层之间使用依赖关系连接。
因为分层风格的连接要遵守特定的交互协议,所以在实现中要着重完成层之间的交互接口定义,并适当允许层内部实现的多样性。
4. 优点
● 设计机制清晰,易于理解。通过将系统按照不同的抽象层次组织为层次结构,分层风格可以将混杂的耦合逻辑分解为几个不同的部分(例如网络通信协议的分层),每个部分变得更简单、纯粹和易于理解,从而使得整个设计机制非常清晰。
● 支持并行开发。分层风格的不同层次之间遵守成熟、稳定和标准化的交互协议,也就是说一旦层次之间的连接明确下来,就很少会发生改变。而且只要不破坏交互协议,每个层次内部的开发决策不会对其他层次内部的开发决策产生影响。所以分层风格能够支持并行开发,它的每个层次都可以交给一个团队进行独立开发。
● 更好的可复用性与内部可修改性。因为不同层次之间通过成熟、稳定的交互协议通信,因此,只要遵守其交互协议,不同的层次部件就能够互相替换,具有很好的可复用性。在不影响交互协议的情况下,每个层次可以自由安排其内部实现机制,因此分层风格也具有很好的内部可修改性。
5. 缺点
● 交互协议难以修改。虽然分层风格能够很好地实现内部可修改性,但是它难以修改交互协议,这也是它要求交互协议比较成熟和稳定的原因。
因为,一方面,对交互协议的修改意味着层次的对外行为需要变更;
另一方面,不同层次是对同一系统的不同程度的抽象,因此对外行为常常存在于所有层次,只是抽象程度不同而已。
最后,如果交互协议需要改变,那么就可能需要改变所有的层次。
● 性能损失。分层风格禁止跨层调用,这使得每一个外界请求都需要沿着层次逐一深入,多次调用,这可能会产生冗余的调用处理,带来不必要的性能损失。
● 难以确定层次数量和粒度。如果层的粒度太大,就只会有少数几个层次,不能完全发挥分层风格的可复用性和内部可修改性。反之,如果层的粒度太小,层次的数量就会太多,引人不必要的复杂性,带来额外的性能损失。
6. 应用
分层风格适用于具备下列特性的系统:
● 主要功能是能够在不同抽象层次上进行任务分解的复杂处理;
● 能够建立不同抽象层次之间的稳定交互协议;
● 没有很高的实时性要求,能够容忍稍许的延迟。
此外,那些需要进行并行开发的软件系统也可能会使用分层风格,以便于任务分配和工作开展。
在现有的软件系统中,分层风格是一种经常被用到的体系结构风格,像网络通信、交互系统、硬件控制系统、系统平台等都会使用分层风格。例如,ISO网络通信模型、TCP/IP网络通信模型等都使用了分层风格。
四、MVC风格
Model-View-Control
1. 简介
如图9-7所示,模型-视图-控制风格以程序调用为连接件,将系统功能组织为模型、视图和控制三个部件。
模型封装了系统的数据和状态信息,实现业务逻辑,对外提供数据服务和执行业务逻辑。
视图封装了用户交互,提供业务展现,接收用户行为。
控制封装了系统的控制逻辑,根据用户行为调用需要执行的业务逻辑和数据更新,并且根据执行后的系统状态决定后续的业务展现。
2. 设计决策与约束
模型-视图-控制风格的重要设计决策与约束有:
● 模型、视图、控制分别是关于业务逻辑、表现和控制的三种不同内容抽象。
● 如果视图需要持续地显示某个数据的状态,那么它首先需要在模型中注册对该数据的兴趣。如果该数据状态发生了变更,模型会主动通知视图,然后再由视图查询数据的更新情况。
● 视图只能使用模型的数据查询服务,只有控制部件可以调用可能修改模型状态的程序。
● 用户行为虽然由视图发起,但是必须转交给控制部件处理。对接收到的用户行为,控制部件可能会执行两种处理中的一种或两种 :调用模型的服务,执行业务逻辑;提供下一个业务展现。
● 模型部件相对独立,既不依赖于视图,也不依赖于控制。虽然模型与视图之间存在一个“通知变更”的连接,但该连接的交互协议是非常稳定的,可以认为是非常弱的依赖。
3. 实现
模型-视图-控制风格需要为模型、视图和控制的每个部件实例建立模块实现,各模块间存在导入/导出关系,程序调用连接件不需要显式的实现。
4. 优点
● 易开发性。模型、视图、控制分别是关于业务逻辑、表现和控制的三种不同内容抽象,设计机制清晰,易于开发。
● 视图和控制的可修改性。模型封装了系统的业务逻辑,所以是三种类型中最为复杂的系统部件。MVC中模型是相对独立的,所以对视图实现和控制实现的修改不会影响到模型实现。再考虑到业务逻辑通常比业务表现和控制逻辑更加稳定,所以MVC具有一定的
可修改性优势。
● 适宜于网络系统开发的特征。MVC不仅允许视图和控制的可修改性,而且其对业务逻辑、表现和控制的分离使得一个模型可以同时建立并保持多个视图,这非常适用于网络系统开发。
5. 缺点
● 复杂性。MVC将用户任务分解成了表现、控制和模型三个部分,这增加了系统的复杂性,不利于理解任务实现。
● 模型修改困难。视图和控制都要依赖于模型,因此,模型难以修改。
6. 应用
因为有适宜于网络系统开发的特征,所以MVC风格主要用于网络系统的开发。
比较分层风格与MVC风格,如表9-3所示。