软件构造系列学习笔记(5.2)————设计可复用软件

设计可复用软件

5-1节学习了可复用的层次、形态、表现;本节从类、API、框架三个层面学习如何设计可复用软件实体的具体技术。

目录

  • 设计可复用的类
  • 设计可复用库与框架

设计可复用类

上节复用类和接口介绍了继承与重写,重载,参数多态与泛型编程等方法。

行为子类型与Liskov替换原则

子类型多态:客户端可用统一的方式处理不同类型的对象 。来看下面这个例子

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

在可以使用a的场景,都可以用c1和c2代替而不会有任何问题。

a = c1; 
a = c2;

Let q(x) be a property provable about objects x of type T, then q(y) should be provable for objects y of type S where S is a subtype of T. ——Barbara Liskov

Java中编译器执行的规则(静态类型检查):

  • 子类型可以增加方法,但不可删
  • 子类型需要实现抽象类型中的所有未实现方法
  • 子类型中重写的方法必须有相同或子类型的返回值
  • 子类型中重写的方法必须使用同样类型的参数
  • 子类型中重写的方法不能抛出额外的异常

Liskov Substitution Principle (LSP)也适用于指定的行为(methods):

  • 更强的不变量
  • 更弱的前置条件
  • 更强的后置条件

Example 1 for Behavioral subtyping (LSP)
子类实现相同的不变量(同时附加了一个)
重写方法有相同的前置条件和后置条件
这里写图片描述

Example 2 for Behavioral subtyping (LSP)
子类实现相同的不变量(同时附加了一个)
重写方法start有更弱的前置条件
重写方法brake有更强的后置条件
这里写图片描述

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

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

Covariance (协变):
父类型->子类型:越来越具体specific 。
返回值类型:不变或变得更具体 。
异常的类型:也是如此。
这里写图片描述

Contravariance (反协变、逆变):
父类型->子类型:越来越具体specific 。
参数类型:要相反的变化,要不变或越来越抽象。
这里写图片描述

这在Java中是不允许的,因为它会使重载规则复杂化。

这里写图片描述

数组是协变的:

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:
泛型是类型不变的。举例来说
ArrayList<String>List<String>的子类型
List<String>不是List<Object>的子类型

编译完成后,编译器会丢弃类型参数的类型信息; 因此这种类型的信息在运行时不可用。这个过程称为类型擦除( type erasure),因此泛型不是协变的。

类型擦除:如果类型参数是无界的,则将泛型类型中的所有类型参数替换为它们的边界或对象。 因此,生成的字节码只包含普通的类,接口和方法。
这里写图片描述
举例来说:

List<Integer> myInts = new ArrayList<Integer>(); 
myInts.add(1); 
myInts.add(2); 
List<Number> myNums = myInts; 
myNums.add(3.14); //compiler error
static long sum(List<Number> numbers) { 
    long summation = 0; 
    for(Number number : numbers) { 
        summation += number.longValue(); 
    } 
    return summation; 
}
List<Integer> myInts = asList(1,2,3,4,5); 
List<Long> myLongs = asList(1L, 2L, 3L, 4L, 5L); 
List<Double> myDoubles = asList(1.0, 2.0, 3.0, 4.0, 5.0); 
sum(myInts); 
sum(myLongs); 
sum(myDoubles); //compiler error

我们不能将integers列表视为numbers列表的子类型。
这对于类型系统来说是不安全的,编译器会立即拒绝它。

这里写图片描述

泛型中的通配符
无界通配符类型使用通配符()指定,例如List <?>,这被称为未知类型的列表。
在两种情况下,无界通配符是一种有用的方法:

  • 如果您正在编写可使用Object类中提供的功能实现的方法。
  • 当代码使用泛型类中不依赖于类型参数的方法时。 例如,List.sizeList.clear。 事实上,Class <?>经常被使用,因为Class <T>中的大多数方法不依赖于T

来看下面的一个例子:

public static void printList(List<Object> list) { 
    for (Object elem : list) 
        System.out.println(elem + " "); 
    System.out.println(); 
} 

printList的目标是打印任何类型的列表,但它无法实现该目标 ,它仅打印Object实例列表; 它不能打印List <Integer>List <String>List <Double>等,因为它们不是List <Object>的子类型。
要编写通用的printList方法,请使用List <?>

public static void printList(List<?> list) { 
    for (Object elem: list) 
System.out.println(); 
} 
List<Integer> li = Arrays.asList(1, 2, 3); 
List<String>  ls = Arrays.asList("one", "two", "three"); 
printList(li); 
printList(ls);

这里写图片描述

委托与组合(Delegation and Composition)

在开始讲委托之前,首先看一个排序的例子。
这里写图片描述

如果你的ADT需要比较大小,或者要放入CollectionsArrays进行排序,可实现Comparator接口并override compare()函数。下面为具体例子
这里写图片描述

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

委派/委托:一个对象请求另一个对象的功能 。例如上面排序的例子,Sorter委派了Comparator的功能。委派是复用的一种常见形式。分为显性委派:将发送对象传递给接收对象;以及隐性委派:由语言的成员查找规则。下面是一个简单的栗子。

这里写图片描述

委托依赖于动态绑定,因为它要求给定的方法调用可以在运行时调用不同的代码段。它的过程如下简图:
这里写图片描述

Receiver对象将操作委托给Delegate对象,同时Receiver对象确保客户端不会滥用委托对象。

委派 vs. 继承
继承:通过新操作扩展基类或覆盖操作。
委托:捕获操作并将其发送给另一个对象。
许多设计模式使用继承和委派的组合。
这里写图片描述

如果子类只需要复用父类中的一小部分方法,可以不需要使用继承,而是通过委派机制来实现。一个类不需要继承另一个类的全部方法,通过委托机制调用部分方法。

这里写图片描述

复合继承原则
或称为复合复用原则(CRP):类应该通过其组合(通过包含实现所需功能的其他类的实例)实现多态行为和代码重用,而不是从基类或父类继承。
“委托” 发生在objet层面,而“继承”发生在class层面。来看一个具体的CRP例子:

Employee类具有计算员工年度奖金的方法:

class Employee { 
    Money computeBonus() {... // default computation} 
    ... 
}

Employee的不同子类:Manager, Programmer, Secretary等可能希望重写此方法以反映某些类型的员工比其他员工获得更慷慨的奖金这一事实:

class Manager extends Employee { 
    @Override Money computeBonus() {... // special computation} 
    ... 
}

这个解决方案有几个问题。 所有Manager对象获得相同的奖金。 如果我们想改变管理者之间的奖金计算怎么办?引入Manager的特殊子类?

class SeniorManager extends Manager { 
    @Override Money computeBonus() {... // more special computation} 
    ... 
}

如果我们想改变特定员工的奖金计算会怎样? 例如,如果我们想要将史密斯从Manager推广到SeniorManager,该怎么办?
如果我们决定让所有Manager获得与Programmer相同的奖金呢? 我们是否应该将Programmer中的计算算法复制并粘贴到Manager中?

核心问题:每个Employee对象的奖金计算方法都不同,在object层面而非class层面。
一个基于CRP的解决办法:
这里写图片描述

其他的类也可以如此解决。
更一般的设计:
这里写图片描述

使用接口定义不同侧面的行为;接口之间通过extends实现 行为的扩展(接口组合);类implements 组合接口。

委派的类型
(1)Dependency: 临时性的delegation
使用类的最简单形式是调用它的方法。这两种类之间的关系形式被称为“uses-a”关系,其中一个类别使用另一个类别而不实际地将其作为属性。 例如,它可能是一个参数或在方法中本地使用。
这里写图片描述

(2) Association: 永久性的delegation
一个对象类之间的持久关系,它允许一个对象实例引起另一个对象执行一个动作。两个类之间的关系为“has-a”关系,一个类有另一个作为属性/实例变量。
这里写图片描述

(3) Composition: 更强的delegation
组合是一种将简单对象或数据类型组合成更复杂的对象的方法。两个类之间的关系为“is_part_of”关系,一个类有另一个作为属性/实例变量。
这里写图片描述

(4) Aggregation
聚合:对象存在于另一个之外,在外部创建,所以它作为参数传递给构造者。两个类的关系也是“has-a”关系。
这里写图片描述

组合与聚合
在组合中,当拥有的对象被破坏时,被包含的对象也被破坏。在聚合中,这不一定是真的。以生活中的事物为例:大学拥有多个部门,每个部门都有一批教授。 如果大学关闭,部门将不复存在,但这些部门的教授将继续存在。 一位教授可以在一个以上的部门工作,但一个部门不能成为多个大学的一部分。大学与部门之间的关系即为组合,而部分与教授之间的关系为聚合。

设计可复用库与框架

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

API和库

API是程序员最重要的资产和“荣耀”,吸引外部用户,提高声誉。
建议:始终以开发API的标准面对任何开发任务;面向“复用”编程而不是面向“应用”编程。
难度:要有足够良好的设计,一旦发布就无法再自由改变。
编写一个API需要考虑以下方面:

  • API应该做一件事,且做得很好
  • API应该尽可能小,但不能太小
  • Implementation不应该影响API
  • 记录文档很重要
  • 考虑性能后果
  • API必须与平台和平共存
  • 类的设计:尽量减少可变性,遵循LSP原则
  • 方法的设计:不要让客户做任何模块可以做的事情,及时报错

框架

框架分为白盒框架和黑盒框架。
白盒框架:
通过子类化和重写方法进行扩展(使用继承);
通用设计模式:模板方法;
子类具有主要方法但对框架进行控制。

黑盒框架:
通过实现插件接口进行扩展(使用组合/委派);
常用设计模式:Strategy, Observer ;
插件加载机制加载插件并对框架进行控制。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值