CH07_封装

news2024/11/19 6:19:57

封装记录(Encapsulate Record | 162)

曾用名:以数据类代替记录(Replace Record with Data Class)

在这里插入图片描述

organization = {name: "Acme Gooseberries", country: "GB"};
class Organization {
    constructor(data) {
        this._name = data.name;
        this._country = data.country;
	}
    get name() {return this._name;}
    set name(arg) {this._name = arg;}
    get country() {return this._country;}
    set country(arg) {this._country = arg;}
}

动机

记录型结构是多数编程语言提供的一种常见特性。它们能直观的组织起存在关联的数据,可以将数据作为有意义的端元传递,而不仅是一堆数据的拼凑。但简单的记录型结构也有缺陷,需要清晰的区分“记录中存储的数据”和“通过计算得到的数据”。

对于可变数据,建议使用类对象,对象可以隐藏结构的细节,该对象的用户不必追究存储的细节和计算的过程,同时,这种封装还有助于字段的改名(外界通过访问函数来获取字段的值)。

记录型结构可以有两种类型:一种需要声明合法的字段名字,另一种可以随便用任何字段名字。后者常由语言库本身实现,并通过类的形式提供出来,这些类称为散列(hash)、映射(map)、散列映射(hashmap)、字典(dictionary)或关联数组(associative array)等。但使用这类结构也有缺陷,那就是一条记录上持有什么字段往往不够直观。

程序中间常常需要互相传递嵌套的列表(list)或散列映射结构,这些数据结构后续经常需要被序列化成JSON或XML。这样的嵌套结构同样值得封装,这样,如果后续其结构需要变更或者需要修改记录内的值,封装能更好地应对变化。

做法

  • 对持有记录的变量使用封装变量(132),将其封装到一个函数中。

  • 创建一个类,将记录包装起来,并将记录变量的值替换为该类的一个实例。然后在类上定义一个访问函数,用于返回原始的记录。修改封装变量的函数,令其使用这个访问函数。

  • 测试

  • 新建一个函数,让它返回该类的对象,而非那条元素的记录。

  • 对于该记录的每处使用点,将原先返回记录的函数调用替换为那个返回实例对象的函数调用。使用对象上的访问函数来获取数据的字段,如果该字段的访问函数还不存在,那就创建一个。每次更改之后运行测试。

    如果该记录比较复杂,例如是个嵌套解构,那么先重点关注客户端对数据的更新操作,对于读取操作可以考虑返回一个数据副本或只读的数据代理。

  • 移除类对元素记录的访问函数,那个容易搜索的返回原始数据的函数也要一并删除。

  • 测试

  • 如果记录中的字段本身也是复杂结构,考虑对其再次应用封装记录(162)或封装集合(170)手法。

封装集合(Encapsulate Collection | 170)

在这里插入图片描述

class Person {
    get courses(){return this._courses;}
    set courses(aList){this._courses-aList;}
    // ...
}
class Person {
    get courses(){return this.courses.slice();}
    addCourse(aCourse){...}
    removeCourse(aCourse){...}
    // ...
}

动机

封装程序中的所有可变数据,可以很容易看清楚数据被修改的地点和修改方式,需要更改数据结构时就非常方便。但封装集合时通常会犯一个错误:只对集合变量的访问进行了封装,但依然让取值函数返回集合本身。这使得集合的成员变量可以直接被修改,而封装它的类则全然不知,无法介入。为避免此种情况,可以在类上提供一些修改集合的方法——通常是“添加”和“移除”方法。

一种避免直接修改集合的方法是,永远不直接返回集合的值。这种方法提倡,不要直接使用集合的字段,而是通过定义类上的方法来代替。

还有一种方法是,以某种形式限制集合的访问权,只允许对集合进行读操作(比如,在Java中可以很容易地返回集合的一个只读代理)。

最常见的做法是,为集合提供一个取值函数,但令其返回一个集合的副本。这样即使有人修改了副本,被封装的集合也不会受到影响。

采用哪种方法并无定式,最重要的是在同个代码库中做法要保持一致。

做法

  • 如果集合的引用尚未被封装起来,先用封装变量(132)封装它
  • 在类上添加用于“添加集合元素”和“移除集合元素”的函数(如果存在对集合的设值函数,尽可能先用移除设值函数(331)移除它。如果不能移除该设值函数,至少让它返回集合的一个副本)
  • 执行静态检查
  • 查找集合的引用点。如果有调用者直接修改集合,令该处调用使用新的添加/移除元素的函数。每次修改后执行测试
  • 修改集合的取值函数,使其返回一份只读数据,可以使用只读代理或者数据副本。
  • 测试

以对象取代基本类型(Replace Primitive with Object | 174)

曾用名:以对象取代数据值(Replace Data Value with Object)

曾用名:以类取代类型码(Replace Type Code with Class)

在这里插入图片描述

orders.filter(o => "high" === o.priority || "rush" === o.priority);
orders.filter(o => o.priority.higherThan(new Priority("normal")))

动机

开发初期,往往决定以简单的数据项表示简单的情况,比如使用数字或字符串等。但随着开发的进行,可能会发现这些简单数据不再那么简单。

一旦发现随某个数据的操作不仅仅局限于打印时,可以为它创建一个新类。一开始这个类也许只是简单包装一下简单类型的数据,不过只要有了类,日后添加业务累哦及就简单多了。

做法

  • 如果变量尚未被封装起来,先使用封装变量(132)封装它。
  • 为这个数据值创建一个简单的类。类的构造函数应该保存这个数据值,并为它提供一个取值函数。
  • 执行静态检查。
  • 修改第一步得到的设值函数,令其创建一个新类的对象并将其存入字段,如果有必要的话,同时修改字段的类型声明
  • 修改取值函数,令其调用新类的取值函数,并返回结果。
  • 测试
  • 考虑对第一步得到的访问函数使用函数改名(124),以便更好反应其用途。
  • 考虑应用将引用对象改为值对象(252)或将值对象改为引用对象(256),明确指出先对象的角色是值对象还是引用对象。

以查询取代临时变量(Replace Temp with Query | 178)

在这里插入图片描述

const basePrice = this._quantity * this._itemPrice;
if (basePrice > 1000)
	return basePrice * 0.95;
else
	return basePrice * 0.98;
get basePrice() {this._quantity * this._itemPrice;}
...
if (this.basePrice() > 1000)
	return this.basePrice() * 0.95;
else
	return this.basePrice() * 0.98;

动机

临时变量的一个作用是保存某段代码的返回值,以便在函数的后面部分使用它。临时变量允许编程中引用之前的值,既能解释它的含义,还能避免对代码进行重复计算。但尽管使用变量很方便,很多时候还是值得更进一步,将它们抽取成函数。

抽取成函数能避免在多个函数中重复编写计算逻辑。

这项重构手法在类中施展效果最好,因为类为待提炼函数提供了一个共同的上下文。

以查询取代临时变量(178)手法只适用于处理某些类型的临时变量:那些只被计算一次且之后不再被修改的变量。

做法

  • 检查变量在使用前是否已经完全计算完毕,检查计算它的那段代码是否每次都能得到一样的值
  • 如果变量目前不是只读的,但是可以改造成为只读变量,那就先改造它。
  • 测试
  • 将为变量赋值的代码提炼成函数(确保待提炼函数没有副作用。若有,先应用将查询函数和修改函数分离(306)手法隔离副作用)
  • 测试
  • 应用内联变量(123)手法移除临时变量

提炼类(Extract Class |182 )

反向重构:内联类(186)

在这里插入图片描述

class Person {
    get officeAreaCode() {return this._officeAreaCode;}
    get officeNumber() {return this._officeNumber;}
}
class Person {
    get officeAreaCode() {return this._telephoneNumber.areaCode;}
    get officeNumber() {return this._telephoneNumber.number;}
}
class TelephoneNumber {
    get areaCode() {return this._areaCode;}
    get number() {return this._number;}
}

动机

你也许听过类似这样的建议:一个类应该是一个清晰的抽象,只处理一些明确的责任,等等。但是在实际工作中,类会不断成长扩展。你会在这儿加入一些功能,在那儿加入一些数据。给某个类添加一项新责任时,你会觉得不值得为这项责任分离出一个独立的类。于是,随着责任不断增加,这个类会变得过分复杂。很快,你的类就会变成一团乱麻。

维护一个有大量函数和数据的类,这样的类往往因为太大而不易理解。此时需要考虑哪些部分可以分离出去,并将它们分离到一个独立的类中。如果某些数据和某些函数总是一起出现,某些数据经常同时变化甚至彼此相依,这就表示应该将它们分离出去。

往往在开发后期出现的信号是类的子类化方式。如果发现子类化只影响类的部分特性,或如果发现某些特性需要以一种方式来子类化,某些特性则需要以另一种方式子类化,这就意味着需要分解原来的类。

做法

  • 决定如何分解类所负的责任。
  • 创建一个新的类,用以表现从旧类中分离出来的责任(如果旧类剩下来的责任与旧类的名称不符合,为旧类改名)
  • 构造旧类时创建一个新类的实列,建立“从旧类访问新类”的连接关系
  • 对于想搬移的每一个字段,运用搬移字段(207)搬移之。每次更改后测试。
  • 使用搬移函数(198)将必要函数搬移到新类。先搬移较低层的函数(也就是“被其他函数调用”多于“调用其他函数”者)。每次更改后运行测试
  • 检查两个类的接口,去掉不再需要的函数,必要时为函数重新取一个合适新环境的名字
  • 决定是否公开新的类。如果确定需要,考虑新类应用将引用对象改为值对象(252)使其成为一个值对象

内联类(Inline Class | 186)

反向重构:提炼类(182)

在这里插入图片描述

class Person {
    get officeAreaCode() {return this._telephoneNumber.areaCode;}
    get officeNumber() {return this._telephoneNumber.number;}
}
class TelephoneNumber {
    get areaCode() {return this._areaCode;}
    get number() {return this._number;}
}
class Person {
    get officeAreaCode() {return this._officeAreaCode;}
    get officeNumber() {return this._officeNumber;}
}

动机

内联类正好与提炼类(182)相反。如果一个类不再承担足够责任,不再有单独存在的理由(这通常是因为此前的重构动作移走了这个类的责任),将这个类塞进另外一个类中。

应用这个手法的另一个场景是,手头有两个类,想重新安排它们肩负的职责,并让它们产生关联。这时发现先用本手法将它们内联成一个类再用提炼类(182)去分离其职责会更加简单。

做法

  • 对于待内联类(源类)中的所有public函数,在目标类上创建一个对应的函,新创建的所有函数应该直接委托至源类。
  • 修改袁磊public方法的所有引用点,令它们调用目标类对应的委托方法。每次更改后运行测试。
  • 将源类中的函数于数据全部搬移到目标类,每次修改之后进行测试,直到源类编程空壳为止。
  • 删除源类

隐藏委托关系(Hide Delegate | 189)

反向重构:移除中间人(192)

在这里插入图片描述

manager = aPerson.department.manager;
manager = aPerson.manager;
class Person {
	get manager() {return this.department.manager;}
}

动机

一个好的模块化的设计,“封装”是最关键特征之一。“封装”意味着每个模块都应该尽可能少了解系统的其他部分。一旦发生变化,需要了解这一变化的模块就会比较少,这会使变化比较容易进行。

如果某些客户端先通过服务对象的字段得到另一个对象(受托类),然后调用后者的函数,那么客户就必须知晓这一层委托关系。万一受托类修改了接口,变化会波及通过服务对象使用它的所有客户端。

做法

  • 对于每个委托关系中的函数,在服务对象端建立一个简单的委托函数。
  • 调整客户端,令它只调用服务对象提供的函数。每次调整后运行测试。
  • 如果将来不再有任何客户端需要取用Delegate(受托类),便可移除服务对象中的相关函数。
  • 测试。

移除中间人(Remove Middle Man | 192)

反向重构:隐藏委托关系(189)

在这里插入图片描述

manager = aPerson.manager;
class Person {
	get manager() {return this.department.manager;}
}
manager = aPerson.department.manager;

动机

“封装受托对象”这层封装也是有代价的。每当客户端要使用受托类的新特性时,你就必须在服务端添加一个简单委托函数。随着受托类的特性(功能)越来越多,更多的转发函数就会使人烦躁。服务类完全变成了一个中间人(81),此时就应该让客户直接调用受托类。

很难说什么程度的隐藏才是合适的,可以在系统运行过程中不断进行调整。

做法

  • 为受托对象创建一个取值函数。

  • 对于每个委托函数,让其客户端转为连续的访问函数调用。每次替换后运行测试

    替换完委托方法的所有调用点后,就可以删掉这个委托方法了。
    这能通过可自动化的重构手法来完成,可以先对受托字段使用封装变量(132),再应用内联函数(115)内联所有使用它的函数。

替换算法(Substitute Algorithm | 195)

在这里插入图片描述

function foundPerson(people) {
    for(let i = 0; i < people.length; i++) {
        if (people[i] === "Don") {
        	return "Don";
        }
        if (people[i] === "John") {
        	return "John";
        }
        if (people[i] === "Kent") {
        	return "Kent";
        }
    }
    return "";
}
function foundPerson(people) {
    const candidates = ["Don", "John", "Kent"];
    return people.find(p => candidates.includes(p)) || '';
}

动机

如果发现做一件事可以有更清晰的方式,就用比较清晰的方式取代复杂的方式。“重构”可以把一些复杂的东西分解为较简单的小块。

替换一个巨大且复杂的算法是非常困难的,只有先将它分解为较简单的小型函数,才能很有把握地进行算法替换工作。

做法

  • 整理一下待替换的算法,保证它已经被抽取到一个独立的函数中。
  • 先只为这个函数准备测试,以便固定它的行为。
  • 准备好另一个(替换用)算法。
  • 执行静态检查。
  • 运行测试,比对新旧算法的运行结果。如果测试通过,那就大功告成;否则,在后续测试和调试过程中,以旧算法为比较参照标准。

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

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

相关文章

HTML整站规划与规范

文章目录 命名规则命名命名书写 包含样式规范样式重置样式引入页面结构页面宽度页面高度与背景页面设计 网址图标 命名规则 命名 根据每块元素的主题、功能、页面上的位置命名&#xff0c;便于后期更改与维护。 另外&#xff1a;如果所有样式放在同一文件下&#xff0c;可以给…

计算如何剥出艺术品

背景&#xff1a; 今天给大家介绍一篇中国科学技术大学发表的论文《Computational Peeling Art Design》。论文要解决是&#xff1a;如何把球状三维物体的表面连续展开成一些艺术画面&#xff0c;要求是展开表面要占三维物体表面整个面积&#xff0c;展开表面要和艺术体形状尽可…

[CISCN 2022 初赛]online_crt

文章目录 涉及知识点代码审计解题过程 涉及知识点 CVE-2022-1292漏洞OpenSSLssrf 代码审计 app.py源码 import datetime import json import os import socket import uuid from cryptography import x509 from cryptography.hazmat.backends import default_backend from …

LeetCode(力扣)55. 跳跃游戏Python

LeetCode20. 有效的括号 题目链接代码 题目链接 https://leetcode.cn/problems/jump-game/ 代码 class Solution:def canJump(self, nums: List[int]) -> bool:if len(nums) < 1:return Truecover 0for i in range(len(nums)):if i < cover:cover max(cover, i …

【NVIDIA CUDA】2023 CUDA夏令营编程模型(四)

博主未授权任何人或组织机构转载博主任何原创文章&#xff0c;感谢各位对原创的支持&#xff01; 博主链接 本人就职于国际知名终端厂商&#xff0c;负责modem芯片研发。 在5G早期负责终端数据业务层、核心网相关的开发工作&#xff0c;目前牵头6G算力网络技术标准研究。 博客…

性能测试结果评估与展示

面向性能测试部门 对测试资产进行集中管理以及从项目或系统维度进行汇总展示是两种行之有效的管理手段。这些测试资产包括脚本、缺陷描述、测试记录、测试报告、项目需求等资料,通过对这些资料进行分类,当原有人员缺失的情况下,新接手的测试工程师能快速了解关键信息。 使…

Android RecyclerView BaseSectionQuickAdapter实现分组功能

详情网站&#xff1a;手把手教你使用BaseSectionQuickAdapter实现分组功能&#xff0c;史上最详细Adapter使用教程_basequickadapter 分组_杨阿程的博客-CSDN博客 //加入二个包implementation com.android.support:recyclerview-v7:26.0.0-beta1implementation com.github.Cym…

SAP 选择屏幕动态通过Radio Button 显示与隐藏以及控制是否必输

如何在选择屏幕上进行动态展示屏幕字段&#xff0c;并且进行必输项检查控制 1. 选择屏幕定义 SELECTION-SCREEN BEGIN OF BLOCK b1 WITH FRAME TITLE TEXT-001.SELECTION-SCREEN BEGIN OF LINE.PARAMETERS: p_r1 TYPE c RADIOBUTTON GROUP grp USER-COMMAND uc DEFAULT X. &q…

【推荐】赴日IT课程 做赴日IT我该学什么?

许多想要做赴日IT的朋友问我说&#xff0c;我都该准备什么&#xff0c;或者我该学些什么才能达到可以做赴日程序员的水平呢&#xff1f;今天我就来跟大家聊一下这个问题。要说做准备&#xff0c;你需要有全日制大专及以上的学历才能获得赴日的资格&#xff0c;如果没有我们就先…

【Linux】线程的概念

文章目录 &#x1f4d6; 前言1. 线程的引入1.1 执行流&#xff1a;1.2 线程的创建&#xff1a;1.3 线程的等待&#xff1a; 2. 查看线程2.1 链接线程库&#xff1a;2.2 ps -aL&#xff1a; 3. 页表的认识3.1 二级页表&#xff1a;3.2 页表的实际大小&#xff1a; 4. 再看线程4.…

全面深入理解TCP协议(超详细)

目录 前言 TCP协议格式 确认应答机制(ACK) 理解可靠性 确认应答的机制 16位窗口大小 缓冲区 流量控制 6个标志位 16位紧急指针 ★三次握手&#xff0c;四次挥手 如何理解连接 如何理解三次握手 如何理解四次挥手 TCP可靠性机制 确认应答机制(补充) ​编辑…

70、Spring Data JPA 的 自定义查询(全手动,自己写完整 SQL 语句)

1、方法名关键字查询&#xff08;全自动&#xff0c;既不需要提供sql语句&#xff0c;也不需要提供方法体&#xff09; 2、Query查询&#xff08;半自动&#xff1a;提供 SQL 或 JPQL 查询&#xff09; 3、自定义查询&#xff08;全手动&#xff09; ★ 自定义查询&#xff08…

前端开发中,文本单行或多行溢出使用省略号显示

1.文本单行溢出使用省略号显示 关键代码如下&#xff1a; .box1{width: 200px;height: 30px;line-height: 30px;margin: 0 auto;background-color: rgba(220, 220, 220, 0.751);/* 单行文本超出隐藏 用省略号代替 */white-space: nowrap;overflow: hidden;text-overflow: ellip…

SpringMVC系列(七)之自定义注解

目录 一. Java注解简介 1.1 Java注解分类 1.2 JDK基本注解 Override Deprecated SuppressWarnings 1.3 JDK元注解 从 Java 7 开始&#xff0c;额外添加了 3 个注解: 1.4 自定义注解 如何自定义注解&#xff1f; 二. 自定义注解示例 枚举类&#xff1a; 示例一&…

Echarts 散点图的详细配置过程

文章目录 散点图 简介配置步骤简易示例 散点图 简介 Echarts散点图是一种常用的数据可视化图表类型&#xff0c;用于展示两个或多个维度的数据分布情况。散点图通过在坐标系中绘制数据点的位置来表示数据的关系。 Echarts散点图的特点如下&#xff1a; 二维数据展示&#xff…

JAVA成员变量首字母小写,第二个字母大写报错问题(原因:Lombok与Spring冲突)

1、问题现象&#xff1a; JAVA类里定义成员变量使用首字母小写&#xff0c;第二个字母大写 Getter Setter public class BrandQueryObject extends QueryObject{private String pName; }结果页面报错&#xff0c;无法找到类型为 cn.wolfcode.ssm.query.BrandQueryObject 的对象…

【Linux】常用工具(上)

Linux 常用工具 一、Linux 软件包管理器 yum1. 软件包2. 查看软件包3. 安装/卸载软件4. yum 其他指令的功能 二、Linux 编辑器 - vim 使用1. vim 的基本概念2. vim 的基本操作&#xff08;1&#xff09;光标移动&#xff08;命令模式&#xff09;&#xff08;2&#xff09;光标…

两届 TOKEN 2049 之间,孙宇晨和波场的布局与野心

2022 年在新加坡举办的 TOKEN 2049 大会上&#xff0c;波场TRON创始人、火币全球顾问委员会成员孙宇晨作为特邀嘉宾出席&#xff0c;并曾提出“波场 TRON 下一步的发展目标是成为主流金融机构”的生态愿景&#xff0c;揭示了波场生态的全新发展方向&#xff0c;以及孙宇晨作为区…

企业架构LNMP学习笔记49

Redis数据持久化操作&#xff1a; 数据、持久化&#xff08;数据在服务或者软件重启之后不丢失&#xff09;。 如果数据只存储在内存中&#xff0c;肯定会丢失&#xff0c;实现持久化&#xff0c;就需要把数据存储在磁盘中&#xff08;hdd ssd&#xff09;。 memcached在宕机…

Linux下生成可执行程序的每一步过程以及链接库的初步认识

程序的翻译 程序在形成可执行程序之前都经历过一系列十分复杂的过程&#xff0c;也就是我们程序的翻译&#xff0c;程序的翻译经过以下阶段&#xff1a; 预处理&#xff08;进行宏替换) 编译&#xff08;生成汇编) 汇编&#xff08;生成机器可识别代码&#xff09; 连接&#…