哈尔滨工业大学软件构造课程笔记第四章第二节

4.2 面向复用的软件构造技术

1. 设计可重用的类

在OOP中设计可重用的类
▪封装和信息隐藏
▪继承与超越
▪多态、子类型和重载
▪泛型编程

▪行为分型和利斯科夫替代原理(LSP)
▪授权和组成

(1)行为分型和利斯科夫替代原理(LSP)

行为分型
子类型多态:客户端可用统一的方式处理
不同类型的对象

如果对于类型T的对象x,q(x) 成立,那么对于类型T的子类型S的对象y,q(y) 也成立。

Java中编译器强制的规则(静态类型检查)
子类型可以增加方法,但不可删
子类型需要实现抽象类型 (接口、抽象类)中所有未实现的方法
子类型中重写的方法必须有相同或子类型的返回值或者符合co-variant的参数
子类型中重写的
方法必须使用同样类型的参数或者符contra-variant的参数(此种情况Java目前按照重载overload处理)
子类型中重写的方法不能抛出额外的异常

也适用于指定的行为(方法):
更强的不变量
更弱的前置条件
更强的后置条件

利斯科夫替代原理(LSP)
LSP是子类型关系的一个特殊定义,称为
强行为子类型化

在编程语言中,LSP依赖于以下限制:
前置条件不能强化
后置条件不能弱化
不变量要保持
子类型方法参数:逆变
子类型方法的返回值:协变
异常类型:协变

协变
父类型→子类型:越来越具体specific
返回值类型:不变或变得更具体
异常的类型:也是如此。

class T {
     
  Object a() {
     }
}
class S extends T {
     
  @Override 
  String a() {
     } 
}
class T {
     
  void b( ) throws Throwable {
     }
}
class S extends T {
     
  @Override 
  void b( ) throws IOException {
     }
}
class U extends S {
     
  @Override 
  void b( ) {
     }
}

反协变、逆变
父类型→子类型:越来越具体specific
参数类型:要相反的变化,要不变或越来
越抽象

class T {
     
  void c( String s ) {
     }
}
class S extends T {
     
@Override 
  void c( Object s ) {
     } 
}

目前Java中遇到这种情况,当作overload看待

协变和逆变
Java中数组是协变的: 对T[]数组,可以保存类型T及其子类型的数据

泛型中的LSP
泛型是类型不变的
ArrayList是List的子类型
List不是List的子类型

类型参数在编译后被丢弃,在运行时不可用
这个过程称为类型擦除

泛型不是协变的。

可采用通配符实现两个泛型类的协变

** Class类型类**
Java在运行时,为所有对象维护一个运行时类型标识,这个标识跟踪对象所属的类,用来确定选择哪个方法运行。保存这些信息的类叫做“Class类型类”。注意:Class是类的名字,不是关键词class。每个”Class”的对象描述了一个类的信息

什么是类型擦除?
虚拟机中没有泛型类型对象-所有对象都属于普通类
泛型信息只存在于编译阶段,在运行时会被”擦除”
定义泛型类型时,会自动提供一个对应的原始类型(非泛型类型),原始类型的名字就是去掉类型参数后的泛型类型名。
擦除时类型变量会被擦除,替换为限定类型,如果没有限定类型则替换为Object类型。

运行时类型查询只适用于原始类型

泛型的通配符
无界通配符类型是使用通配符(?)指定的,例如List。

有两种情况下,无界通配符是一种有用的方法:
-如果你正在写一个可以使用Object类中提供的功能来实现的方法。
-当代码使用泛型类中不依赖于类型参数的方法时。例如,列表。大小或List.clear。
-事实上,Class之所以经常使用,是因为其中的大多数方法
类不依赖于T。

<? super A> 下限通配符

<? extends A> 上限通配符

(2)授权和组成

Comparator接口
这个接口对实现它的每个类的对象进行总体排序。
▪这种排序被称为类的自然排序,类的比较方法被称为类的自然比较方法。

另一种方法:让你的ADT实现Comparable接口,然后override compareTo() 方法

与使用Comparator的区别:不需要构建新的Comparator类,比较代
码放在ADT内部。

这不再是委托

委托
委派/委托:一个对象请求另一个对象的功能
委派是复用的一种常见形式
Sorter可以使用任意排序顺序重用
Comparators可以在需要比较整数的任意客户端代码中重用

可以将委托描述为在实体之间共享代码和数据的低级机制。
-显式委托:将发送对象传递给接收对象
-隐式委托:通过语言的成员查找规则

委托与继承
继承:通过新操作扩展基类或覆盖操作。
委托:捕捉一个操作并将其发送到另一个对象

很多设计模式将继承和委托结合使用

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

本质上,这种重构将两个类分开,并使超类成为子类的助手,而不是子类的父类。
一个类不需要继承另一个类的全部方法,通过委托机制调用部分方法
从而避免继承大量无用的方法

复合超继承原理
或者称为组合重用原则(CRP)
类应该通过它们的组合(通过包含实现所需功能的其他类的实例)来实现多态行为和代码重用,而不是从基类或父类继承。
-最好是组合一个对象可以做什么(has_a或use_a),而不是扩展它是什么(is_a)。

委托发生在object层面,而继承发生在class层面

▪组合胜于继承的实现通常从创建代表系统必须表现的行为的各种接口开始。
▪根据需要建立和添加实现所确定的接口的类到业务领域类中。
▪因此,系统行为的实现不需要继承。

▪ 使用接口定义系统必须对外展示的不同侧面的行为
▪ 接口之间通过extends实现行为的扩展(接口组合)
▪ 类implements 组合接口
▪ 从而规避了复杂的继承关系

委托的类型
Dependency依赖(A使用B)
Association关联(A有B)
Composition/aggregation合成/聚合(A拥有B)
– 可以认为Composition/Aggregation是Association的两种具体形态

这种分类是根据委托和委托方之间的“耦合程度”进行的

(1) Dependency: 临时性的delegation
使用类的最简单形式是调用它的方法;▪两个类之间的这种关系形式被称为“使用-a”关系,在这种关系中,一个类使用另一个类,而不把它作为一个属性。-例如,可以是一个参数,或者在方法中局部使用。

(2) Association: 永久性的delegation
关联:对象类之间的一种持久关系,允许一个对象实例使另一个对象实例代表它执行操作。

  • has_a:一个类有另一个属性/实例变量通过成员变量
    -这种关系是结构性的,因为它指定一种对象连接到另一种对象,并不代表行为。

(3)组合:更强的协会,但难以变化
▪组合是一种将简单的对象或数据类型组合成更复杂的对象或数据类型的方法。

  • is_part_of:一个类有另一个作为属性/实例变量
    -实现一个对象包含另一个对象

(4) Aggregation: 更弱的association,可动态变化
聚合:对象存在于其他对象之外,是在外部创建的,因此它作为参数传递给识解者。
——has_a

Dependency(A使用一个或多个B)
Association(A有一个或多个B)
Composition/aggregation(A拥有一个或多个B)

都支持1对多的delegation

2. 设计系统级可重用库和框架


库:一组提供可重用功能的类和方法(api)

框架
▪框架:可以定制到应用程序中的可重用框架代码
▪框架调用客户端代码
-好莱坞的原则:“不要给我们打电话。我们会打电话给你。”

哈尔滨工业大学软件构造课程笔记第四章第二节_第1张图片库和框架的实践
▪定义关键抽象及其接口
▪定义对象交互和不变量
▪定义控制流程
▪提供建筑指导
▪提供默认值

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

更多的方面
▪API:应用程序编程接口,库或框架的接口
▪客户端:使用API的代码
▪插件:定制框架的客户端代码
▪扩展点:一个框架支持插件扩展的地方
▪协议:API和客户端之间预期的交互顺序
▪回调:一个插件方法,框架将调用它来访问定制的功能
▪生命周期方法:根据协议和插件状态,按顺序调用的回调方法

(1)API的设计

为什么API设计很重要?
如果你编程,你就是一个API设计者,API可以是你最大的资产之一
-好的代码是模块化的-每个模块都有一个API
-用户投入巨大:获取,写作,学习
-从api的角度考虑可以提高代码质量
-成功的公共api捕获用户

也会成为你最大的负债
-坏的API会导致支持调用流的无终止
-可以抑制前进的能力

公共api是永远的——一个正确的机会
-一旦模块有用户,就不能随意改变API

一个好的API的特征
▪简单易学
▪易于使用,即使没有文件
▪不易误用
▪易于阅读和维护使用它的代码
▪足够强大,以满足要求
▪易于进化
▪适合观众

(2)框架设计

白盒和黑盒框架
▪白盒框架
-通过子类化和覆盖方法扩展
-常用设计模式:模板方法
子类有主方法,但给框架控制
▪黑盒框架
-扩展通过实现一个插件接口
-常见的设计模式:策略,观察者
-插件加载机制加载插件并对框架进行控制

▪白盒框架使用子类化/子类型化——继承
-允许扩展每个非私有方法
-需要了解超类的实现
-每次只接一个分机
——编制在一起
-通常称为开发人员框架
▪黑盒框架使用成分——委派/组合
-允许扩展接口中公开的功能
-只需要了解界面
——多个插件
-通常提供更多的模块化
-可单独部署(jar . dll,…)
-通常称为终端用户框架和平台

哈尔滨工业大学软件构造课程笔记第四章第二节_第2张图片框架设计注意事项
▪一旦设计,几乎没有机会改变
▪关键决策:将普通零件与可变零件分开
-你想解决什么问题?
▪可能出现的问题:
-扩展点太少:仅限于一小部分用户
-扩展点太多:难学、慢
-太泛:重用价值小

典型的框架设计和实现
▪定义你的领域
-识别潜在的通用部件和可变部件
-设计和编写示例插件/应用程序
▪提取和实施共同的部分作为框架
▪为可变部件提供插件接口和回调机制
-在适当的地方使用众所周知的设计原则和模式…
▪获得大量反馈,并不断改进
▪这通常被称为“领域工程”。

进化设计:提取共性
▪抽取界面是进化设计的一个新步骤:
抽象类是从具体类中发现的
-接口是从抽象类中提取的
▪一旦体系结构稳定就开始运行
-从类中移除非公共方法
-将默认实现移动到实现接口的抽象类中

运行一个框架
▪有些框架可以自己运行
Eclipse -如。
▪必须扩展其他框架才能运行

  • Swing, JUnit, MapReduce, servlet
    ▪加载插件的方法:
    -客户端写main(),创建一个插件并传递给框架
    -框架写main(),客户端传递插件名作为命令行参数或环境变量
    -框架寻找一个神奇的位置,然后配置文件或.jar文件被自动加载和处理。

哈尔滨工业大学软件构造课程笔记第四章第二节_第3张图片

(3) Java集合框架

什么是集合和集合框架?
▪集合:收集元素的结构
▪主要用途:数据存储和检索,数据传输
-熟悉的例子:java.util。向量,java.util.Hashtable,数组
▪收集框架:一个统一的体系结构
-接口-实现-独立性
-实现-可重用的数据结构
-算法-可重用的功能
▪最有名的例子

  • c++标准模板库(STL)
  • Java集合框架(JCF)

架构总述
▪核心收集接口
▪通用的实现
▪包装器的实现
▪抽象的实现
▪算法
哈尔滨工业大学软件构造课程笔记第四章第二节_第4张图片哈尔滨工业大学软件构造课程笔记第四章第二节_第5张图片Collection接口

public interface Collection<E> {
     
int size();
boolean isEmpty();
boolean contains(Object element);
boolean add(E element); // Optional
boolean remove(Object element); // Optional
Iterator<E> iterator();
Object[] toArray();
T[] toArray(T a[]);
// Bulk Operations
boolean containsAll(Collection<?> c);
boolean addAll(Collection<? Extends E> c); // Optional
boolean removeAll(Collection<?> c); // Optional
boolean retainAll(Collection<?> c); // Optional
void clear(); // Optional
...
}

Iterator接口
枚举接口的替换
-添加删除方法
-改进方法名公共接口迭代器 {boolean hasNext();
E next ();无效的remove ();/ /可选
}

Set接口
▪不增加收集方法!
增加无重复元素的要求
强制equals和hashCode计算

List接口:对象序列

public interface List<E> extends Collection<E> {
     
E get(int index);
E set(int index, E element); // Optional
void add(int index, E element); // Optional
Object remove(int index); // Optional
boolean addAll(int index, Collection<? extends E> c);
// Optional
int indexOf(Object o);
int lastIndexOf(Object o);
List<E> subList(int from, int to);
ListIterator<E> listIterator();
ListIterator<E> listIterator(int index);
}

Map接口:一个 Key-Value映射

public interface Map<K,V> {
     
int size();
boolean isEmpty();
boolean containsKey(Object key);
boolean containsValue(Object value);
Object get(Object key);
Object put(K key, V value); // Optional
Object remove(Object key); // Optional
void putAll(Map<? Extends K, ? Extends V> t); // Optional
void clear(); // Optional
// Collection Views
public Set<K> keySet();
public Collection<V> values();
public Set<Map.Entry<K,V>> entrySet();
}
// Iterate over all keys in Map m
Map<Key, Val> m;
for (iterator<Key> i = m.keySet().iterator(); i.hasNext(); )
System.out.println(i.next());
// As of Java 5 (2004)
for (Key k : m.keySet())
System.out.println(i.next());
// “Map algebra” 代数操作
Map<Key, Val> a, b;
boolean isSubMap = a.entrySet().containsAll(b.entrySet()); //子集
Set<Key> commonKeys =
new HashSet<>(a.keySet()).retainAll(b.keySet)); //交集
//Remove keys from a that have mappings in b
a.keySet().removeAll(b.keySet()); //差集

通用的实现
一致的命名和行为

选择一个实现
▪Set

  • HashSet——O(1)访问,没有订单保证
    -TreeSet- O(log n)访问,排序
    ▪Map
  • HashMap - TreeMap
  • ▪List
  • ArrayList——O(1) random access, O(n) insert/remove
  • LinkedList——O(n) random access, O(1) insert/remove;
    •用于队列和deques(不再是一个好主意!)

不可变封装器
▪匿名实现
▪静态工厂方法
▪每个核心接口一个
▪提供只读访问—不可变!

同步封装器
▪不是线程安全的!
▪同步包装器:线程安全的新方法
匿名实现,每个核心接口一个
-静态工厂采取适当类型的集合
-确保线程安全,如果所有访问都通过包装器
-必须手动同步迭代
▪同步包装器的方式现在已经过时
-同步包装器基本上是过时的
——已经被并行容器所替代

便利实现
Arrays.asList(E[] a)
允许数组被“查看”为List
-连接到基于集合的api

EMPTY_SET, EMPTY_LIST, EMPTY_MAP
不支持add操作

singleton(E o) 返回只包含指定对象的不可变集

nCopies(int n, T o) 返回由指定对象n个副本构成的不可变List

可重复使用的算法

static <T extends Comparable<? super T>> void sort(List<T> list);
static int binarySearch(List list, Object key);
static <T extends Comparable<? super T>> T min(Collection<T> coll);
static <T extends Comparable<? super T>> T max(Collection<T> coll);
static <E> void fill(List<E> list, E e);
static <E> void copy(List<E> dest, List<? Extends E> src);
static void reverse(List<?> list);
static void shuffle(List<?> list);

兼容性
▪新旧系列可以自由操作
▪向上兼容性
– Vector implements List
– Hashtable implements Map
– Arrays.asList(myArray)
▪向后兼容
– myCollection.toArray()
– new Vector<>(myCollection)
– new Hashtable<>(myMap)

新版本的特性,使其能够与更新或更强大的版本一起工作
允许与旧版本互操作的版本的属性

API设计指南
避免特定的容器类型
•任何收集接口(收集,地图(Collection, Map最好)
•数组有时可能更可取
-输出值类型:
•任何集合接口或类
•数组

为您的遗留集合提供适配器

3.总结

▪设计可重用类
-继承和重写
——重载
-参数多态性和泛型编程
-行为子类型和Liskov代换原理(LSP)
-组成及授权
▪设计系统级可重用库和框架

  • API和库
          • -框架
  • Java集合框架(示例)

你可能感兴趣的:(哈工大,学习笔记,软件构造,设计模式,java,编程语言)