CH12_处理继承关系

news2024/11/18 3:47:33

函数上移(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 {/*...*/}

动机

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

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

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

做法

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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

AI数字员工创新工作模式丨市女协走进实在智能,沉浸体验RPA Agent智能魅力

11月10日,杭州市女企业家协会组织走进副会长张军燕企业——杭州实在智能科技有限公司,开展“领跑AI时代,做先进企业”人工智能沙龙活动。 本次沙龙是一场真正关注最前沿AI大模型与RPA自动化技术、寻找企业确定性增长的科技盛宴,也…

《深入浅出.NET框架设计与实现》阅读笔记(四)

静态文件系统 通过ASP.NET Core 提供的静态文件模块和静态文件中间件,可以轻松的让应用程序拥有访问静态文件的功能,同时可以基于IFileProvider对象来自定义文件系统,如基于Redis做扩展文件系统 启动静态文件服务 在Program.cs 类中&#x…

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

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

国产企业级低代码开发哪个最好?这一款超好用

低代码开发平台(Low-code Development Platform)正在迅速崛起,成为未来软件技术发展的主导趋势。通过使用低代码开发平台,企业能够显著提高开发效率,降低对专业开发人员的依赖,并实现更快速的软件交付和使用…

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

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

某头部通信企业:SDLC+模糊测试,保障数实融合安全发展

某头部通信企业是全球领先的综合通信信息解决方案提供商,为全球电信运营商、政企客户和消费者提供创新的技术与产品解决方案。该企业持续关注核心技术攻关,深入打造系列化标杆项目和价值场景,加强数字化平台的推广应用,加快共建开…

蓝桥杯每日一题2023.11.15

题目描述 此处的快速排序有一个思想:以一个数x来判定这l至r区间的数的大小,如果a[l]小于x就与右侧的a[r]交换,最后x可以将这个区间的数进行一分为二。填空出就是已经将x移动到左部分和右部分之间,来确定二分的一个界点 答案&…

大模型在数据分析场景下的能力评测|进阶篇

做数据分析,什么大模型比较合适? 如何调优大模型,来更好地做数据计算和洞察分析? 如何降低整体成本,同时保障分析体验?10月25日,我们发布了数据分析场景下的大模型能力评测框架(点击…

NovelD: A Simple yet Effective Exploration Criterion论文笔记

NovelD:一种简单而有效的探索准则 1、Motivation 针对稀疏奖励环境下的智能体探索问题,许多工作中采用各种内在奖励(Intrinsic Reward)设计来指导困难探索环境中的探索 ,例如: ICM:基于前向动力学模型的好奇心驱动探索RND&…

外贸客户管理系统是什么?推荐的管理软件?

外贸客户管理系统哪个好用?海洋建站如何选管理系统? 外贸客户管理系统,是一款专为外贸企业设计的客户关系管理系统,旨在帮助外贸企业建立与维护客户关系,提高客户满意度和忠诚度,提升企业业绩。海洋建站将…

机器学习-搜索技术:从技术发展到应用实战的全面指南

在本文中,我们全面探讨了人工智能中搜索技术的发展,从基础算法如DFS和BFS,到高级搜索技术如CSP和优化问题的解决方案,进而探索了机器学习与搜索的融合,最后展望了未来的趋势和挑战,提供了对AI搜索技术深刻的…

属兔人连续两年不顺,运势低迷要化解

属兔人为人生性浪漫,有着美好憧憬, 与人相处的时候总是谦和待人,不会随便发脾气, 也不喜欢与人发生争执,不善于算计别人。 对于自己的另一半,是一个很温暖的人,为人细腻,并且懂得体谅…

ping: www.baidu.com: Name or service not known解决办法

解决服务器无法ping通外网问题 1、问题描述: 配置了网卡信息,发现还是无法访问外网,并报ping: www.baidu.com: Name or service not known信息 2、问题原因: 这就是外网没开通好 3、解决方法: 修改网卡文件&#xff…

KODExplorer中ace.js代码编辑器中自定义PHP提示片段

目录 KODExplorerace.js参考 KODExplorer 这是搭建云盘工具,该工具可以作为在线开发工具使用,其中使用了ace.js作为编辑器,这里主要讲解ace.js编辑器中如何自定义代码提示下载旧版本,再升级到新版本,直接下载新版本没…

【华为OD题库-022】阿里巴巴找黄金宝箱(IV)-java

题目 一贫如洗的椎夫阿里巴巴在去砍柴的路上,无意中发现了强盗集团的藏宝地,藏宝地有编号从0-N的子,每个箱子上面有一个数字,箱子排列成一个环,编号最大的箱子的下一个是编号为0的箱子。请输出每个箱子贴的数字之后的第…

wx小程序web-view uniapp H5

最近微信小程序对有视频播放的审核严,需要提供“文娱类资质”。而申请这个资质比较繁琐。所以我们在小程序上用web-view做跳转到H5,这是小程序关于web-view文档说明https://developers.weixin.qq.com/miniprogram/dev/component/web-view.html 开发前配…

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

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

荧光量子效率积分球的优势是什么

荧光量子效率积分球是一种测量设备,可以用于测量荧光材料在特定波长下的量子效率。它由一个具有高朗伯特性的漫反射PTFE材料制成,具有高达99%的反射率和朗伯特性。积分球有三个开口,分别为光入射口、样品口和光出射口。光入射口设置有一准直镜…

2023年【电工(高级)】考试报名及电工(高级)考试试卷

题库来源:安全生产模拟考试一点通公众号小程序 2023年【电工(高级)】考试报名及电工(高级)考试试卷,包含电工(高级)考试报名答案和解析及电工(高级)考试试卷…

Linux 读写权限的配置

文章目录 Linux文件权限详解 一、文件权限二、修改文件访问权限的方法三、UMASK值四、三种特殊权限suid、sgid、sticky(sticky权限工作环境中相对常用)五、ACL访问控制列表六、文件权限操作的常用命令 Linux文件权限详解 Linux系统中不仅是对用户与组根…