首页 > 其他分享 >面向 Reuse 的软件构造技术

面向 Reuse 的软件构造技术

时间:2024-05-16 16:18:39浏览次数:15  
标签:Reuse 复用 Cat extends 面向 new 类型 软件 class

前几章介绍了软件构造的核心理论——ADT,核心技术——OOP,其核心是保证代码质量、提高代码安全性

本章面向一个重要的外部指标:可复用性——如何构造出可在不同应用中重复使用的软件模块/API

为什么复用?

软件复用有两个视角:

  • 面向复用编程:开发出可复用的软件
  • 基于复用编程:利用已有的可复用软件搭建应用系统

三点好处:

  • 降低成本和开发时间
  • 经过充分测试,可靠、稳定
  • 标准化,在不同的应用中保持一致

但是,代价也不低:

  • 可复用组件的设计需要定义明确、开放、接口规范简洁、易懂,并着眼于未来的使用
  • 它涉及组织、技术和流程变更,以及支持这些变更的工具成本

开发可复用的软件一般流程如下:

使用已有软件进行开发的一般流程:

怎么复用?

源码层面的复用

说白了就是搜索相应的代码。复制过来为自己所用

模块层面的复用:类/接口

使用继承委托

Library 层面的复用:API/包

Library: 提供可复用功能的类和方法的集合

系统层面的复用:框架(Framework)

所谓框架,就是一组具体类、抽象类、及其之间的连接关系

开发者根据框架的规约,填充自己的代码进去,形成完整系统

框架分为两种:

  • 白盒框架:通过代码层面的继承进行框架扩展
  • 黑盒框架:通过实现特定接口进行框架扩展

设计可复用的类

LSP 原则(Liskov Substitution Principle)

子类型多态:使用者可以用统一的方式处理不同类型的对象

看如下代码:

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

要保持这样一种原则:

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

  • Same or stronger invariants 更强的不变量
  • Same or weaker preconditions 更弱的前置条件
  • Same or stronger postconditions 更强的后置条件

总结来说,就是以下七点:

  1. 子类必须完全的实现父类的方法(子类型需要实现抽象类中的所有未实现的方法)
  2. 子类可以有自己的个性(子类型可以增加方法)
  3. 子类型方法参数:要么不变要么逆变,返回值:协变
  4. 子类型中重写的方法不能抛出额外的异常:协变
  5. 重写和实现父类的方法时输入参数可以被放大(前置条件不能强化/逆变)
  6. 重写和实现父类的方法时输出参数可以被缩小(后置条件不能弱化/协变)
  7. 更强/保持不变量

协变(Covariance)

所谓协变就是无论是父类型到子类型还是方法的返回值类型还是异常的类型都越来越具体,你甚至可以选择不抛出异常(rainy:爷爷不能使用煤气灶做鱼香肉丝,但是你可以)

比如:

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() throws {...}
}

反协变(Contravariance)

反协变是指从父类型到子类型越来越具体,方法的参数类型相反,要不变或越来越抽象

比如:

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

在 Java 中,这种情况被看作 Overload

总结如图:

泛型中的 LSP

先说说类型擦除(type erasure)

  • 虚拟机中没有泛型类型对象,所有对象都属于普通类
  • 泛型信息只存在于编译阶段,在运行时会被擦除
  • 擦除时,类型变量会被替换为限定类型,如果没有限定类型则替换为Object类型

举例,类型参数没有限定时:

类型参数有限定时:

由此明确一点:

  • ArrayList<String>List<String>的子类型
  • List<String>不是List<Object>的子类型,尽管StringObject的子类型

类似的,Box<Integer>不是Box<Number>的子类型:

那么两个泛型类的协变如何实现呢?

通配符(Wildcards)

可以采用通配符(Wildcards)

无限定通配符?表示,在以下情况使用

  • 方法的实现不依赖于类型参数
  • 方法的实现只依赖于Object类中的功能

比如,我们想设计一个方法,打印任意类型List中所有内容

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

由于泛型不协变,这个时候只能打印List<Object>

但是有了通配符就好办了:

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

除此以外,还有下限通配符<? super A>

指只能接受类型 A 以及 A 的父类作为类型参数

上限通配符<? extends A>

指只能接受类型 A 以及 A 的子类作为类型参数,这里的extends既可以代表类的extends,也可以代表接口的implements

比如,可以写出对存放数的List的求和方法:

public static double sumofList(List<? extends Number> list) {
    double s = 0.0;
    for (Number n : list)
        s += n.doubleValue();
    return s;
}

也可以有多种限定的写法:

<T extends B1 & B2 & B3>表示接受的类型参数要是后面所有类的子类型,由于 Java 不能多继承,所以B1,B2,B3中最多只能有一个类,剩下的都是接口,要把类写在最前面

总结如图:

PECS

PECS就是producer-extends, consumer-super

  • 带有子类型限定的上限通配符可以从泛型对象读取
  • 带有超类型限定的下限通配符可以向泛型对象写入

举例:

producer-extends

class Animal{}
class Cat extends Animal{}
class whiteCat extends Cat{}
class BlackCat extends Cat{}

List<? extends Cat> animals= new ArrayList<Cat>();
animals.add(new whiteCat()); //compile error
animals.add(new Cat()); //compile error
animals.add(new Animal()); //compile error
animals.add(new Object()); //compile error
animals.add(null); //succeed, but it is meaningless.
//不能放入任何类型,因为编译器只知道animals中应该放入Cat的某种子类型,但具体放哪种子类型它无法确定
animals= new ArrayList<WhiteCat>();
…
animals= new ArrayList<BlackCat>();
//假如允许放入WhiteCat后,animals可能指向BlackCat集合;反之亦然。
Cat s1 = animals.get(0); //类型上界为Cat,Cat及其父类都能接收返回值
Animal s2 = animals.get(0); //Cat类型可以用Animal接收
WhiteCat s3 = animals.get(0); //error:子类型对象不能接收父类型返回值

consumer-super:

class Animal{}
class Cat extends Animal{}
class whiteCat extends Cat{}
class BlackCat extends Cat{}

List<? super Cat> b = new ArrayList<>(); //参数类型下界是Cat
b.add(new Cat()); //ok
b.add(new WhiteCat()); //ok 子类型也可以
b.add(new Animal()); //error 超类不可以
b.add(null); //ok

Object o1 = b.get(0);//返回类型是未知的,只能用Object类型接收

委托(delegation)

委派的三个要素:

  • 委派给谁?
  • 什么时候进行委派
  • 什么让委派的对象进行具体操作

举个排序的例子:

这个例子可以看到 ADT 中比较大小的一种方式,可以实现Comparator接口,并且重写compare()函数

所谓委托(delegation)就是一个对象请求另一个对象的功能

委托是复用的一种常见形式,它可以描述为一种低级的代码与数据的共享机制

  • 显式委托:发送对象直接传递给接收对象
  • 隐式委托:语言的一些特定规则

再举个简单的例子:

B通过一个A的成员变量,构建起两个类之间的委托,这时显式的

再比如,我们要实现一个能在添加或删除时打印信息的List,使用委托机制代码就很简洁了

委托(delegation)与继承(inheritance)

写到这里,谈谈委托与继承的区别

  • 继承:继承一个基类,添加新的方法或者重写原来的方法来实现某个功能
  • 委托:将某个功能的一部分直接委托给其它对象

委托能办到的,继承似乎都能办到,那么为什么不直接使用继承呢?

譬如,如果子类只需要复用父类的一小部分方法,完全可以不需要继承,而是通过委托机制来实现,从而避免继承大量无用的方法

合成复用原则(CRP)

内容:

  • 类应该通过它们的组合(通过包含实现所需功能的其他类的实例)而不是从基类或父类继承来实现多态的行为和代码重用
  • 组合一个对象可以做什么(has_a 或 use_a)比扩展它是什么(is_a)更好

也就是说,组合要优先于继承(组合式委托的一种形式)

注意:委托发生在对象的层面,而继承发生在类的层面

那么为什么在对象层面更好呢?举个例子:

Employee类有一个方法用于计算奖金:

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

它会有很多不同的子类,例如Manager,Programmer,Secretary,那么计算它们的奖金的时候肯定要重写方法:

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

如果不同类型的manager需要不同的计算方式,那么就有需要引入子类:

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

如果要将某个人从Manager提升为SeniorManager,那么该怎么处理呢?

核心问题在于:每个Employee对象的奖金计算方法都不同,这在对象层面而不是层面

显然,委托机制要更好,使用 CRP 原则的一种实现可以是:

class Manager {
    ManagerBonusCalculator mbc = new ManagerBonusCalculator();
    Money computeBonus() {
        return mbc.computeBonus();
    }
}

class ManagerBonusCalculator {
    Maney computeBonus {... // special computation}
}

设计如图:

举例

再举个例子,能够清晰地展现出从继承到委托的变化

假设要开发一套动物 ADT

  • 各种不同种类的动物,每类动物有自己的独特行为,某些行为可能在不同类型的动物之间复用
  • 考虑到生物学和AI的进展,动物的“行为”可能会发生变化

例如:

  • 行为:飞、叫、…
  • 动物:既会飞又会叫的鸭子、天鹅;不会飞但会叫的猫、狗;…
  • 有10余种“飞”的方式、有10余种“叫”的方式;而且持续增加

第一种实现方式:可以直接为每一种动物都设置一个类:

缺陷很明显,很多动物的飞法其实是一样的,会存在大量重复

第二种实现方式:利用继承,比如先定义一个能飞的动物的抽象类,实现通用的的飞法,每种能飞的动物类都继承这个类,如果有不同的飞法重写即可

这样做实现了对某些通用行为的复用,但是也有缺陷

  • 需要针对飞法设计复杂的继承关系树
  • 由于不能多继承,那么就不能同时支持针对叫法的继承
  • 动物行为发生变化时,继承树也要随之变化

第三种实现方式:利用组合

思路:

  • 使用接口定义系统必须对外展示的不同侧面的行为
  • 接口之间通过extends实现行为的扩展(接口组合)
  • 类直接implements组合之后的接口

这样就能规避复杂的继承关系

比如分别设计抽象行为的接口:

interface Flyable {
    public void fly();
}
interface Quackable {
    public void quack();
}

然后将接口组合,也就是行为的组合,比如:

interface Ducklike extends Flyable, Quackable{}

然后再在具体的类中实现这个接口

一种实现如图:

委托的类型

三种形态:

  • Dependency (A use B)
  • Association (A has B)
  • Composition/aggregation (A owns B)

使用者调用某个功能也就呈现出这样的结构:

接下来,我们逐一分析:

Dependency: 临时性的委托

Dependency: a temporary relationship that an object requires other objects (suppliers) for their implementation.

  • 使用某个类最简单的办法就是直接调用它的方法,通过方法的参数或者在方法的局部中使用发生联系
  • 也就说这种联系是对象的某个行为带来的

举例

Association: 永久的委托

Association: a persistent relationship between classes of objects that allows one object instance to cause another to perform an action on its behalf.

  • has_a: 一个类将另一个类作为变量
  • 这种关系时结构化的,因为它只定义了着一种类型的对象与另一个对象的关系,这种关系不是对象的行为带来的

举例

Composition: 更强的联系

Composition is a way to combine simple objects or data types into more complex ones.

  • is_part _of: has_a: 一个类将另一个类作为变量
  • 最后的实现可以看作一个对象包含了另一个对象

但是这种实现难以变化

举例

Aggregation: 更弱的联系

Aggregation: the object exists outside the other, is created outside, so it is passed as an argument to the construtor.

  • has_a

这种实现可以动态变化

举例

设计系统层面的 API 库与框架(frameworks)

可以说,API 是一个程序员最重要的资产和荣耀

  • 好的代码都是模块化的——它们都有 API
  • 要始终以开发 API 的标准面对任何开发任务,面向“复用”编程,而不是面向“应用”编程

白盒(Whitebox)框架与黑盒(Blackbox)框架

白盒框架

  • 通过编写子类和重写方法进行扩展
  • 常见的设计模式:Template Method
  • 子类有main方法,但是控制权在框架

黑盒框架

  • 通过实现插件接口(plugin interface)进行扩展
  • 常见的设计模式:Strategy, Observer
  • 框架中的框架加载机制加载插件

举例

举一个计算器的例子:

不用框架

使用白盒框架

使用黑盒框架

Whitebox vs. Blackbox Frameworks

  • 白盒框架使用子类继承

  • 允许对所有非私有的方法扩展

  • 需要理解父类的实现

  • 同时只能实现一种扩展

  • 所有代码在一起编译

  • 也被叫做开发人员(developer)框架

  • 黑盒框架使用委托

  • 允许扩展接口中的功能

  • 只需要理解接口

  • 有多种插件

  • 提供更多的模块化

  • 可以从开发环境中分离出来(.jar, .dll, ...)

  • 也被叫做最终用户(end-user)框架,平台(platforms)

总结

本文从四个层面(源码级别,模块级别,库级别,系统级别)分别讲解了如何设计复用

尤其分析了设计可复用的的方法,从中导出著名的 LSP 原则和 CRP 原则

本文使用 Zhihu On VSCode 创作并发布

标签:Reuse,复用,Cat,extends,面向,new,类型,软件,class
From: https://www.cnblogs.com/Phantasia/p/18196186

相关文章

  • 软件评测师笔记11--可靠性测试相关
    什么是可靠性产品在规定的条件和时间内完成特定的功能,产品维持的性能指标 可靠性测试目的1、发现软件系统在需求、设计、编码、测试、实施等各方面的各种缺陷2、为软件的使用和维护提供可靠性数据3、确认软件是否达到可靠性的定量要求 影响可靠性因素环境、软件规模、......
  • 可以高效记事、储存文件的桌面便签软件
    在日常工作中,我们经常面临各种信息和文件的管理挑战。比如,在策划一个重要的项目时,需要随时记录并更新进度、存储相关文件;在与客户沟通时,需要快速记下他们的需求和反馈;在准备会议时,需要整理会议要点和相关资料。这些场景下,一个能高效记事和储存文件的工具就显得至关重要。那么可以......
  • 面向正确性与健壮性的软件构造
    ConstructionforRobustness&Correctness面向正确性与健壮性的软件构造概念正确性:对正确的输出返回正确的结果健壮性:对扯淡不合规的输入有正常合理的表现(报错、提示等)量化标准Java中的Error与Exception对Exception的处理定义分类Checked与Unchecked的Exceptionthrow......
  • 软件设计师基础学习 十三
    十三、结构化开发方法13.1*系统分析与概述1,认识、理解当前的环境,获得当前系统的“物理模型”2.从当前系统的“物理模型”抽象出当前系统的“逻辑模型”3.对当前系统的“逻辑模型”进行分析和优化,建立目标系统的“逻辑模型”4.对目标系统的逻辑模型具体化(物理化),建立目标系......
  • 软件设计师基础学习 十四
    十四、面向对象技术14.1面向对象开发概念:对象:由数据及其操作所构成的封装体,是系统中用来描述客观事务的一个实体,是构成系统的一个基本单位。一个对象通常可以由对象名、属性和方法3个部分组成类:现实世界中实体的形式化描述,类将该实体的属性(数据)和操作(函数)封装在一起......
  • 常用软件
    目录页常用软件图片工具图片工具绘图软件软件名称说明软件名称说明软件名称说明PPTVisioAI或PS视频工具视频工具视频直播软件软件名称(下载地址)说明备注OBSStudio一款视频直播录制软件,为用户提供了视频、文本、图像等......
  • 5款兼容Linux系统的国产软件,支持内网使用,满足信创用户办公需求
    随着信息技术应用创新(信创)的推进,对国产软件的需求日益增长,尤其是在保障信息安全和提升办公效率方面。Linux系统因其开源和安全性特点,受到了许多政企的青睐。今天给大家分享5款兼容Linux系统的国产软件,它们不仅支持内网使用,而且能够满足信创用户的办公需求。 01、永中文档 永......
  • 软件开发与创新-原型设计工具Figma介绍
    Figma是什么?Figma是一个基于浏览器的协作式UI设计工具,从推出至今越来越受到UI设计师的青睐,也有很多的设计团队投入了Figma的怀抱,接下来我会带大家深入了解Figma,以及Figma都有什么优点。Figma能干什么?UI设计Figma是为UI设计而生的设计工具,除了有和Sketch一样基......
  • 【ubuntu】几个软件安装源(22.04为例)
    1、阿里https://developer.aliyun.com/mirror/ubuntu22.04为例debhttps://mirrors.aliyun.com/ubuntu/jammymainrestricteduniversemultiversedeb-srchttps://mirrors.aliyun.com/ubuntu/jammymainrestricteduniversemultiversedebhttps://mirrors.aliyun.com......
  • 软件设计模式概念篇
    创建型模式1、创建型模式(CreationalPattern)对类的实例化过程进行了抽象,能够将软件模块中对象的创建和对象的使用分离。2、为了使软件的结构更加清晰,外界对于这些对象只需要知道它们共同的接口,而不需要清楚其具体的实现细节,使整个系统的设计更加符合单一职责原则。3、创建型......