CH12_处理继承关系

函数上移(Pull Up Method)

反向重构:函数下移(Push Down Method)

在这里插入图片描述

class Employee {/*...*/}
class Salesman extends Employee {
	get name() {/*...*/}
}
class Engineer extends Employee {
	get name() {/*...*/}
}
class Employee {
	get name() {/*...*/}
}
class Salesman extends Employee {/*...*/}
class Engineer extends Employee {/*...*/}

动机

避免重复代码是很重要的。重复的两个函数现在也许能够正常工作,但假以时日却只会成为滋生bug的温床。无论何时,只要系统内出现重复,你就会面临“修改其中一个却未能修改另一个”的风险。

如果某个函数在各个子类中的函数体都相同(它们很可能是通过复制粘贴得到的),这就是最显而易见的函数上移
适用场合。

函数上移过程中最麻烦的一点就是,被提升的函数可能会引用只出现于子类而不出现于超类的特性。此时,就得用字段上移(353)和函数上移先将这些特性(类或者函数)提升到超类。

做法

  • 检查待提升函数,确定它们是完全一致的。

    如果它们做了相同的事情,但函数体并不完全一致,那就先对它们进行重构,直到其函数体完全一致。

  • 检查函数体内引用的所有函数调用和字段都能从超类中调用到。

  • 如果待提升函数的签名不同,使用改变函数声明(124)将那些签名都修改为你想要在超类中使用的签名。

  • 在超类中新建一个函数,将某一个待提升函数的代码复制到其中。

  • 执行静态检查。

  • 移除一个待提升的子类函数。

  • 测试。

  • 逐一移除待提升的子类函数,直到只剩下超类中的函数为止。

字段上移(Pull Up Field)

反向重构:字段下移(Push Down Field)

在这里插入图片描述

class Employee {/*...*/} // Java
class Salesman extends Employee {
	private String name;
}	
class Engineer extends Employee {
	private String name;
}
class Employee {
	protected String name;
}
class Salesman extends Employee {/*...*/}
class Engineer extends Employee {/*...*/}

动机

如果各子类是分别开发的,或者是在重构过程中组合起来的,常常会发现它们拥有重复特性,特别是字段更容易重复。

本项重构可从两方面减少重复:首先它去除了重复的数据声明;其次可以将使用该字段的行为从子类移至超类,从而去除重复的行为。

做法

  • 针对待提升之字段,检查它们的所有使用点,确认它们以同样的方式被使用。

  • 如果这些字段的名称不同,先使用变量改名(137)为它们取个相同的名字。

  • 在超类中新建一个字段。

    新字段需要对所有子类可见(在大多数语言中protected权限便已足够)。

  • 移除子类中的字段。

  • 测试。

构造函数本体上移(Pull Up Constructor Body)

在这里插入图片描述

class Party {/*...*/}
class Employee extends Party {
    constructor(name, id, monthlyCost) {
        super();
        this._id = id;
        this._name = name;
        this._monthlyCost = monthlyCost;
    }
}
class Party {
    constructor(name){
    	this._name = name;
    }
}
class Employee extends Party {
    constructor(name, id, monthlyCost) {
        super(name);
        this._id = id;
        this._monthlyCost = monthlyCost;
    }
}

动机

构造函数是很奇妙的东西。它们不是普通函数,使用它们比使用普通函数受到更多的限制。

如果看见各个子类中的函数有共同行为,可以使用提炼函数(106)将它们提炼到一个独立函数中,然后使用函数上移(350)将这个函数提升至超类。

如果重构过程过于复杂,我会考虑转而使用以工厂函数取代构造函数(334)。

做法

  • 如果超类还不存在构造函数,首先为其定义一个。确保让子类调用超类的构造函数。
  • 使用移动语句(223)将子类中构造函数中的公共语句移动到超类的构造函数调用语句之后。
  • 逐一移除子类间的公共代码,将其提升至超类构造函数中。对于公共代码中引用到的变量,将其作为参数传递给超类的构造函数。
  • 测试。
  • 如果存在无法简单提升至超类的公共代码,先应用提炼函数(106),再利用函数上移(350)提升之。

函数下移(Push Down Method)

反向重构:函数上移(Pull up Method)

在这里插入图片描述

class Employee {
	get quota {/*...*/}
}
class Engineer extends Employee {/*...*/}
class Salesman extends Employee {/*...*/}
class Employee {/*...*/}
class Engineer extends Employee {/*...*/}
class Salesman extends Employee {
	get quota {/*...*/}
}

动机

如果超类中的某个函数只与一个(或少数几个)子类有关,那么最好将其从超类中挪走,放到真正关心它的子类中去。这项重构手法只有在超类明确知道哪些子类需要这个函数时适用。如果超类不知晓这个信息,那就得用以多态取代条件表达式(272),只留些共用的行为在超类。

做法

  • 将超类中的函数本体复制到每一个需要此函数的子类中。
  • 删除超类中的函数。
  • 测试。
  • 将该函数从所有不需要它的那些子类中删除。
  • 测试。

字段下移(Push Down Field)

反向重构:字段上移(Pull Up Field)

在这里插入图片描述

class Employee { // Java
	private String quota;
}
class Engineer extends Employee {/*...*/}
class Salesman extends Employee {/*...*/}
class Employee {/*...*/}
class Engineer extends Employee {/*...*/}
class Salesman extends Employee {
	protected String quota;
}

动机

如果某个字段只被一个子类(或者一小部分子类)用到,就将其搬移到需要该字段的子类中。

做法

  • 在所有需要该字段的子类中声明该字段。
  • 将该字段从超类中移除。
  • 测试。
  • 将该字段从所有不需要它的那些子类中删掉。
  • 测试。

以子类取代类型码(Replace Type Code with Subclasses)

包含旧重构:以State/Strategy取代类型码(Replace Type Code with State/Strategy)

包含旧重构:提炼子类(Extract Subclass)

反向重构:移除子类(Remove Subclass)

在这里插入图片描述

function createEmployee(name, type) {
	return new Employee(name, type);
}
function createEmployee(name, type) {
    switch (type) {
        case "engineer": return new Engineer(name);
        case "salesman": return new Salesman(name);
        case "manager": return new Manager (name);
    }
}

动机

软件系统经常需要表现“相似但又不同的东西”,比如员工可以按职位分类(工程师、经理、销售)。表现分类关系的第一种工具是类型码字段——根据具体的编程语言,可能实现为枚举、符号、字符串或者数字。

大多数时候,有这样的类型码就够了。也可以更进一步,引入子类:可以用多态来处理条件逻辑。如果有几个函数都在根据类型码的取值采取不同的行为,多态就显得特别有用。引入子类之后,可以用以多态取代条件表达式(272)来处理这些函数。

另外,有些字段或函数只对特定的类型码取值才有意义,例如“销售目标”只对“销售”这类员工才有意义。此时可以创建子类,然后用字段下移(361)把这样的字段放到合适的子类中去。

在使用以子类取代类型码时,需要考虑一个问题:应该直接处理携带类型码的这个类,还是应该处理类型码本身
呢?如果子类的类别是可变的,那么也不能使用直接继承的方案。可以运用以对象取代基本类型(174)把类型码包装成“父级”类,然后对其使用以子类取代类型码(362)。

做法

  • 自封装类型码字段。
  • 任选一个类型码取值,为其创建一个子类。覆盖类型码类的取值函数,令其返回该类型码的字面量值。
  • 创建一个选择器逻辑,把类型码参数映射到新的子类。
  • 测试。
  • 针对每个类型码取值,重复上述”创建子类,添加选择器逻辑“的过程。每次修改后执行测试。
  • 去除类型码字段。
  • 测试。
  • 使用函数下移(359)和以多态取代条件表达式(272)处理原本访问了类型码的函数。全部处理完后,就可以移除类型码的访问函数。

移除子类(Remove Subclass)

曾用名:以字段取代子类(Replace Subclass with Fields)

反向重构:以子类取代类型码(362)

在这里插入图片描述

class Person {
	get genderCode() {return "X";}
}
class Male extends Person {
	get genderCode() {return "M";}
}
class Female extends Person {
	get genderCode() {return "F";}
}
class Person {
	get genderCode() {return this._genderCode;}
}

动机

子类很有用,它们为数据结构的多样和行为的多态提供支持,它们是针对差异编程的好工具。但随着软件的演化,子类所支持的变化可能会被搬移到别处,甚至完全去除,这时子类就失去了价值。有时添加子类是为了应对未来的功能,结果构想中的功能压根没被构造出来,或者用了另一种方式构造,使该子类不再被需要了。

子类存在着就有成本,阅读者要花心思去理解它的用意,所以如果子类的用处太少,就不值得存在了。此时,最好的选择就是移除子类,将其替换为超类中的一个字段。

做法

  • 使用以工厂函数取代构造函数(334),把子类的构造函数包装到超类的工厂函数中。
  • 如果有任何代码检查子类的类型,先用提炼函数(106)把类型检查逻辑包装起来,然后用搬移函数(198)将其搬到超类。每次修改后执行测试。
  • 新建一个字段,用于代表子类的类型。
  • 将原本针对子类的类型做判断的函数改为使用新建的类型字段。
  • 删除子类。
  • 测试。

本重构手法常用于一次移除多个子类,此时需要先把这些子类都封装起来(添加工厂函数、搬移类型检查),然后再逐个将它们折叠到超类中。

提炼超类(Extract Superclass)

在这里插入图片描述

class Department {
    get totalAnnualCost() {/*...*/}
    get name() {/*...*/}
    get headCount() {/*...*/}
}
class Employee {
    get annualCost() {/*...*/}
    get name() {/*...*/}
    get id() {/*...*/}
}
class Party {
    get name() {/*...*/}
    get annualCost() {/*...*/}
}
class Department extends Party {
    get annualCost() {/*...*/}
    get headCount() {/*...*/}
}
class Employee extends Party {
    get annualCost() {/*...*/}
    get id() {/*...*/}
}

动机

如果看见两个类在做相似的事,可以利用基本的继承机制把它们的相似之处提炼到超类。可以用字段上移(353)把相同的数据搬到超类,用函数上移(350)搬移相同的行为。

大多数人谈到面向对象时,认为继承必须预先仔细计划,应该根据“真实世界”的分类结构建立对象模型。很多时候,合理的继承关系是在程序演化的过程中才浮现出来的:发现了一些共同元素,希望把它们抽取到一处,于是就有了继承关系。

另一种选择就是提炼类(182)。这两种方案之间的选择,其实就是继承和委托之间的选择,总之目的都是把重复的行为收拢一处。

做法

  • 为原本的类新建一个空白的超类。
  • 测试。
  • 使用构造函数本体上移(355)、函数上移(350)和字段上移(353)手法,逐一将子类的共同元素上移到超类。
  • 检查留在子类中的函数,看它们是否还有共同的成分。如果有,可以先用提炼函数(106)将其提炼出来,再用函数上移(350)搬到超类。
  • 检查所有使用原本的类的客户端代码,考虑将其调整为用超类的接口。

折叠继承关系(Collapse Hierarchy)

在这里插入图片描述

class Employee {/*...*/}
class Salesman extends Employee {/*...*/}
class Employee {/*...*/}

动机

在重构类继承体系时,如果会发现一个类与其超类已经没多大差别,可以把超类和子类合并起来。

做法

  • 选择想移除的类:是超类还是子类?
  • 使用字段上移(353)、字段下移(361)、函数上移(350)和函数下移(359),把所有元素都移到同一个类中。
  • 调整即将被移除的那个类的所有引用点,令它们改而引用合并后留下的类。
  • 移除我们的目标;此时它应该已经成为一个空类。
  • 测试。

以委托取代子类(Replace Subclass with Delegate)

在这里插入图片描述

class Order {
    get daysToShip() {
    	return this._warehouse.daysToShip;
    }
}
class PriorityOrder extends Order {
    get daysToShip() {
    	return this._priorityPlan.daysToShip;
    }
}
class Order {
    get daysToShip() {
        return (this._priorityDelegate)
            ? this._priorityDelegate.daysToShip
            : this._warehouse.daysToShip;
    }
}
class PriorityOrderDelegate {
    get daysToShip() {
    	return this._priorityPlan.daysToShip
    }
}

动机

如果一个对象的行为有明显的类别之分,继承是很自然的表达方式。可以把共用的数据和行为放在超类中,每个子类根据需要覆写部分特性。在面向对象语言中,继承很容易实现,因此也是程序员熟悉的机制。

但继承也有其短板。最明显的是,继承这张牌只能打一次。导致行为不同的原因可能有多种,但继承只能用于处理一个方向上的变化。更大的问题在于,继承给类之间引入了非常紧密的关系。在超类上做任何修改,都很可能破坏子类。

这两个问题用委托都能解决。对于不同的变化原因,可以委托给不同的类。委托是对象之间常规的关系。与继承关系相比,使用委托关系时接口更清晰、耦合更少。

有一条流行的原则:“对象组合优于类继承”(“组合”跟“委托”是同一回事)。

做法

  • 如果构造函数有多个调用者,首先用以工厂函数取代构造函数(334)把构造函数包装起来。
  • 创建一个空的委托类,这个类的构造函数应该接受所有子类特有的数据项,并且经常以参数的形式接受一个指回超类的引用。
  • 在超类中添加一个字段,用于安放委托对象。
  • 修改子类的创建逻辑,使其初始化上述委托字段,放入一个委托对象的实例。
  • 选择一个子类中的函数,将其移入委托类。
  • 使用搬移函数(198)手法搬移上述函数,不要删除源类中的委托代码。
  • 如果被搬移的源函数还在子类之外被调用了,就把留在源类中的委托代码从子类移到超类,并在委托代码之前加上卫语句,检查委托对象存在。如果子类之外已经没有其他调用者,就用移除死代码(237)去掉已经没人使用的委托代码。
  • 测试。
  • 重复上述过程,直到子类中所有函数都搬到委托类。
  • 找到所有调用子类构造函数的地方,逐一将其改为使用超类的构造函数。
  • 测试。
  • 运用移除死代码(237)去掉子类。

以委托取代超类(Replace Superclass with Delegate)

曾用名:以委托取代继承(Replace Inheritance with Delegate)

在这里插入图片描述

class List {/*...*/}
class Stack extends List {/*...*/}
class Stack {
    constructor() {
    	this._storage = new List();
    }
}
class List {/*...*/}

动机

在面向对象程序中,通过继承来复用现有功能,是一种既强大又便捷的手段。只要继承一个已有的类,覆写一些功能,再添加一些功能,就能达成目的。但继承也有可能造成困扰和混乱。

如果超类的一些函数对子类并不适用,就说明不应该通过继承来获得超类的功能。

合理的继承关系有一个重要特征:子类的所有实例都应该是超类的实例,通过超类的接口来使用子类的实例应该完全不出问题。

做法

  • 在子类中新建一个字段,使其引用超类的一个对象,并将这个委托引用初始化为超类的新实例。
  • 针对超类的每个函数,在子类中创建一个转发函数,将调用请求转发给委托引用。每转发一块完整逻辑,都要执行测试。
  • 当所有超类函数都被转发函数覆写后,就可以去掉继承关系。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/148107.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

软件性能测试学习笔记(LoadRunner):从零开始

文章目录 概述LoadRunner的使用创建编辑脚本(Virtual User Generator)集合点思考时间事务检查点关联参数化 运行负载测试(Controller) 性能测试报告场景设置表格测试指标记录表 其他的杂谈内容 概述 软件的性能测试与软件的功能测…

在抖音电商,他们帮女性实现了L码自由

“很多(女装)店铺只做到L,甚至L(其实)是M码。”身高1米6、体重60公斤的达人鸭嗓明明120斤 在抖音上吐槽道,“尤其是夏天的连衣裙,胸围很多不超过85厘米,那它的意思就是你可以胖&…

优思学院|一文快速看懂TRIZ原理

在创新领域,TRIZ被翻译为发明问题的解决理论。TRIZ理论深刻揭示了创造发明的内在规律和原理,专注于澄清和强调系统中存在的矛盾,旨在完全解决这些矛盾,实现最终的理想解决方案。实践证明,运用TRIZ理论不仅能够极大地加…

python安装pip install报错Could not fetch URL https://pypi.org/simple/pip/...更换镜像源

更换镜像源 一. 现象pycharm使用 pip install xxx安装包时,一直报错: 二. 原因:三. 解决办法:一. 临时使用二. 永久更改三. 永久更改1. Windowswindows环境下Windows(示例win10) 2. Linux or Mac3. Pycharm…

【从删库到跑路】MySQL数据库 | 全局锁 | 表级锁 | 行级锁

文章目录 🌹简述🎄全局锁⭐数据备份🎈设置全局锁🎈对表进行备份🎈释放锁 🎄表级锁🛸表锁⭐读锁⭐写锁 🛸元数据锁🛸意向锁⭐意向共享锁⭐意向排他锁 🎄行级锁…

类属性修改(为什么python类不具备被赋值能力?)

为什么python类不具备被赋值能力?,用魔术方法收集实参,在类中可以定义方法处理实际参数,实现对类“赋值”。 (笔记模板由python脚本于2023年11月15日 12:45:27创建,本篇笔记适合初通Python类class的coder翻阅) 【学习的…

Fedora Linux 39 正式版官宣 11 月 发布

导读Fedora Linux 39 正式版此前宣布将于 10 月底发布,不过这款 Linux 发行版面临了一些延期,今天开发团队声称,Fedora Linux 39 正式版将于 11 月 7 日发布。 过查询得知,在近日的 "Go / No-Go" 会议上,开…

多维时序 | MATLAB实现PSO-BiLSTM-Attention粒子群优化双向长短期记忆神经网络融合注意力机制的多变量时间序列预测

多维时序 | MATLAB实现PSO-BiLSTM-Attention粒子群优化双向长短期记忆神经网络融合注意力机制的多变量时间序列预测 目录 多维时序 | MATLAB实现PSO-BiLSTM-Attention粒子群优化双向长短期记忆神经网络融合注意力机制的多变量时间序列预测预测效果基本介绍模型描述程序设计参考…

nacos集群配置(超完整)

win配置与linux一样,换端口或者换ip,文章采用的 linux不同IP,同一端口 节点ipportnacos1192.168.253.168848nacos2192.168.253.178848nacos3192.168.253.188848 单IP多个端口 1.复制两个,重命名 2.修改 conf目录下的 application…

【软考篇】中级软件设计师 第二部分(一)

中级软件设计师 第二部分(一) 八. 层次化结构8.1 局部性原理8.2 体系8.3 分类8.3.1 存取方式8.3.2 工作方式 8.4 Cache8.4.1 例题 8.5 地址映像 九. 主存编址9.1 例题一 十. 可靠性10.1 串联系统和并联系统 十一. 网络安全11.1 保密性11.2 完整性&#x…

Qt基础 QT QTextEdit自动滑动

目录 1.吐槽那些写文章不动脑子的人,不带脑子就别写,误人子弟 2.问题解决: 1.吐槽那些写文章不动脑子的人,不带脑子就别写,误人子弟 最近公司在做一个提词项目,本来对这里功能难易感觉属于一般的,谁知道碰到一个很简单问题,搞了半天,先喷一下百度浏览器 不知道是…

Visual Studio Code安装和设置中文

文章目录 Visual Studio Code安装Visual Studio Code设置中文 步骤如下: Visual Studio Code安装 1.下载安装包 VS Code的官网 下载链接中的“az764295.vo.msecnd.net” 替换为国内镜像地址“vscode.cdn.azure.cn”,下载速度直接飙升至几十 Mb/s。(在官网下载速度…

HCIA-经典综合实验(二)

经典综合实验(二) 实验拓扑配置步骤配置Eth-Trunk聚合链路第一步 配置二层VLAN第二步 配置MSTP生成树第三步 配置相关IP地址第四步 配置DHCP及DHCP中继第五步 配置三层的网关冗余协议 VRRP及OSPF第六步 配置静态路由,NAT地址转换及其他配置完善 配置验证…

微服务基础,分布式核心,常见微服务框架,SpringCloud概述,搭建SpringCloud微服务项目详细步骤,含源代码

微服务基础 系统架构的演变 随着会联网的发展,网站应用的规模不断扩大,常规的应用架构已经无法应对,分布式服务架构以及微服务架构势在必行,必须一个治理系统确保架构有条不紊的演进 单体应用框架 Web应用程序发展的早期&…

【Qt-23】Qt charts绘制曲线图

一、QChart简介 QChart是Qt中专门用于绘制图表的模块,支持折线图、柱状图、饼图等常见类型。其主要组成部分有: QChart:整个图表的容器,管理图表中的所有数据和图形属性QChartView:继承自QGraphicsView,用于…

Python安装第三方库出错完美解决方法

错误 Could not find a version that satisfies the requirement PIL (from versions: none) ERROR: No matching distribution found for PILTry to run this command from the system terminal. Make sure that you use the correct version of pip installed for your Pyth…

轻松找回您的珍贵回忆的最好的 6 种照片数据恢复软件!

照片是珍惜过去珍贵时刻的唯一方式。它们让记忆永存,帮助我们重温生命中最美好的时刻。但是,当这些时刻丢失时会发生什么?您是否曾经因系统崩溃而意外删除或丢失照片?丢失照片可能令人心碎,但仍有希望,因为…

力扣每日一道系列 --- LeetCode 138. 随机链表的复制

📷 江池俊: 个人主页 🔥个人专栏: ✅数据结构探索 ✅LeetCode每日一道 🌅 有航道的人,再渺小也不会迷途。 LeetCode 138. 随机链表的复制 给你一个长度为 n 的链表,每个节点包含一个额外增加…

【双指针】:Leetcode283.移动零

朋友们、伙计们,我们又见面了,本专栏是关于各种算法的解析,如果看完之后对你有一定的启发,那么请留下你的三连,祝大家心想事成! C 语 言 专 栏:C语言:从入门到精通 数据结构专栏&…

[数据结构]—带头双向循环链表——超详解

💓作者简介🎉:在校大二迷茫大学生 💖个人主页🎉:小李很执着 💗系列专栏🎉:数据结构 每日分享✨:旅行是为了迷路,迷路是为了遇上美好❣️❣️❣️ …