软件构造-构件复用

可复用构件与软件复用的关系

1.可复用构件的定义及特征

构件是指应用系统中可以明确辨识的有机构成成分,它具有相对独立性、互换性和功能性的特征。可复用构件则是指具有相对独立的功能和可复用价值的构件。
可复用构件应具备以下特征:

  • 有用性,必须提供有用的功能;
  • 可用性,必须易于理解和使用;
  • 可靠性 ,构件自身及其变形必须能正确工作;
  • 适应性,应易于通过参数化等方式在不同语境中进行配置;
  • 可移植性,能在不同的硬件平台和软件环境中工作。

随着对软件复用理解的深入,构件的概念已经不再局限于源代码构件,而是延伸到系统和软件的需求规约、系统和软件的构架、文档测试计划、测试案例和数据,以及其他对开发活动有用的信息,这些信息都可以称为可复用软件构件。

2.软件复用的核心是可复用构件

可复用构件技术是支持软件复用的核心技术,是近几年来迅速发展并受到高度重视的一个学科分支。随着软件产业工业化的发展,也许未来会出现这样一种产业模式:构件生产商专门生产构件,然后再由零售商等构件管理者将它们拿到市场上销售。系统集成商则根据自己应用系统的需求进行构件的采购、集成和组装。

可复用构件的主要技术流派

目前,软件构件技术流派主要有三种:

1.COM

COM构件实现规范由微软提出,它使开发人员可以利用其中的通信机制,组装不同开发商的构件。COM的核心是一组应用程序调用接口,该接口提供了创建构件组建构件的功能。为支持网络环境,微软对COM进行了扩充,这就是DCOM。COM规范具有以下特点:

  • 构件间的互操作基于指针进行,依赖于操作系统的API;
  • 对 Windows的依赖性强,对其他操作系统支持相对不足;
  • 构件运行环境的提供者仅限于微软,但支持COM规范的开发工具较多,如VC+ +、VB和Builder等。

2.JavaBean

JavaBean构件实现规范是由SUN公司在Java语言的基础上提出的。由于Java是一种纯对象式语言,因此JavaBean构件规范比较完备、简洁。JavaHBean规范具有以下特点:

  • 构件模型比较完备;
  • 仅支持Java语言;
  • 构件运行环境有SUN支持,其他厂商也可提供运行环境。支持该模型的开发工具较多,如Visual Cafe、Visual age for Java等。

3.CORBA

CORBA实现规范由OMG提出。OMG首先发布了OMA(对象管理体系结构) ,提出了构件互操作的软总线——ORB (Object Request Broker) ,并将构件分为三类:公共对象服务COS、面向领域的公共设施CF和应用对象AF。CORBA规范具有以下特点:

  • 构件间的复用以 ORB为中介,对处理机、操作系统、语言的异构性支持性强;
  • 充分借鉴JavaBean的构件原型;
  • 提供构件运行环境的厂商较多,对开发支持的工具相对较少,通常需要传统开发工具的支持等。

设计可复用的类

1.子类型多态和利斯科夫替换原则

子类型多态:客户端可用统一的方式处理不同类型的对象,例如:

Animal a = new Animal(); 
Animal c1 = new Cat(); 
Cat c2 = new Cat();

在可以使用a的场景,都可以用c1和c2代替而不会有任何问题(但反过来不行,即不能用父类来实例化一个子类引用)

  1. java静态类型检查中的规则:
    1. 子类型可以增加方法,但不可删父类原有的方法
    2. 子类型需要实现抽象类型中的所有未实现方法
    3. 子类型中重写的方法必须有相同或子类型的返回值
    4. 子类型中重写的方法必须使用同样类型的参数(或该类型的父类型,但java中不支持,会自动默认为重载)
    5. 子类型中重写的方法不能抛出额外的异常
  2. 规约上的准则:
    1. 更强的不变量
    2. 更弱的前置条件
    3. 更强的后置条件
  3. 里氏替换原则的主要作用就是规范继承时子类的一些书写规则。其主要目的就是保持父类方法不被覆盖。
    含义:
    子类必须完全实现父类的方法
    子类可以有自己的个性
    覆盖或实现父类的方法时输入参数可以被放大
    覆盖或实现父类的方法时输出结果可以被缩小
    利斯科夫替换原则本质是强行为子类型化,其要求为:
    1. 前置条件不能强化
    2. 后置条件不能弱化
    3. 不变量要保持
    4. 子类型方法参数:逆变
    5. 子类型方法的返回值:协变
    6. 异常类型:协变
  4. 协变:子类型方法的返回值是其父类型方法的返回值的子类型,即子类型及其方法的返回值在各自的继承树上的相对位置是协同的
  5. 逆变:子类型参数的类型是其父类型方法的返回值的父类型,即子类型及其方法的参数在各自的继承树上的相对位置是相反的
  • 逆变与协变综述:如果A、B表示类型,f(⋅)表示类型转换,≤表示继承关系(比如,A≤B表示A是由B派生出来的子类):
    f(⋅)是逆变(contravariant)的,当A≤B时有f(B)≤f(A)成立;
    f(⋅)是协变(covariant)的,当A≤B时有f(A)≤f(B)成立;
    f(⋅)是不变(invariant)的,当A≤B时上述两个式子均不成立,即f(A)与f(B)相互之间没有继承关系。
  • 协变(Co-variance):
    父类型->子类型:越来越具体(specific)。
    在LSP中,返回值和异常的类型:不变或变得更具体 。
    例子:
    在这里插入图片描述
  • 逆变(Contra-variance):
    父类型->子类型:越来越抽象。
    参数类型:要相反的变化,不变或越来越抽象。
    例子:
    在这里插入图片描述
    但这在Java中是不允许的,因为它会使重载规则复杂化。
    总结:
    在这里插入图片描述
    (1.子类型(属性、方法)关系;2.不变性,重写方法;3.协变,方法返回值变具体;4.逆变,方法参数变抽象;5.协变,参数变的更具体,协变不安全)
  1. java遇到逆变时,当作overload(重载)处理
  2. 实例:数组类型
    数组是协变的:一个数组T[ ] ,可能包含了T类型的实例或者T的任何子类型的实例
    即子类型的数组可以赋予父类型的数组进行使用,但数组的类型实际为子类型。
    下面报错的原因是myNumber指向的还是一个Integer[] 而不是Number[]
Number[] numbers = new Number[2]; 
numbers[0] = new Integer(10); 
numbers[1] = new Double(3.14);

Integer[] myInts = {1,2,3,4}; 
Number[] myNumber = myInts;

myNumber[0] = 3.14; //run-time error!
  1. 泛型中的LSP原则
Java中泛型是不变的,但可以通过通配符"?"实现协变和逆变:
<? extends>实现了泛型的协变:
List<? extends Number> list = new ArrayList<Integer>();
<? super>实现了泛型的逆变:
List<? super Number> list = new ArrayList<Object>();
由于泛型的协变只能规定类的上界,逆变只能规定下界,使用时需要遵循PECS(producer--extends, consumer-super):
要从泛型类取数据时,用extends;
要往泛型类写数据时,用super;
既要取又要写,就不用通配符(即extends与super都不用)。

泛型声明时存在类型擦除,即尖括号里面的内容只在编译时被考虑,运行时不考虑
实例:

		ArrayList<String> 是List<String>的子类型 
		List<String>不是List<Object>的子类型

简单来说,尖括号里面的类型和其它尖括号里面的类型即使有直接继承关系,但对于整个泛型是没有继承关系的
可以用通配符来解决泛型继承的问题<?>,<? extends AClass>,<? super BClass>
在这里插入图片描述

2.委托和组合

2.1 Java排序实例

如果ADT需要比较大小,或者要放入Collections或Arrays进行排序,可实现Comparator接口并override compare()函数。
在这里插入图片描述
另一种方法:让ADT实现Comparable接口,然后overridecompareTo() 方法。与使用Comparator的区别:不需要构建新的Comparator类,比较代码放在ADT内部。
在这里插入图片描述

2.2 委托(Delegation)

委派/委托:一个对象请求另一个对象的功能,是复用的一种常见形式。
显性委派:将发送对象传递给接收对象;
隐性委派:由语言的成员查找规则。
委托可以被描述为在实体之间共享代码和数据的低级机制
在这里插入图片描述
委派设计模式:是一种用来实现委派的软件设计模式;
委托依赖于动态绑定,因为它要求给定的方法调用可以在运行时调用不同的代码段。
委派的过程如下:
在这里插入图片描述
Receiver对象将操作委托给Delegate对象,同时Receiver对象确保客户端不会滥用委托对象;

委托和继承的比较:

  • 继承:通过新操作扩展基类或覆盖操作。
  • 委托:把基类拿过来作为新的类的元素,可以用基类以实现的功能扩展新类的功能
  • 委托可以替代继承的情况:如果子类只需要复用父类中的一小部分方法,可以不需要使用继承,而是通过委派机制来实现
2.3 组合继承原则

类应该通过它们的组合实现多态行为和代码重用(通过包含实现所需功能的其他类的实例),而不是从基类或父类继承。
组合原则:

  • 使用接口定义系统必须对外展示的不同侧面的行为,例如,一只鸟可以叫也可以飞,那么可以定义两个接口,quackable和flyable。
  • 接口之间通过extends实现行为的扩展(接口组合)。然后可以定义一个接口birdable同时继承了上述两个接口,这样这个新的接口就有了上述两个接口的全部功能。
  • 类implements 组合接口,从而规避了复杂的继承关系。接下来,我们定义的“鸟类”就可以实现birdable,使得活动顶层接口的功能(在构造实例的过程中,同时要delegation顶层功能的接口)
2.3.1 Dependency: 临时性的delegation

Dependency:对象需要其他对象(供应商)实现的临时关系。
本质上就是通过方法的参数列表把被委托方传进来,或者通过在方法中定义局部变量来产生委托方。临时要用被委托方的方法,在被委托方中的field中并不保存。
实例:

class Course { }

class CourseSchedule {
	List<Course> courses;
	void ass (Course c) {...}
	void remove (Course c) {...}
} 
2.3.2 Association: 永久性的delegation

Association:对象类之间的持久关系,允许一个对象实例使另一个对象实例代表它执行操作。
被委托方常为委托方的属性
实例:

class Teacher {
	private Student [] students;
}
class Student {
	private Teacher teacher;
}

class Course {}
class Student {
	Course[] selectedCourses;
}
2.3.3 Composition: 更强的delegation

Composition是一种将简单对象或数据类型组合成更复杂的对象的方法。
它的特点是,被委托方初始化是在委托方的内部
实例:

class Heart {}

class Person {
	private Heart heart = new Heart();
	public void operation () {
		heart.operation();
	}
}
2.3.4 Aggregation

对象存在于另一个之外,在外部创建,它作为参数传递给construtor
它的特点是,当委托方的对象被删除后,被委托方的实例仍然存在
实例:

class Student {}

class Course {
	private Student[] students;
	public addStudent (Student s) {
		students.append(s);
	}
}

设计可复用的库与框架

之所以library和framework被称为系统层面的复用,是因为它们不仅定义了1个可复用的接口/类,而是将某个完整系统中的所有可复用的接口/类都实现出来,并且定义了这些类之间的交互关系、调用关系,从而形成了系统整体 的“架构”。

相应术语:
API(Application Programming Interface):库或框架的接口
Client(客户端):使用API的代码
Plugin(插件):客户端定制框架的代码
Extension Point:框架内预留的“空白”,开发者开发出符合接口要求的代码( 即plugin) , 框架可调用,从而相当于开发者扩展了框架的功能
Protocol(协议):API与客户端之间预期的交互序列。
Callback(反馈):框架将调用的插件方法来访问定制的功能。
Lifecycle method:根据协议和插件的状态,按顺序调用的回调方法。

API和库

API是程序员最重要的资产和“荣耀”,吸引外部用户,提高声誉。
建议:始终以开发API的标准面对任何开发任务;面向“复用”编程而不是面向“应用”编程。
难度:要有足够良好的设计,一旦发布就无法再自由改变。
编写一个API需要考虑以下方面:
API应该做一件事,且做得很好
API应该尽可能小,但不能太小
Implementation不应该影响API
记录文档很重要
考虑性能后果
API必须与平台和平共存
类的设计:尽量减少可变性,遵循LSP原则
方法的设计:不要让客户做任何模块可以做的事情,及时报错

框架

框架(Framework)是整个或部分系统的可重用设计,表现为一组抽象构件及构件实例间交互的方法;另一种定义认为,框架是可被应用开发者定制的应用骨架。前者是从应用方面而后者是从目的方面给出的定义。
为了增加代码的复用性,可以使用委派和继承机制。同时,在使用这两种机制增加代码复用的过程中,我们也相应地在不同的类之间增加了关系(委派或继承关系)。而对于一个项目而言,各个不同类之间的依赖关系就可以看做为一个框架。一个大规模的项目可能由许多不同的框架组合而成。
框架与设计模式:
框架、设计模式这两个概念总容易被混淆,其实它们之间还是有区别的。构件通常是代码重用,而设计模式是设计重用,框架则介于两者之间,部分代码重用,部分设计重用,有时分析也可重用。在软件生产中有三种级别的重用:内部重用,即在同一应用中能公共使用的抽象块;代码重用,即将通用模块组合成库或工具集,以便在多个应用和领域都能使用;应用框架的重用,即为专用领域提供通用的或现成的基础结构,以获得最高级别的重用性。
框架与设计模式虽然相似,但却有着根本的不同。设计模式是对在某种环境中反复出现的问题以及解决该问题的方案的描述,它比框架更抽象;框架可以用代码表示,也能直接执行或复用,而对模式而言只有实例才能用代码表示;设计模式是比框架更小的元素,一个框架中往往含有一个或多个设计模式,框架总是针对某一特定应用领域,但同一模式却可适用于各种应用。可以说,框架是软件,而设计模式是软件的知识。
框架分为白盒框架和黑盒框架。
白盒框架:
白盒框架是基于面向对象的继承机制。之所以说是白盒框架,是因为在这种框架中,父类的方法对子类而言是可见的。子类可以通过继承或重写父类的方法来实现更具体的方法。
虽然层次结构比较清晰,但是这种方式也有其局限性,父类中的方法子类一定拥有,要么继承,要么重写,不可能存在子类中不存在的方法而在父类中存在。
软件构造课程中有关白盒框架的例子:

public abstract class PrintOnScreen {
    public void print() { 
        JFrame frame = new JFrame(); 
        JOptionPane.showMessageDialog(frame, textToShow());
        frame.dispose();
    } 
    protected abstract String textToShow(); 
}
public class MyApplication extends PrintOnScreen {
@Override protected String textToShow() {
        return "printing this text on " + "screen using PrintOnScreen " + "white Box Framework"; 
    }
}

通过子类化和重写方法进行扩展(使用继承);
通用设计模式:模板方法;
子类具有主要方法但对框架进行控制。
允许扩展每一个非私有方法
需要理解父类的实现
一次只进行一次扩展
通常被认为是开发者框架

黑盒框架:
黑盒框架时基于委派的组合方式,是不同对象之间的组合。之所以是黑盒,是因为不用去管对象中的方法是如何实现的,只需关心对象上拥有的方法。
这种方式较白盒框架更为灵活,因为可以在运行时动态地传入不同对象,实现不同对象间的动态组合;而继承机制在静态编译时就已经确定好。
黑盒框架与白盒框架之间可以相互转换,具体例子可以看一下,软件构造课程中有关黑盒框架的例子,更改上面的白盒框架为黑盒框架:

public interface TextToShow { 
    String text(); 
}

public class MyTextToShow implements TextToShow {
    @Override 
    public String text() { 
        return "Printing"; 
    }
}

public final class PrintOnScreen {
    TextToShow textToShow;   
    public PrintOnScreen(TextToShow tx) { 
        this.textToShow = tx; 
    }
    public void print() { 
        JFrame frame = new JFrame(); 
        JOptionPane.showMessageDialog(frame, textToShow.text());
        frame.dispose(); 
    }
} 

通过实现插件接口进行扩展(使用组合/委派);
常用设计模式:Strategy, Observer ;
插件加载机制加载插件并对框架进行控制。
允许在接口中对public方法扩展
只需要理解接口
通常提供更多的模块
通常被认为是终端用户框架,平台

结束语

采用基于软件构件的软件复用技术,将使软件设计、生产工厂化成为可能,是未来软件开发的发展方向。软件开发的可复用构件技术必将极大地提高软件开发的劳动生产率,縮短应用软件的开发周期,不仅软件质量会更好,软件的维护也会更加容易。因此,该技术一定会有良好的发展前景。

参考:

基于可复用构件的软件复用技术
面向复用的软件构造技术
设计可复用的软件


版权声明:本文为qq_42211249原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。