从类、 API、框架三个层面学习如何设计可复用软件实体的具体技术

前言:未来最有可能产生显著效益的复用是对软件生命周期中一些主要开发阶段的软件制品的复用按抽象程度的高低,可以划分为如下的复用级别:

  • 代码的复用
  • 设计的复用
  • 分析的复用
  • 测试信息的复用

代码的复用

包括目标代码和源代码的复用。其中目标代码的复用级别最低,历史也最久,当前大部分编程语言的运行支持系统都提供了连接(Link)、绑定(Binding)等功能来支持这种复用。

设计的复用

设计结果比源程序的抽象级别更高,因此它的复用受实现环境的影响较少,从而使可复用构件被复用的机会更多,并且所需的修改更少。这种复用有三种途径

  1. 第一种途径是从现有系统的设计结果中提取一些可复用的设计构件,并把这些构件应用于新系统的设计;
  2. 第二种途径是把一个现有系统的全部设计文档在新的软硬件平台上重新实现,也就是把一个设计运用于多个具体的实现;
  3. 第三种途径是独立于任何具体的应用,有计划地开发一些可复用的设计构件。

分析的复用

这是比设计结果更高级别的复用,可复用的分析构件是针对问题域的某些事物或某些问题的抽象程度更高的解法,受设计技术及实现条件的影响很少,所以可复用的机会更大。
复用的途径也有三种,即

  1. 从现有系统的分析结果中提取可复用构件用于新系统的分析;
  2. 用一份完整的分析文档作输入产生针对不同软硬件平台和其它实现条件的多项设计;
  3. 独立于具体应用,专门开发一些可复用的分析构件。

测试信息的复用

主要包括测试用例的复用和测试过程信息的复用。前者是把一个软件的测试用例在新的软件测试中使用,或者在软件作出修改时在新的一轮测试中 使用。后者是在测试过程中通过软件工具自动地记录测试的过程信息,包括测试员的每一个操作、输入参数、测试用例及运行环境等一切信息。这种复用的级别,不便和分析、设计、编程的复用级别作准确的比较,因为被复用的不是同一事物的不同抽象层次,而是另一种信息,但从这些信息的形态看,大体处于与程序代码相当的级别。

设计可复用的类——LSP

  • 在OOP之中设计可复用的类
  • 封装和信息隐藏
  • 继承和重写
  • 多态、子类和重载
  • 泛型编程
  • LSP原则
  • 委派和组合(Composition)

行为子结构

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

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

在可以使用a的场景,都可以用c1和c2代替而不会有任何问题。
在java的静态类型检查之中,编译器强调了几条规则:

  • 子类型可以增加方法,但不可删
  • 子类型需要实现抽象类型中的所有未实现方法
  • 子类型中重写的方法必须有相同或子类型的返回值
  • 子类型中重写的方法必须使用同样类型的参数
  • 子类型中重写的方法不能抛出额外的异常
    行为子结构也适用于指定的方法:
  • 更强的不变量
  • 更弱的前置条件
  • 更强的后置条件
    行为子结构示例:
    示例一:
    在这里插入图片描述
    在上述实例中,子类满足相同的不变量(同时附加了一个重写的方法有相同的前置条件和后置条件,故该结构满足LSP 。
    示例二:
    在这里插入图片描述
    子类满足相同的不变量(同时附加了一个)重写的方法 start 的前置条件更弱重写的方法 brake 的后置条件更强,故该结构满足LSP
    示例三:
    在这里插入图片描述
    子类满足不变量条件更强,故满足LSP。

协变与逆变

综述:如果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):

  1. 父类型->子类型:越来越具体(specific)。
  2. 在LSP中,返回值和异常的类型:不变或变得更具体 。
    在这里插入图片描述
    逆变(Contra-variance):
  3. 父类型->子类型:越来越抽象。
  4. 参数类型:要相反的变化,不变或越来越抽象。

在这里插入图片描述

Liskov替换原则(LSP)

里氏替换原则的主要作用就是规范继承时子类的一些书写规则。其主要目的就是保持父类方法不被覆盖。
含义如下:

  1. 子类必须完全实现父类的方法
  2. 子类可以有自己的个性
  3. 覆盖或实现父类的方法时输入参数可以被放大
  4. 覆盖或实现父类的方法时输出结果可以被缩小

LSP是子类型关系的一个特殊定义,称为(强)行为子类型化。在编程语言中,LSP依赖于以下限制

  • 前置条件不能强化
  • 后置条件不能弱化
  • 不变量要保持或增强
  • 子类型方法参数:逆变
  • 子类型方法的返回值:协变
  • 异常类型:协变

各种应用中的LSP

【数组是协变的】

  • 数组是协变的:一个数组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!

【泛型中的LSP】
Java中泛型是不变的,但可以通过通配符"?"实现协变和逆变:

  • <? extends>实现了泛型的协变: List<? extends Number> list = new ArrayList();
  • <? super>实现了泛型的逆变: List<? super Number> list = new ArrayList();
  • 由于泛型的协变只能规定类的上界,逆变只能规定下界,使用时需要遵循PECS(producer–extends, consumer-super): 要从泛型类取数据时,用extends; 要往泛型类写数据时,用super; 既要取又要写,就不用通配符(即extends与super都不用)。
  • 泛型是类型不变的(泛型不是协变的)。
  • 举例来说 : ArrayList 是List的子类型 List不是List的子类型
    在代码的编译完成之后,泛型的类型信息就会被编译器擦除。因此,这些类型信息并不能在运行阶段时被获得。这一过程称之为类型擦除(type erasure)。
    类型擦除的详细定义:如果类型参数没有限制,则用它们的边界或Object来替换泛型类型中的所有类型参数。因此,产生的字节码只包含普通的类、接口和方法。
    类型擦除的结果: 被擦除 T变成了Object

为了解决类型擦除的问题-----Wildcards(通配符)

无界通配符类型使用通配符(?)指定,例如List <?>,这被称为未知类型的列表。
在两种情况下,无界通配符是一种有用的方法:
如果您正在编写可使用Object类中提供的功能实现的方法。
当代码使用泛型类中不依赖于类型参数的方法时。 例如,List.size或List.clear。 事实上,Class <?>经常被使用,因为Class 中的大多数方法不依赖于T。

设计可复用的类——委派与组合

static void sort(int[] list, boolean ascending) {
...
boolean mustSwap;
if (ascending) {
mustSwap = list[i] < list[j];
} else {
mustSwap = list[i] > list[j];
}
}

 ...
interface Comparator {
boolean compare(int i, int j);
final Comparator ASCENDING = (i, j) -》i < j;
final Comparator DESCENDING = (1, j) -> i > j;
static void sort(int[] list, Comparator cmp) {
boolean mustSwap =
cmp. compare(list[i], 1ist[j]);
}

引入 int compare(T o1 , T o2):用于比较两个变量的大小。
如果你的ADT需要比较大小,或者要放入Collections或Arrays进行排序,可实现Comparator接口并override compare()函数。下面为具体例子:

在这里插入图片描述
另一种方法:让你的ADT实现Comparable接口,然后override compareTo() 方法。与使用Comparator的区别:不需要构建新的Comparator类,比较代码放在ADT内部。下面为具体例子:

public class Edge implements Comparable<Edge> {
Vertex s, t;
double weight;
public int compareTo(Edge 。) {
if(this. getWeight() > o. getweight())
return 1;
else if (.. == ..) return 0;
else return -1;
}
}

设计可复用库与框架

之所以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方法扩展
只需要理解接口
通常提供更多的模块
通常被认为是终端用户框架,平台

相关推荐
©️2020 CSDN 皮肤主题: 1024 设计师:白松林 返回首页