软件构造第五章第二节 设计可复用的软件

Posted hithongming

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了软件构造第五章第二节 设计可复用的软件相关的知识,希望对你有一定的参考价值。

 

第五章第二节  设计可复用的软件

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

Outline

  • 设计可复用的类——LSP
    • 行为子结构
    • Liskov替换原则(LSP)
  • 各种应用中的LSP
    • 数组是协变的
    • 泛型中的LSP
    • 为了解决类型擦除的问题-----Wildcards(通配符)
  • 设计可复用的类——委派与组合
  • 设计可复用库与框架

Notes

## 设计可复用的类——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

 

 【Liskov替换原则(LSP)更多参考:LSP的笔记

  • 里氏替换原则的主要作用就是规范继承时子类的一些书写规则。其主要目的就是保持父类方法不被覆盖。
  • LSP是子类型关系的一个特殊定义,称为(强)行为子类型化。在编程语言中,LSP依赖于以下限制:
    • 前置条件不能强化
    • 后置条件不能弱化
    • 不变量要保持或增强
    • 子类型方法参数:逆变
    • 子类型方法的返回值:协变
    • 异常类型:协变
  • 协变(Co-variance):
    • 父类型->子类型:越来越具体(specific)。
    • 在LSP中,返回值和异常的类型:不变或变得更具体 。
    • 栗子:技术分享图片
  • 逆变(Contra-variance):
    • 父类型->子类型:越来越具体specific 。
    • 参数类型:要相反的变化,不变或越来越抽象。 
    • 栗子:技术分享图片
    • 但这在Java中是不允许的,因为它会使重载规则复杂化。

 总结:

技术分享图片

(1.子类型(属性、方法)关系;2.不变性,重写方法;3.协变,方法返回值变具体;4.逆变,方法参数变抽象;5.协变,参数变的更具体,协变不安全)

 

## 各种应用中的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】

  • 泛型是类型不变的(泛型不是协变的)。举例来说 
    • ArrayList<String> 是List<String>的子类型 
    • List<String>不是List<Object>的子类型
  • 在代码的编译完成之后,泛型的类型信息就会被编译器擦除。因此,这些类型信息并不能在运行阶段时被获得。这一过程称之为类型擦除(type erasure)。
  • 类型擦除的详细定义:如果类型参数没有限制,则用它们的边界或Object来替换泛型类型中的所有类型参数。因此,产生的字节码只包含普通的类、接口和方法。
  • 类型擦除的结果: <T>被擦除 T变成了Object

     技术分享图片

  • Integer是number的子类型,但Box<Integer>也不是Box<Number>的子类型 
  • 这对于类型系统来说是不安全的,编译器会立即拒绝它。

技术分享图片

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

  • 无界通配符类型使用通配符()指定,例如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 <?>

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

技术分享图片

 

 

 

 

 

 

 

## 设计可复用库与框架

  之所以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原则
    • 方法的设计:不要让客户做任何模块可以做的事情,及时报错

【框架】

框架分为白盒框架和黑盒框架。 

  • 白盒框架: 
    • 通过子类化和重写方法进行扩展(使用继承); 
    • 通用设计模式:模板方法; 
    • 子类具有主要方法但对框架进行控制。
  • 黑盒框架: 
    • 通过实现插件接口进行扩展(使用组合/委派); 
    • 常用设计模式:Strategy, Observer ; 
    • 插件加载机制加载插件并对框架进行控制。

 




以上是关于软件构造第五章第二节 设计可复用的软件的主要内容,如果未能解决你的问题,请参考以下文章

软件构造第五章第三节 可复用的设计模式

软件构造 第五章第一节 可复用性的度量形态和外部观察

软件构造第六章第二节 可维护的设计模式

软件构造 第一章第二节 软件开发的质量属性

software construction第一章第二节 软件开发的质量属性

《构建之法》第五章学习总结