重构大师-二-
重构大师(二)
移除对参数的赋值
问题
一些值在方法体内被赋给参数。
解决方案
使用局部变量代替参数。
之前
int discount(int inputVal, int quantity) {
if (quantity > 50) {
inputVal -= 2;
}
// ...
}
之后
int discount(int inputVal, int quantity) {
int result = inputVal;
if (quantity > 50) {
result -= 2;
}
// ...
}
之前
int Discount(int inputVal, int quantity)
{
if (quantity > 50)
{
inputVal -= 2;
}
// ...
}
之后
int Discount(int inputVal, int quantity)
{
int result = inputVal;
if (quantity > 50)
{
result -= 2;
}
// ...
}
之前
function discount($inputVal, $quantity) {
if ($quantity > 50) {
$inputVal -= 2;
}
...
之后
function discount($inputVal, $quantity) {
$result = $inputVal;
if ($quantity > 50) {
$result -= 2;
}
...
之前
def discount(inputVal, quantity):
if quantity > 50:
inputVal -= 2
# ...
之后
def discount(inputVal, quantity):
result = inputVal
if quantity > 50:
result -= 2
# ...
之前
discount(inputVal: number, quantity: number): number {
if (quantity > 50) {
inputVal -= 2;
}
// ...
}
之后
discount(inputVal: number, quantity: number): number {
let result = inputVal;
if (quantity > 50) {
result -= 2;
}
// ...
}
为什么要重构
这个重构的原因与拆分临时变量相同,但在这种情况下我们处理的是参数,而不是局部变量。
首先,如果参数通过引用传递,那么在方法内部更改参数值后,该值会传递给请求调用此方法的参数。这个过程往往是偶然发生的,导致不幸的后果。即使在你的编程语言中通常是通过值(而不是通过引用)传递参数,这种编码怪癖可能会让不习惯的人感到困惑。
其次,将不同值多次赋给单一参数,使你很难知道在任何特定时间点参数中应该包含什么数据。如果参数及其内容有文档记录,但实际值可能与方法内部的预期不同,问题会更加严重。
好处
-
程序的每个元素应只负责一件事。这使得今后的代码维护变得更加容易,因为你可以安全地替换代码而不会产生副作用。
-
这个重构有助于将重复代码提取到单独的方法。
如何重构
-
创建一个局部变量并赋予参数的初始值。
-
在此行之后的所有方法代码中,将参数替换为新的局部变量。
</images/refactoring/banners/tired-of-reading-banner-1x.mp4?id=7fa8f9682afda143c2a491c6ab1c1e56>
</images/refactoring/banners/tired-of-reading-banner.png?id=1721d160ff9c84cbf8912f5d282e2bb4>
你的浏览器不支持 HTML 视频。
厌倦阅读?
难怪,这里所有文本的阅读需要 7 小时。
尝试我们的交互式重构课程。它提供了一种不那么乏味的学习新知识的方法。
我们来看……
用方法对象替换方法
问题
你有一个很长的方法,其中的局部变量交织在一起,以至于无法应用提取方法。
解决方案
将方法转变为一个单独的类,以便局部变量成为类的字段。然后你可以在同一个类中将方法拆分为几个方法。
之前
class Order {
// ...
public double price() {
double primaryBasePrice;
double secondaryBasePrice;
double tertiaryBasePrice;
// Perform long computation.
}
}
之后
class Order {
// ...
public double price() {
return new PriceCalculator(this).compute();
}
}
class PriceCalculator {
private double primaryBasePrice;
private double secondaryBasePrice;
private double tertiaryBasePrice;
public PriceCalculator(Order order) {
// Copy relevant information from the
// order object.
}
public double compute() {
// Perform long computation.
}
}
之前
public class Order
{
// ...
public double Price()
{
double primaryBasePrice;
double secondaryBasePrice;
double tertiaryBasePrice;
// Perform long computation.
}
}
之后
public class Order
{
// ...
public double Price()
{
return new PriceCalculator(this).Compute();
}
}
public class PriceCalculator
{
private double primaryBasePrice;
private double secondaryBasePrice;
private double tertiaryBasePrice;
public PriceCalculator(Order order)
{
// Copy relevant information from the
// order object.
}
public double Compute()
{
// Perform long computation.
}
}
之前
class Order {
// ...
public function price() {
$primaryBasePrice = 10;
$secondaryBasePrice = 20;
$tertiaryBasePrice = 30;
// Perform long computation.
}
}
之后
class Order {
// ...
public function price() {
return (new PriceCalculator($this))->compute();
}
}
class PriceCalculator {
private $primaryBasePrice;
private $secondaryBasePrice;
private $tertiaryBasePrice;
public function __construct(Order $order) {
// Copy relevant information from the
// order object.
}
public function compute() {
// Perform long computation.
}
}
之前
class Order:
# ...
def price(self):
primaryBasePrice = 0
secondaryBasePrice = 0
tertiaryBasePrice = 0
# Perform long computation.
之后
class Order:
# ...
def price(self):
return PriceCalculator(self).compute()
class PriceCalculator:
def __init__(self, order):
self._primaryBasePrice = 0
self._secondaryBasePrice = 0
self._tertiaryBasePrice = 0
# Copy relevant information from the
# order object.
def compute(self):
# Perform long computation.
之前
class Order {
// ...
price(): number {
let primaryBasePrice;
let secondaryBasePrice;
let tertiaryBasePrice;
// Perform long computation.
}
}
之后
class Order {
// ...
price(): number {
return new PriceCalculator(this).compute();
}
}
class PriceCalculator {
private _primaryBasePrice: number;
private _secondaryBasePrice: number;
private _tertiaryBasePrice: number;
constructor(order: Order) {
// Copy relevant information from the
// order object.
}
compute(): number {
// Perform long computation.
}
}
为什么重构
一个方法太长,无法分离,因为局部变量交织在一起,很难彼此隔离。
第一步是将整个方法隔离到一个单独的类中,并将其局部变量转换为类的字段。
首先,这可以在类级别上隔离问题。其次,它为将一个庞大且笨重的方法拆分成几个小方法铺平了道路,而这些小方法与原始类的目的并不相符。
好处
- 将一个长方法隔离到自己的类中,可以防止方法膨胀。同时,这也允许在类内部将其拆分为子方法,而不会用工具方法污染原始类。
缺点
- 新增一个类,增加了程序的整体复杂性。
如何重构
-
创建一个新类。根据你要重构的方法的目的命名它。
-
在新类中创建一个私有字段,用于存储对之前方法所在类的实例的引用。如果需要,可以用来从原始类中获取所需数据。
-
为方法中的每个局部变量创建一个单独的私有字段。
-
创建一个构造函数,接受方法中所有局部变量的值作为参数,并初始化相应的私有字段。
-
声明主方法并将原始方法的代码复制到其中,用私有字段替换局部变量。
-
通过创建一个方法对象并调用其主方法,替换原始类中原始方法的主体。
</images/refactoring/banners/tired-of-reading-banner-1x.mp4?id=7fa8f9682afda143c2a491c6ab1c1e56>
</images/refactoring/banners/tired-of-reading-banner.png?id=1721d160ff9c84cbf8912f5d282e2bb4>
你的浏览器不支持 HTML 视频。
读累了吗?
难怪,阅读我们这里所有的文本需要 7 小时。
尝试我们的交互式重构课程。它提供了一种不那么乏味的学习新知识的方法。
我们来看看…
替代算法
问题
所以你想用一个新的算法替换现有的算法吗?
解决方案
用新算法替换实现算法的方法主体。
之前
String foundPerson(String[] people){
for (int i = 0; i < people.length; i++) {
if (people[i].equals("Don")){
return "Don";
}
if (people[i].equals("John")){
return "John";
}
if (people[i].equals("Kent")){
return "Kent";
}
}
return "";
}
之后
String foundPerson(String[] people){
List candidates =
Arrays.asList(new String[] {"Don", "John", "Kent"});
for (int i=0; i < people.length; i++) {
if (candidates.contains(people[i])) {
return people[i];
}
}
return "";
}
之前
string FoundPerson(string[] people)
{
for (int i = 0; i < people.Length; i++)
{
if (people[i].Equals("Don"))
{
return "Don";
}
if (people[i].Equals("John"))
{
return "John";
}
if (people[i].Equals("Kent"))
{
return "Kent";
}
}
return String.Empty;
}
之前
string FoundPerson(string[] people)
{
List<string> candidates = new List<string>() {"Don", "John", "Kent"};
for (int i = 0; i < people.Length; i++)
{
if (candidates.Contains(people[i]))
{
return people[i];
}
}
return String.Empty;
}
之前
function foundPerson(array $people){
for ($i = 0; $i < count($people); $i++) {
if ($people[$i] === "Don") {
return "Don";
}
if ($people[$i] === "John") {
return "John";
}
if ($people[$i] === "Kent") {
return "Kent";
}
}
return "";
}
之后
function foundPerson(array $people){
foreach (["Don", "John", "Kent"] as $needle) {
$id = array_search($needle, $people, true);
if ($id !== false) {
return $people[$id];
}
}
return "";
}
之前
def foundPerson(people):
for i in range(len(people)):
if people[i] == "Don":
return "Don"
if people[i] == "John":
return "John"
if people[i] == "Kent":
return "Kent"
return ""
之后
def foundPerson(people):
candidates = ["Don", "John", "Kent"]
return people if people in candidates else ""
之前
foundPerson(people: string[]): string{
for (let person of people) {
if (person.equals("Don")){
return "Don";
}
if (person.equals("John")){
return "John";
}
if (person.equals("Kent")){
return "Kent";
}
}
return "";
}
之后
foundPerson(people: string[]): string{
let candidates = ["Don", "John", "Kent"];
for (let person of people) {
if (candidates.includes(person)) {
return person;
}
}
return "";
}
为什么重构
-
渐进式重构并不是改进程序的唯一方法。有时一个方法存在太多问题,以至于拆除该方法并重新开始更为简单。而且也许你找到了一种更简单、更高效的算法。如果是这样,你应该简单地用新算法替换旧算法。
-
随着时间的推移,你的算法可能会被纳入一个知名的库或框架中,而你想要摆脱独立实现,以简化维护。
-
你的程序的需求可能会发生重大变化,以至于你现有的算法无法用于该任务。
如何重构
-
确保你已尽可能简化现有算法。使用提取方法将不重要的代码移动到其他方法中。你算法中的移动部分越少,更容易替换。
-
在一个新方法中创建你的新算法。用新算法替换旧算法,然后开始测试程序。
-
如果结果不匹配,请返回旧实现并比较结果。找出差异的原因。虽然原因往往是旧算法中的错误,但更可能是新算法中的某些部分未能正常工作。
-
当所有测试成功完成后,彻底删除旧算法!
</images/refactoring/banners/tired-of-reading-banner-1x.mp4?id=7fa8f9682afda143c2a491c6ab1c1e56>
</images/refactoring/banners/tired-of-reading-banner.png?id=1721d160ff9c84cbf8912f5d282e2bb4>
你的浏览器不支持 HTML 视频。
厌倦阅读了吗?
难怪,阅读我们这里所有的文本需要 7 小时。
尝试我们的交互式重构课程。它提供了一种不那么乏味的学习新知识的方法。
让我们看看……
在对象之间移动特性
原文:
refactoringguru.cn/refactoring/techniques/moving-features-between-objects
即使你在不同类之间分配功能的方式不尽完美,仍然有希望。
这些重构技术展示了如何安全地在类之间移动功能,创建新类,并隐藏实现细节以避免公开访问。
移动方法
问题: 一个方法在另一个类中的使用频率超过其自身类中的使用频率。
解决方案: 在使用该方法最多的类中创建一个新方法,然后将代码从旧方法移动到那里。将原方法的代码转变为对另一个类中新方法的引用,或者完全删除它。
移动字段
问题: 一个字段在另一个类中的使用频率超过其自身类中的使用频率。
解决方案: 在新类中创建一个字段,并将所有使用旧字段的用户重定向到它。
提取类
问题: 当一个类完成两个类的工作时,会导致尴尬。
解决方案: 相反,创建一个新类,并将负责相关功能的字段和方法放入其中。
内联类
问题: 一个类几乎什么都不做,并且不负责任何事情,也没有计划额外的职责。
解决方案: 将类中的所有特性移到另一个类中。
隐藏委托
问题: 客户从对象А的字段或方法获取对象 B。然后客户调用对象 B 的方法。
解决方案: 在类 A 中创建一个新方法,将调用委托给对象 B。现在客户端对类 B 没有了解或依赖。
去掉中介
问题: 一个类有太多方法,仅仅将请求委托给其他对象。
解决方案: 删除这些方法,并强制客户端直接调用最终方法。
引入外部方法
问题: 一个工具类不包含你需要的方法,而你无法将该方法添加到类中。
解决方案: 将该方法添加到客户端类,并将工具类的对象作为参数传递给它。
引入本地扩展
问题: 一个工具类不包含你需要的一些方法。但你无法将这些方法添加到类中。
解决方案: 创建一个包含这些方法的新类,并使其成为工具类的子类或包装类。
移动方法
问题
一个方法在另一个类中使用得比在它自己类中多。
解决方案
在使用该方法最多的类中创建一个新方法,然后将旧方法中的代码移动到那里。将原方法的代码转换为对另一个类中新方法的引用,或者完全删除它。
之前!移动方法 - 之前之后!移动方法 - 之后
为什么重构
-
你想把一个方法移动到一个包含该方法使用的大部分数据的类中。这使得类的内部更加一致。
-
你想移动一个方法,以减少或消除调用该方法的类对其所在类的依赖。如果调用类已经依赖于你计划将方法移动到的类,这可能会很有用。这减少了类之间的依赖性。
如何重构
-
验证旧方法在其类中使用的所有特性。将它们一起移动可能是个好主意。一般来说,如果某个特性仅被考虑中的方法使用,你肯定应该将其移动。如果该特性也被其他方法使用,你也应该同时移动这些方法。有时移动大量方法比在不同类之间建立关系要容易得多。
确保该方法没有在超类和子类中声明。如果是这样,你要么必须停止移动,要么需要在接收类中实现一种多态,以确保方法在捐赠类之间的不同功能。
-
在接收类中声明新方法。你可能想给这个方法一个更适合它的新名称。
-
决定你将如何引用接收类。你可能已经有一个返回适当对象的字段或方法,但如果没有,你需要写一个新方法或字段来存储接收类的对象。
现在你有了引用接收对象的方法以及它的类中的新方法。掌握这些后,你可以将旧方法转换为对新方法的引用。
-
看看:你能完全删除旧方法吗?如果可以,请在所有使用旧方法的地方放置对新方法的引用。
</images/refactoring/banners/tired-of-reading-banner-1x.mp4?id=7fa8f9682afda143c2a491c6ab1c1e56>
</images/refactoring/banners/tired-of-reading-banner.png?id=1721d160ff9c84cbf8912f5d282e2bb4>
你的浏览器不支持 HTML 视频。
厌倦阅读了吗?
难怪,阅读我们这里所有文本需要 7 小时。
尝试我们的交互式重构课程。这提供了一种更轻松的学习新知识的方法。
让我们看看…
移动字段
问题
一个字段在另一个类中的使用频率高于在其自身类中的使用频率。
解决方案
在新类中创建一个字段,并将所有旧字段的使用者重定向到该字段。
之前!移动字段 - 之前之后!移动字段 - 之后
为什么重构
字段通常作为提取类技术的一部分进行移动。决定将字段保留在哪个类中可能很困难。我们的经验法则是:将字段放在使用它的方法相同的位置(或者其他大多数方法的位置)。
这个规则在字段简单地位于错误位置时也会有所帮助。
如何重构
-
如果字段是公开的,将字段设为私有并提供公共访问方法将使重构变得容易得多(为此,你可以使用封装字段)。
-
在接收类中创建相同的字段及其访问方法。
-
决定如何引用接收类。你可能已经有一个返回适当对象的字段或方法;如果没有,你需要编写一个新的方法或字段来存储接收类的对象。
-
用接收类中的相应方法替换所有对旧字段的引用。如果字段不是私有的,请在超类和子类中处理此事。
-
删除原类中的字段。
</images/refactoring/banners/tired-of-reading-banner-1x.mp4?id=7fa8f9682afda143c2a491c6ab1c1e56>
</images/refactoring/banners/tired-of-reading-banner.png?id=1721d160ff9c84cbf8912f5d282e2bb4>
你的浏览器不支持 HTML 视频。
厌倦阅读了吗?
难怪,阅读我们这里所有文本需要 7 小时。
尝试我们的交互式重构课程。它提供了一种更不乏味的学习新知识的方法。
我们来看一下…
提取类
问题
当一个类完成两个类的工作时,会产生尴尬。
解决方案
相反,创建一个新类,将负责相关功能的字段和方法放入其中。
前后
为什么重构
类通常一开始是清晰且易于理解的。它们各自做好自己的工作,而不会干扰其他类的工作。但随着程序的扩展,方法和字段被添加……最终,一些类承担的责任超出了最初的设想。
好处
-
这个重构方法将帮助维护对单一责任原则的遵循。你的类的代码将更加明显和易于理解。
-
单一责任的类更加可靠,并且对变化更具容忍性。例如,假设你有一个负责十个不同任务的类。当你改变这个类以使其在某一方面更好时,你可能会破坏它在其他九个方面的功能。
缺点
- 如果你在使用这个重构技术时“过度”了,你将不得不求助于内联类。
如何重构
在开始之前,决定你想如何拆分类的责任。
-
创建一个新类以包含相关功能。
-
在旧类和新类之间创建关系。最佳情况下,这种关系是单向的;这使得重用第二个类没有任何问题。尽管如此,如果你认为有必要建立双向关系,随时可以设置。
-
使用移动字段和移动方法来处理你决定移动到新类的每个字段和方法。对于方法,从私有方法开始,以减少产生大量错误的风险。尽量逐步迁移,并在每次移动后测试结果,以避免最后堆积大量的错误修复。
完成移动后,再次查看结果类。一个责任已更改的旧类可以重命名以提高清晰度。再次检查是否可以消除任何双向类关系。
-
还要考虑新类的外部可访问性。你可以通过将其设为私有,完全隐藏类,以通过旧类的字段来管理它。或者,你可以将其设为公共,让客户端直接更改值。你的决策取决于在对新类中的值进行意外直接更改时,对旧类行为的安全性。
</images/refactoring/banners/tired-of-reading-banner-1x.mp4?id=7fa8f9682afda143c2a491c6ab1c1e56>
</images/refactoring/banners/tired-of-reading-banner.png?id=1721d160ff9c84cbf8912f5d282e2bb4>
您的浏览器不支持 HTML 视频。
读得累了吗?
不奇怪,阅读我们这里所有文本需要 7 小时。
尝试我们的重构互动课程,它提供了一种更轻松的学习新知识的方法。
[让我们看看…
内联类
问题
一个类几乎没有做任何事情,也没有负责任何事情,且没有计划额外的职责。
解决方案
将所有特性从一个类移动到另一个类。
之前之后
为什么要重构
- 通常在一个类的特性被“移植”到其他类后,这种技术是必要的,这样原来的类几乎无事可做。
好处
- 消除不必要的类可以释放计算机的操作内存——以及你头脑中的带宽。
如何重构
-
在接收类中,创建捐赠类中存在的公共字段和方法。方法应引用捐赠类的等效方法。
-
将所有对捐赠类的引用替换为对接收类的字段和方法的引用。
-
现在测试程序,确保没有添加错误。如果测试显示一切正常,开始使用移动方法和移动字段将所有功能完全移植到接收类。继续进行,直到原始类完全为空。
-
删除原始类。
</images/refactoring/banners/tired-of-reading-banner-1x.mp4?id=7fa8f9682afda143c2a491c6ab1c1e56>
</images/refactoring/banners/tired-of-reading-banner.png?id=1721d160ff9c84cbf8912f5d282e2bb4>
您的浏览器不支持 HTML 视频。
读得累了吗?
难怪,阅读这里的所有文本需要 7 小时。
尝试我们的互动重构课程。它提供了一种不那么乏味的学习新知识的方法。
让我们看看…
隐藏委托
问题
客户端从对象 A 的字段或方法中获取对象 B。然后客户端调用对象 B 的方法。
解决方案
在类 A 中创建一个新方法,将调用委托给对象 B。现在客户端对类 B 并不了解,也不依赖于类 B。
在之前!在之后!
为什么重构
首先,让我们看看术语:
-
服务器是客户端可以直接访问的对象。
-
委托是包含客户端所需功能的最终对象。
当客户端从另一个对象请求一个对象时,就会出现调用链,然后第二个对象请求另一个对象,以此类推。这些调用序列使客户端参与到类结构的导航中。这些相互关系的任何变化都需要在客户端进行相应的更改。
优势
- 将委托隐藏于客户端。客户端代码越少需要了解对象之间关系的细节,对程序的修改就越容易。
缺点
- 如果你需要创建过多的委托方法,服务器类有可能成为一个不必要的中介,导致过多的中介者。
如何重构
-
为每个被客户端调用的委托类方法,在服务器类中创建一个方法,将调用委托给委托类。
-
更改客户端代码,使其调用服务器类的方法。
-
如果你的更改使客户端不再需要委托类,你可以从服务器类中移除对委托类的访问方法(最初用于获取委托类的方法)。
</images/refactoring/banners/tired-of-reading-banner-1x.mp4?id=7fa8f9682afda143c2a491c6ab1c1e56>
</images/refactoring/banners/tired-of-reading-banner.png?id=1721d160ff9c84cbf8912f5d282e2bb4>
您的浏览器不支持 HTML 视频。
厌倦阅读了吗?
难怪,阅读我们这里所有的文本需要 7 小时。
尝试我们的交互式重构课程。它提供了一种更轻松的学习新知识的方法。
让我们看看…
去掉中介
问题
一个类有太多方法只是简单地委托给其他对象。
解决方案
删除这些方法,并强迫客户端直接调用最终方法。
之前之后
为什么要重构
为了描述这个技术,我们将使用隐藏委托中的术语:
-
服务器是客户端可以直接访问的对象。
-
委托是包含客户端所需功能的最终对象。
有两种类型的问题:
-
服务器类本身不执行任何操作,仅仅增加了不必要的复杂性。在这种情况下,考虑一下这个类是否真的需要。
-
每当向委托添加新功能时,您需要在服务器类中为其创建一个委托方法。如果进行了大量更改,这将相当繁琐。
如何重构
-
创建一个获取委托类对象的 getter,以便从服务器类对象访问。
-
在服务器类中用对委托类方法的直接调用替换对委托方法的调用。
</images/refactoring/banners/tired-of-reading-banner-1x.mp4?id=7fa8f9682afda143c2a491c6ab1c1e56>
</images/refactoring/banners/tired-of-reading-banner.png?id=1721d160ff9c84cbf8912f5d282e2bb4>
您的浏览器不支持 HTML 视频。
读累了吗?
不奇怪,阅读我们这里的所有文本需要 7 个小时。
尝试我们的互动重构课程。它提供了一种不那么乏味的学习新知识的方法。
让我们看看……
引入外部方法
问题
实用类不包含你需要的方法,你不能将方法添加到该类中。
解决方案
将方法添加到客户端类中,并将实用类的对象作为参数传递给它。
之前
class Report {
// ...
void sendReport() {
Date nextDay = new Date(previousEnd.getYear(),
previousEnd.getMonth(), previousEnd.getDate() + 1);
// ...
}
}
之后
class Report {
// ...
void sendReport() {
Date newStart = nextDay(previousEnd);
// ...
}
private static Date nextDay(Date arg) {
return new Date(arg.getYear(), arg.getMonth(), arg.getDate() + 1);
}
}
之前
class Report
{
// ...
void SendReport()
{
DateTime nextDay = previousEnd.AddDays(1);
// ...
}
}
之后
class Report
{
// ...
void SendReport()
{
DateTime nextDay = NextDay(previousEnd);
// ...
}
private static DateTime NextDay(DateTime date)
{
return date.AddDays(1);
}
}
之前
class Report {
// ...
public function sendReport() {
$previousDate = clone $this->previousDate;
$paymentDate = $previousDate->modify("+7 days");
// ...
}
}
之后
class Report {
// ...
public function sendReport() {
$paymentDate = self::nextWeek($this->previousDate);
// ...
}
/**
* Foreign method. Should be in Date.
*/
private static function nextWeek(DateTime $arg) {
$previousDate = clone $arg;
return $previousDate->modify("+7 days");
}
}
之前
class Report:
# ...
def sendReport(self):
nextDay = Date(self.previousEnd.getYear(),
self.previousEnd.getMonth(), self.previousEnd.getDate() + 1)
# ...
之后
class Report:
# ...
def sendReport(self):
newStart = self._nextDay(self.previousEnd)
# ...
def _nextDay(self, arg):
return Date(arg.getYear(), arg.getMonth(), arg.getDate() + 1)
之前
class Report {
// ...
sendReport(): void {
let nextDay: Date = new Date(previousEnd.getYear(),
previousEnd.getMonth(), previousEnd.getDate() + 1);
// ...
}
}
之后
class Report {
// ...
sendReport() {
let newStart: Date = nextDay(previousEnd);
// ...
}
private static nextDay(arg: Date): Date {
return new Date(arg.getFullYear(), arg.getMonth(), arg.getDate() + 1);
}
}
为什么要重构
你有代码使用某个类的数据和方法。你意识到代码在该类的新方法中看起来和工作得会更好。但你无法将方法添加到类中,因为,例如,该类位于第三方库中。
当你想将代码移到方法中时,如果代码在程序的不同地方重复多次,这种重构会带来很大的回报。
由于你将实用类的对象传递给新方法的参数,你可以访问其所有字段。在方法内部,你可以做几乎所有你想做的事情,就像该方法是实用类的一部分一样。
好处
- 消除代码重复。如果你的代码在多个地方重复,可以用方法调用替换这些代码片段。即使考虑到外部方法位于次优位置,这种做法也优于重复。
缺点
- 在客户端类中有实用类的方法并不总是对维护代码的人来说是清晰的。如果该方法可以在其他类中使用,你可以通过为实用类创建一个包装器并将方法放在那里来获益。当有多个这样的实用方法时,这也会很有帮助。引入本地扩展可以帮助解决这个问题。
如何重构
-
在客户端类中创建一个新方法。
-
在此方法中创建一个参数,以传递实用类的对象。如果可以从客户端类中获得该对象,则不必创建这样的参数。
-
将相关代码片段提取到此方法中,并用方法调用替换它们。
-
一定要在方法的注释中保留外部方法标签,并建议如果将来可能的话,将该方法放入实用类中。这将使未来维护软件的人更容易理解该方法为何位于此特定类中。
</images/refactoring/banners/tired-of-reading-banner-1x.mp4?id=7fa8f9682afda143c2a491c6ab1c1e56>
</images/refactoring/banners/tired-of-reading-banner.png?id=1721d160ff9c84cbf8912f5d282e2bb4>
你的浏览器不支持 HTML 视频。
厌倦阅读了吗?
不奇怪,阅读我们这里所有的文本需要 7 小时。
尝试我们的互动重构课程。它提供了一种不那么乏味的学习新知识的方法。
让我们看看……
引入本地扩展
问题
一个工具类不包含你需要的一些方法。但你无法将这些方法添加到类中。
解决方案
创建一个包含方法的新类,并使其成为工具类的子类或包装类。
前后
为什么重构
你正在使用的类没有你需要的方法。更糟糕的是,你无法添加这些方法(例如,因为这些类在第三方库中)。有两种解决方案:
-
从相关类创建一个子类,包含方法并从父类继承其他一切。这样更简单,但有时会受到工具类本身的阻碍(由于
final
)。 -
创建一个包装类,包含所有新方法,并在其他地方委托给工具类的相关对象。此方法工作量较大,因为你不仅需要代码来维护包装器与工具对象之间的关系,还需要大量简单的委托方法,以模拟工具类的公共接口。
好处
- 通过将附加方法移到单独的扩展类(包装类或子类)中,可以避免使客户端类充满不合适的代码。程序组件更加连贯,也更易于重用。
如何重构
-
创建一个新的扩展类:
-
选项 A:使其成为工具类的子类。
-
选项 B:如果你决定创建一个包装器,请在其中创建一个字段以存储将进行委托的工具类对象。使用此选项时,你还需要创建重复工具类公共方法的简单委托方法。
-
-
创建一个构造函数,使用工具类构造函数的参数。
-
还可以创建一个替代的“转换”构造函数,仅将原始类的对象作为参数。这将帮助将扩展替代原始类的对象。
-
在类中创建新的扩展方法。将其他类的外部方法移动到此类中,或者如果其功能已在扩展中存在,则删除外部方法。
-
在需要其功能的地方,用新的扩展类替换对工具类的使用。
</images/refactoring/banners/tired-of-reading-banner-1x.mp4?id=7fa8f9682afda143c2a491c6ab1c1e56>
</images/refactoring/banners/tired-of-reading-banner.png?id=1721d160ff9c84cbf8912f5d282e2bb4>
你的浏览器不支持 HTML 视频。
厌倦阅读?
难怪,阅读我们这里所有文本需要 7 小时。
尝试我们的互动重构课程。它提供了一种更轻松的学习新知识的方法。
让我们看看…
组织数据
原文:
refactoringguru.cn/refactoring/techniques/organizing-data
这些重构技术有助于数据处理,用丰富的类功能替换原始类型。
另一个重要结果是理顺类之间的关联,使类更加可移植和可重用。
自我封装字段
问题: 你直接访问类内部的私有字段。
解决方案: 为字段创建一个 getter 和 setter,仅通过它们访问字段。
用对象替换数据值
问题: 一个类(或一组类)包含一个数据字段。该字段具有自己的行为和相关数据。
解决方案: 创建一个新类,将旧字段及其行为放入该类中,并在原始类中存储该类的对象。
将值更改为引用
问题: 所以你有许多相同实例的单一类,需要用一个对象替换它们。
解决方案: 将相同的对象转换为单个引用对象。
将引用更改为值
问题: 你有一个引用对象,它太小且不常改变,以至于不值得管理其生命周期。
解决方案: 将其转换为值对象。
用对象替换数组
问题: 你有一个包含各种类型数据的数组。
解决方案: 用一个对象替换数组,该对象将为每个元素具有单独的字段。
重复观察数据
问题: 域数据是否存储在负责 GUI 的类中?
解决方案: 将数据分离到不同的类中,确保领域类与 GUI 之间的连接和同步是个好主意。
将单向关联更改为双向
问题: 你有两个类,它们各自需要使用对方的功能,但它们之间的关联仅是单向的。
解决方案: 将缺失的关联添加到需要它的类中。
将双向关联更改为单向
问题: 你有两个类之间的双向关联,但其中一个类并不使用另一个的功能。
解决方案: 删除未使用的关联。
用符号常量替换魔法数字
问题: 你的代码使用一个有特定含义的数字。
解决方案: 用一个具有可读名称的常量替换这个数字,以解释数字的含义。
封装字段
问题: 你有一个公共字段。
解决方案: 将字段设为私有并为其创建访问方法。
封装集合
问题: 一个类包含一个集合字段,并有简单的 getter 和 setter 用于操作该集合。
解决方案: 使 getter 返回的值为只读,并创建用于添加/删除集合元素的方法。
用类替换类型代码
问题: 一个类有一个包含类型代码的字段。此类型的值未在操作条件中使用,并且不会影响程序的行为。
解决方案: 创建一个新类,使用其对象代替类型代码值。
用子类替换类型代码
问题: 你有一个编码类型直接影响程序行为(此字段的值在条件中触发各种代码)。
解决方案: 为编码类型的每个值创建子类。然后将相关行为从原始类提取到这些子类中。用多态替换控制流代码。
用状态/策略替换类型代码
问题: 你有一个编码类型影响行为,但你无法使用子类来摆脱它。
解决方案: 用状态对象替换类型代码。如果需要用类型代码替换字段值,则“插入”另一个状态对象。
用字段替换子类
问题: 你有子类仅在其(常量返回)方法上有所不同。
解决方案: 用父类中的字段替换方法,并删除子类。
自我封装字段
原文:
refactoringguru.cn/self-encapsulate-field
自我封装与普通的封装字段不同:这里给出的重构技术是在私有字段上执行的。
问题
你在类内部直接访问私有字段。
解决方案
创建一个字段的 getter 和 setter,并仅使用它们来访问该字段。
之前
class Range {
private int low, high;
boolean includes(int arg) {
return arg >= low && arg <= high;
}
}
之后
class Range {
private int low, high;
boolean includes(int arg) {
return arg >= getLow() && arg <= getHigh();
}
int getLow() {
return low;
}
int getHigh() {
return high;
}
}
之前
class Range
{
private int low, high;
bool Includes(int arg)
{
return arg >= low && arg <= high;
}
}
之后
class Range
{
private int low, high;
int Low {
get { return low; }
}
int High {
get { return high; }
}
bool Includes(int arg)
{
return arg >= Low && arg <= High;
}
}
之前
private $low;
private $high;
function includes($arg) {
return $arg >= $this->low && $arg <= $this->high;
}
之后
private $low;
private $high;
function includes($arg) {
return $arg >= $this->getLow() && $arg <= $this->getHigh();
}
function getLow() {
return $this->low;
}
function getHigh() {
return $this->high;
}
之前
class Range {
private low: number
private high: number;
includes(arg: number): boolean {
return arg >= low && arg <= high;
}
}
之后
class Range {
private low: number
private high: number;
includes(arg: number): boolean {
return arg >= getLow() && arg <= getHigh();
}
getLow(): number {
return low;
}
getHigh(): number {
return high;
}
}
为什么重构
有时直接在类内部访问私有字段根本不够灵活。你希望能够在首次查询时初始化字段值,或者在字段的新值分配时对其执行某些操作,或者在子类中以各种方式做到这一点。
优势
-
间接访问字段是通过访问方法(getter 和 setter)对字段进行操作。这种方法比直接访问字段灵活得多。
-
首先,当字段中的数据被设置或接收时,你可以执行复杂的操作。惰性初始化和字段值验证可以很容易地在字段的 getter 和 setter 中实现。
-
第二,更重要的是,你可以在子类中重新定义 getter 和 setter。
-
-
你可以选择不为字段实现 setter。字段值将仅在构造函数中指定,从而使字段在整个对象生命周期内不可更改。
缺点
- 当使用直接访问字段时,代码看起来更简单且更具表现力,尽管灵活性降低。
如何重构
-
为该字段创建一个 getter(和可选的 setter)。它们应为
protected
或public
。 -
查找所有对字段的直接调用,并将它们替换为 getter 和 setter 调用。
</images/refactoring/banners/tired-of-reading-banner-1x.mp4?id=7fa8f9682afda143c2a491c6ab1c1e56>
</images/refactoring/banners/tired-of-reading-banner.png?id=1721d160ff9c84cbf8912f5d282e2bb4>
你的浏览器不支持 HTML 视频。
读腻了吗?
难怪,阅读我们这里的所有文本需要 7 小时。
尝试我们的互动重构课程。它提供了一种更轻松的学习新知识的方法。
让我们看看…
用对象替换数据值
问题
一个类(或一组类)包含一个数据字段。该字段有自己的行为和相关数据。
解决方案
创建一个新类,将旧字段及其行为放入该类中,并在原始类中存储该类的对象。
之前之后
为什么重构
这种重构基本上是提取类的一个特例。不同之处在于重构的原因。
在提取类中,我们有一个负责不同事务的单一类,我们希望将其责任拆分开来。
用对象替换数据值时,我们有一个原始字段(数字、字符串等),由于程序的发展,这些字段不再简单,现在有了相关的数据和行为。一方面,这些字段本身并不可怕。然而,这些字段和行为的组合可能在多个类中同时存在,造成重复代码。
因此,为此我们创建一个新类,并将字段及相关的数据和行为转移到该类中。
好处
- 改善类内部的关联性。数据和相关行为都在一个类内。
如何重构
在开始重构之前,查看是否有直接引用该字段的地方。如果有,请使用自我封装字段将其隐藏在原始类中。
-
创建一个新类,并将字段及相关的 getter 复制到该类中。此外,创建一个接受字段简单值的构造函数。该类不会有 setter,因为每次发送给原始类的新字段值都会创建一个新的值对象。
-
在原始类中,将字段类型更改为新类。
-
在原始类的 getter 中,调用关联对象的 getter。
-
在 setter 中,创建一个新的值对象。如果之前在构造函数中为字段设置了初始值,可能还需要在构造函数中创建一个新对象。
下一步
应用这种重构技术后,明智的做法是在包含对象的字段上应用将值更改为引用。这允许存储一个与值对应的单一对象的引用,而不是为同一个值存储多个对象。
通常,当你希望一个对象负责一个现实世界的对象(如用户、订单、文档等)时,需要采用这种方法。同时,这种方法对日期、金钱、范围等对象并不实用。
</images/refactoring/banners/tired-of-reading-banner-1x.mp4?id=7fa8f9682afda143c2a491c6ab1c1e56>
</images/refactoring/banners/tired-of-reading-banner.png?id=1721d160ff9c84cbf8912f5d282e2bb4>
您的浏览器不支持 HTML 视频。
厌倦阅读了吗?
不奇怪,我们这里的所有文本阅读需要 7 小时。
尝试我们的交互式重构课程。它提供了一种不那么乏味的学习新知识的方法。
我们来看看…
将值更改为引用
问题
所以你有很多相同实例的单一类,需要用一个对象来替换。
解决方案
将相同的对象转换为一个单一的引用对象。
之前!将值更改为引用 - 之前之后!将值更改为引用 - 之后
为什么重构
在许多系统中,对象可以被分类为值或引用。
-
引用:当一个现实世界的对象只对应程序中的一个对象时。引用通常是用户/订单/产品等对象。
-
值:一个现实世界的对象对应程序中的多个对象。这些对象可以是日期、电话号码、地址、颜色等。
引用与值的选择并不总是明确的。有时有一个简单的值,包含少量不变的数据。然后就需要添加可变数据,并在每次访问对象时传递这些更改。在这种情况下,就需要将其转换为引用。
好处
- 一个对象包含有关特定实体的所有最新信息。如果程序中的一个部分更改了该对象,这些更改可以从使用该对象的程序的其他部分访问。
缺点
- 引用的实现要复杂得多。
如何重构
-
在生成引用的类上使用用工厂方法替换构造函数。
-
确定哪个对象将负责提供对引用的访问。你不再需要创建一个新对象,而是需要从存储对象或静态字典字段中获取它。
-
确定引用是提前创建还是根据需要动态创建。如果对象是提前创建的,请确保在使用之前加载它们。
-
更改工厂方法,使其返回一个引用。如果对象是提前创建的,请决定在请求不存在的对象时如何处理错误。你可能还需要使用重命名方法来通知该方法仅返回现有对象。
</images/refactoring/banners/tired-of-reading-banner-1x.mp4?id=7fa8f9682afda143c2a491c6ab1c1e56>
</images/refactoring/banners/tired-of-reading-banner.png?id=1721d160ff9c84cbf8912f5d282e2bb4>
你的浏览器不支持 HTML 视频。
厌倦阅读了吗?
难怪,阅读我们这里的所有文本需要 7 小时。
尝试我们的交互式重构课程。它提供了一种更轻松的学习新知识的方法。
让我们看看…
将引用更改为值
问题
你有一个引用对象,它太小且不常更改,以至于无法合理管理其生命周期。
解决方案
将其转化为值对象。
之前之后
为什么重构
从引用切换到值的灵感可能来源于使用引用时的不便。引用需要你进行管理:
-
它们总是需要请求存储中的必要对象。
-
内存中的引用可能不方便使用。
-
与值相比,在分布式和并行系统中处理引用特别困难。
如果你更希望有不可更改的对象,而不是其状态可能在其生命周期内发生变化的对象,值尤其有用。
好处
-
对象的一个重要属性是它们应该是不可更改的。对于返回对象值的每个查询,应获得相同的结果。如果这一点成立,那么即使有多个对象表示相同的事物,也不会出现问题。
-
值的实现要简单得多。
缺点
- 如果一个值是可更改的,请确保如果任何对象发生变化,所有表示同一实体的其他对象中的值也会更新。这是如此繁琐,以至于为此目的创建一个引用更为简单。
如何重构
-
使对象不可更改。对象不应该有任何设置器或其他改变其状态和数据的方法(移除设置方法在这里可能会有帮助)。数据赋值给值对象字段的唯一地方是构造函数。
-
创建一个比较方法,以便能够比较两个值。
-
检查你是否可以删除工厂方法并将对象构造函数设为公共。
</images/refactoring/banners/tired-of-reading-banner-1x.mp4?id=7fa8f9682afda143c2a491c6ab1c1e56>
</images/refactoring/banners/tired-of-reading-banner.png?id=1721d160ff9c84cbf8912f5d282e2bb4>
你的浏览器不支持 HTML 视频。
厌倦阅读?
难怪,阅读我们这里所有的文本需要 7 小时。
尝试我们的交互式重构课程。它提供了一种不那么乏味的学习新知识的方法。
让我们看看…
用对象替换数组
原文:
refactoringguru.cn/replace-array-with-object
这种重构技术是用对象替换数据值的特殊情况。
问题
你有一个包含各种数据类型的数组。
解决方案
用一个将为每个元素拥有单独字段的对象替换数组。
之前
String[] row = new String[2];
row[0] = "Liverpool";
row[1] = "15";
之后
Performance row = new Performance();
row.setName("Liverpool");
row.setWins("15");
之前
string[] row = new string[2];
row[0] = "Liverpool";
row[1] = "15";
之后
Performance row = new Performance();
row.SetName("Liverpool");
row.SetWins("15");
之前
$row = [];
$row[0] = "Liverpool";
$row[1] = 15;
之后
$row = new Performance;
$row->setName("Liverpool");
$row->setWins(15);
之前
row = [None * 2]
row[0] = "Liverpool"
row[1] = "15"
之后
row = Performance()
row.setName("Liverpool")
row.setWins("15")
之前
let row = new Array(2);
row[0] = "Liverpool";
row[1] = "15";
之后
let row = new Performance();
row.setName("Liverpool");
row.setWins("15");
为什么重构
数组是存储数据和单一类型集合的绝佳工具。但如果你像使用邮政箱一样使用数组,把用户名存储在箱子 1 中,把用户地址存储在箱子 14 中,总有一天你会对此感到非常不满。这种方法会导致灾难性的失败,当有人把东西放入错误的“箱子”时,还需要花时间弄清楚哪个数据存储在哪里。
好处
-
在结果类中,你可以放置之前存储在主类或其他地方的所有相关行为。
-
一个类的字段比数组的元素更容易文档化。
如何重构
-
创建一个新类来包含数组中的数据。将数组本身作为公共字段放入类中。
-
在原始类中创建一个字段来存储该类的对象。不要忘记在你初始化数据数组的地方也创建该对象。
-
在新类中,为每个数组元素逐一创建访问方法。给它们起自解释的名称,表明它们的功能。同时,将主代码中对数组元素的每次使用替换为相应的访问方法。
-
当所有元素的访问方法都创建完成后,使数组变为私有。
-
对于数组的每个元素,在类中创建一个私有字段,然后更改访问方法以便使用这个字段而不是数组。
-
当所有数据被移动后,删除数组。
</images/refactoring/banners/tired-of-reading-banner-1x.mp4?id=7fa8f9682afda143c2a491c6ab1c1e56>
</images/refactoring/banners/tired-of-reading-banner.png?id=1721d160ff9c84cbf8912f5d282e2bb4>
你的浏览器不支持 HTML 视频。
厌倦了阅读?
难怪阅读这里所有文本需要 7 小时。
尝试我们的交互式重构课程。它提供了一种更轻松的学习新知识的方法。
让我们看看…
重复观察到的数据
问题
域数据是否存储在负责 GUI 的类中?
解决方案
然后,将数据分离到不同的类中是个好主意,以确保域类和 GUI 之间的连接和同步。
前!重复观察到的数据 - 前后!重复观察到的数据 - 后
为什么重构
你想要为相同的数据提供多个接口视图(例如,你同时有桌面应用和移动应用)。如果未能将 GUI 与域分离,你将很难避免代码重复和大量错误。
好处
-
你将责任分配给业务逻辑类和表示类(参见单一职责原则),这使你的程序更具可读性和可理解性。
-
如果需要添加新的接口视图,创建新的表示类;你不需要触碰业务逻辑的代码(参见开闭原则)。
-
现在不同的人可以同时处理业务逻辑和用户界面。
何时不使用
-
这种重构技术在其经典形式中使用观察者模板进行,但不适用于网页应用程序,因为所有类在对网页服务器的查询之间都会被重建。
-
尽管如此,将业务逻辑提取到单独类中的一般原则对于网页应用也是合理的。但这将根据你的系统设计采用不同的重构技术来实现。
如何重构
-
在GUI 类中隐藏对域数据的直接访问。为此,最好使用自我封装字段。因此,你需要为这些数据创建获取器和设置器。
-
在GUI 类事件的处理程序中,使用设置器来设置新字段值。这将使你能够将这些值传递给相关的域对象。
-
创建一个域类,并将GUI 类中的必要字段复制到其中。为所有这些字段创建获取器和设置器。
-
为这两个类创建一个观察者模式:
-
在域类中,创建一个用于存储观察者对象(GUI 对象)的数组,以及注册、删除和通知它们的方法。
-
在GUI 类中,创建一个用于存储对域类的引用的字段,以及
update()
方法,该方法将响应对象的变化并更新GUI 类中的字段值。请注意,值的更新应直接在方法中进行,以避免递归。 -
在GUI 类构造函数中,创建一个域类的实例并将其保存在你创建的字段中。将GUI 对象注册为域对象的观察者。
-
在领域类字段的 setter 中,调用通知观察者的方法(换句话说,在GUI 类中的更新方法),以便将新值传递给 GUI。
-
修改GUI 类字段的 setter,以便它们直接在领域对象中设置新值。注意确保值不是通过领域类的 setter 设置的——否则将导致无限递归。
-
</images/refactoring/banners/tired-of-reading-banner-1x.mp4?id=7fa8f9682afda143c2a491c6ab1c1e56>
</images/refactoring/banners/tired-of-reading-banner.png?id=1721d160ff9c84cbf8912f5d282e2bb4>
您的浏览器不支持 HTML 视频。
读累了吗?
不奇怪,阅读我们这里所有的文本需要 7 小时。
尝试我们的交互式重构课程。它提供了一种不那么乏味的学习新知识的方法。
让我们看看…
将单向关联更改为双向
原文:
refactoringguru.cn/change-unidirectional-association-to-bidirectional
问题
你有两个类需要使用对方的特性,但它们之间的关联仅是单向的。
解决方案
将缺失的关联添加到需要它的类中。
之前!将单向关联更改为双向 - 之前之后!将单向关联更改为双向 - 之后
为什么重构
最初这些类有单向关联。但随着时间的推移,客户端代码需要访问关联的两侧。
好处
- 如果一个类需要反向关联,你可以简单地计算它。但如果这些计算很复杂,最好保留反向关联。
缺点
-
双向关联比单向关联更难实现和维护。
-
双向关联使类相互依赖。使用单向关联时,其中一个可以独立于另一个使用。
如何重构
-
添加一个用于保存反向关联的字段。
-
决定哪个类将是“主导”。这个类将包含创建或更新关联的方法,随着元素的添加或更改,建立类中的关联,并调用建立关联的工具方法。
-
为“非主导”类创建一个建立关联的工具方法。该方法应使用参数中给定的内容来完成字段。给该方法一个明显的名称,以便之后不会用于其他目的。
-
如果控制单向关联的旧方法在“主导”类中,请用来自关联对象的工具方法补充这些方法。
-
如果控制关联的旧方法在“非主导”类中,请在“主导”类中创建这些方法,调用它们并将执行委托给它们。
</images/refactoring/banners/tired-of-reading-banner-1x.mp4?id=7fa8f9682afda143c2a491c6ab1c1e56>
</images/refactoring/banners/tired-of-reading-banner.png?id=1721d160ff9c84cbf8912f5d282e2bb4>
你的浏览器不支持 HTML 视频。
读腻了吗?
不奇怪,阅读我们这里所有文本需要 7 小时。
尝试我们的互动重构课程。它提供了一种更轻松的学习新知识的方法。
让我们看看…
将双向关联转换为单向关联
原文:
refactoringguru.cn/change-bidirectional-association-to-unidirectional
问题
你在类之间有双向关联,但其中一个类不使用另一个的特性。
解决方案
删除未使用的关联。
之前之后
为什么重构
双向关联通常比单向关联更难维护,需要额外的代码来正确创建和删除相关对象。这使得程序变得更加复杂。
此外,实施不当的双向关联可能会导致垃圾收集问题(反过来会导致未使用对象的内存膨胀)。
示例:垃圾收集器从内存中移除不再被其他对象引用的对象。假设创建了一对对象User
-Order
,使用后被遗弃。但这些对象不会从内存中清除,因为它们仍然互相引用。也就是说,随着编程语言的进步,这个问题变得不那么重要,现在语言会自动识别未使用的对象引用并将其从内存中移除。
还有类之间的相互依赖问题。在双向关联中,两个类必须互相了解,这意味着它们不能单独使用。如果存在许多这样的关联,程序的不同部分变得过于相互依赖,任何一个组件的变化可能会影响其他组件。
好处
-
简化不需要该关系的类。更少的代码意味着更少的代码维护。
-
减少类之间的依赖。独立的类更容易维护,因为对一个类的任何更改只影响该类。
如何重构
-
确保以下条件之一对你的类成立:
-
不使用任何关联。
-
还有另一种获取关联对象的方法,例如通过数据库查询。
-
相关对象可以作为参数传递给使用它的方法。
-
-
根据你的情况,包含与另一个对象关联的字段应该被替换为参数或方法调用,以不同的方式获取该对象。
-
删除将关联对象分配给字段的代码。
-
删除现在未使用的字段。
</images/refactoring/banners/tired-of-reading-banner-1x.mp4?id=7fa8f9682afda143c2a491c6ab1c1e56>
</images/refactoring/banners/tired-of-reading-banner.png?id=1721d160ff9c84cbf8912f5d282e2bb4>
你的浏览器不支持 HTML 视频。
厌倦了阅读?
不奇怪,这里所有文本的阅读时间达到 7 小时。
尝试我们的交互式重构课程。这提供了一种不那么枯燥的学习新知识的方法。
让我们看看…
用符号常量替换魔法数字
原文:
refactoringguru.cn/replace-magic-number-with-symbolic-constant
问题
你的代码使用了一个具有特定含义的数字。
解决方案
用一个具有易读名称的常量替换这个数字,以解释该数字的含义。
之前
double potentialEnergy(double mass, double height) {
return mass * height * 9.81;
}
之后
static final double GRAVITATIONAL_CONSTANT = 9.81;
double potentialEnergy(double mass, double height) {
return mass * height * GRAVITATIONAL_CONSTANT;
}
之前
double PotentialEnergy(double mass, double height)
{
return mass * height * 9.81;
}
之后
const double GRAVITATIONAL_CONSTANT = 9.81;
double PotentialEnergy(double mass, double height)
{
return mass * height * GRAVITATIONAL_CONSTANT;
}
之前
function potentialEnergy($mass, $height) {
return $mass * $height * 9.81;
}
之后
define("GRAVITATIONAL_CONSTANT", 9.81);
function potentialEnergy($mass, $height) {
return $mass * $height * GRAVITATIONAL_CONSTANT;
}
之前
def potentialEnergy(mass, height):
return mass * height * 9.81
之后
GRAVITATIONAL_CONSTANT = 9.81
def potentialEnergy(mass, height):
return mass * height * GRAVITATIONAL_CONSTANT
之前
potentialEnergy(mass: number, height: number): number {
return mass * height * 9.81;
}
之后
static const GRAVITATIONAL_CONSTANT = 9.81;
potentialEnergy(mass: number, height: number): number {
return mass * height * GRAVITATIONAL_CONSTANT;
}
为什么要重构
魔法数字是源代码中遇到的数值,但没有明显的含义。这个“反模式”使得理解程序和重构代码变得更加困难。
当你需要更改这个魔法数字时,会出现更多困难。查找和替换无法解决这个问题:相同的数字可能在不同地方用于不同目的,这意味着你需要验证每一行使用这个数字的代码。
好处
-
符号常量可以作为其值含义的实时文档。
-
更改常量的值比在整个代码库中搜索这个数字要容易得多,且不会意外改变用于其他目的的相同数字。
-
减少代码中对数字或字符串的重复使用。这在值复杂且较长时尤其重要(例如
3.14159
或0xCAFEBABE
)。
重要信息
不是所有的数字都是神奇的。
如果数字的目的很明显,就不需要替换。一个经典例子是:
for (i = 0; i < сount; i++) { ... }
替代方案
-
有时可以用方法调用替换魔法数字。例如,如果你有一个表示集合中元素数量的魔法数字,你不需要在检查集合的最后一个元素时使用它。相反,使用标准方法获取集合长度。
-
魔法数字有时用作类型代码。假设你有两种用户类型,并在一个类中使用数字字段来指定哪个是哪个:管理员为
1
,普通用户为2
。在这种情况下,你应该使用一种重构方法来避免类型代码:
-
用类替换类型代码
-
用子类替换类型代码
-
用状态/策略替换类型代码
-
如何重构
-
声明一个常量并将魔法数字的值赋给它。
-
找到所有魔法数字的提及。
-
对于你找到的每个数字,请仔细检查在这种特定情况下的魔法数字是否与常量的目的相对应。如果是,请用你的常量替换这个数字。这是一个重要步骤,因为相同的数字可能意味着完全不同的事情(并可能用不同的常量替换)。
</images/refactoring/banners/tired-of-reading-banner-1x.mp4?id=7fa8f9682afda143c2a491c6ab1c1e56>
</images/refactoring/banners/tired-of-reading-banner.png?id=1721d160ff9c84cbf8912f5d282e2bb4>
您的浏览器不支持 HTML 视频。
厌倦阅读了吗?
不奇怪,阅读我们这里所有的文本需要 7 小时。
尝试我们的交互式重构课程,它提供了更轻松的学习新知识的方法。
让我们看看……
封装字段
问题
你有一个公共字段。
解决方案
将字段设置为私有,并为其创建访问方法。
之前
class Person {
public String name;
}
之后
class Person {
private String name;
public String getName() {
return name;
}
public void setName(String arg) {
name = arg;
}
}
之前
class Person
{
public string name;
}
之后
class Person
{
private string name;
public string Name
{
get { return name; }
set { name = value; }
}
}
之前
public $name;
之后
private $name;
public getName() {
return $this->name;
}
public setName($arg) {
$this->name = $arg;
}
之前
class Person {
name: string;
}
之后
class Person {
private _name: string;
get name() {
return this._name;
}
setName(arg: string): void {
this._name = arg;
}
}
为什么重构
面向对象编程的支柱之一是 封装,即隐蔽对象数据的能力。否则,所有对象都是公共的,其他对象可以在没有任何检查和制衡的情况下获取和修改你的对象的数据!数据与与此数据相关的行为分离,程序部分的模块化受到损害,维护变得复杂。
益处
-
如果组件的数据和行为密切相关并且在代码中的同一位置,那么你维护和开发该组件会更容易。
-
你还可以执行与访问对象字段相关的复杂操作。
何时不使用
-
在某些情况下,由于性能考虑,封装是不明智的。这些情况很少见,但一旦发生,这种情况非常重要。
比如说,你有一个图形编辑器,其中包含具有 x 和 y 坐标的对象。这些字段未来不太可能改变。此外,程序涉及大量不同的对象,这些字段都存在。因此,直接访问坐标字段可以节省大量本来会被调用访问方法占用的 CPU 周期。
作为这种特殊情况的一个例子,Java 中的 Point 类的所有字段都是公共的。
如何重构
-
为字段创建 getter 和 setter。
-
找到字段的所有调用。用 getter 替换字段值的接收,用 setter 替换新的字段值的设置。
-
在替换所有字段调用后,将字段设置为私有。
下一步
封装字段只是将数据与涉及该数据的行为更紧密结合的第一步。在你为访问字段创建简单方法后,应重新检查这些方法被调用的地方。这些区域的代码很可能在访问方法中看起来更合适。
</images/refactoring/banners/tired-of-reading-banner-1x.mp4?id=7fa8f9682afda143c2a491c6ab1c1e56>
</images/refactoring/banners/tired-of-reading-banner.png?id=1721d160ff9c84cbf8912f5d282e2bb4>
你的浏览器不支持 HTML 视频。
厌倦阅读了吗?
难怪,阅读我们这里的所有文本需要 7 个小时。
尝试我们的交互式重构课程。它提供了一种不那么乏味的学习新知识的方法。
让我们看看…
封装集合
问题
一个类包含一个集合字段以及用于操作集合的简单 getter 和 setter。
解决方案
使 getter 返回的值为只读,并创建用于添加/删除集合元素的方法。
BeforeAfter
为什么重构
一个类包含一个字段,该字段包含一个对象集合。这个集合可以是数组、列表、集合或向量。为操作集合创建了正常的 getter 和 setter。
但是,集合应该通过一种与其他数据类型使用的协议略有不同的方式来使用。getter 方法不应该返回集合对象本身,因为这会让客户端在不知情的情况下更改集合内容。此外,这会向客户端显示对象数据的内部结构过多。获取集合元素的方法应该返回一个不允许更改集合或泄露过多结构数据的值。
此外,不应该有将值分配给集合的方法。相反,应该有用于添加和删除元素的操作。通过这种方式,拥有对象可以控制集合元素的添加和删除。
这样的协议恰当地封装了集合,从而最终减少了拥有类与客户端代码之间的关联程度。
好处
-
集合字段被封装在一个类中。当调用 getter 时,它返回集合的副本,这防止了在包含集合的类不知情的情况下意外更改或覆盖集合元素。
-
如果集合元素包含在基本类型内,例如数组,则可以创建更方便的方法来操作集合。
-
如果集合元素包含在非基本容器(标准集合类)中,通过封装集合可以限制对集合不必要的标准方法的访问(例如限制添加新元素)。
如何重构
-
创建用于添加和删除集合元素的方法。这些方法必须接受集合元素作为参数。
-
如果在类构造函数中未完成,则将空集合分配给该字段作为初始值。
-
查找集合字段 setter 的调用。更改 setter,使其使用添加和删除元素的操作,或使这些操作调用客户端代码。
请注意,setter 只能用于用其他元素替换所有集合元素。因此,建议将 setter 名称(重命名方法)更改为replace
。
-
查找所有在调用集合获取器后集合被更改的地方。将代码更改为使用您新的添加和删除元素的方法。
-
更改获取器,使其返回集合的只读表示。
-
检查使用集合的客户端代码,找出在集合类内部看起来更好的代码。
</images/refactoring/banners/tired-of-reading-banner-1x.mp4?id=7fa8f9682afda143c2a491c6ab1c1e56>
</images/refactoring/banners/tired-of-reading-banner.png?id=1721d160ff9c84cbf8912f5d282e2bb4>
您的浏览器不支持 HTML 视频。
厌倦了阅读?
不奇怪,阅读我们这里的所有文本需要 7 小时。
尝试我们的交互式重构课程。它提供了一种更轻松的学习新知识的方法。
让我们看看…
用类替换类型代码
原文:
refactoringguru.cn/replace-type-code-with-class
什么是类型代码? 类型代码发生在没有单独数据类型时,你有一组数字或字符串,这些值形成某个实体的允许值列表。这些特定的数字和字符串通常通过常量给出可理解的名称,这就是为什么这种类型代码如此常见的原因。
问题
一个类有一个字段包含类型代码。这个类型的值不在操作符条件中使用,也不影响程序的行为。
解决方案
创建一个新类,并使用它的对象来代替类型代码的值。
之前之后
为什么重构
类型代码最常见的原因之一是在与数据库工作时,当数据库中有字段编码了某个复杂概念的数字或字符串。
例如,你有一个User
类,其字段user_role
包含每个用户的访问权限信息,可能是管理员、编辑者或普通用户。因此,在这种情况下,这些信息在字段中分别编码为A
、E
和U
。
这种方法的缺点是什么?字段的设置器通常不检查发送的值,这可能会在某人向这些字段发送意外或错误的值时造成大问题。
此外,这些字段无法进行类型验证。你可以向它们发送任何数字或字符串,这不会被你的 IDE 进行类型检查,甚至允许你的程序运行(然后崩溃)。
好处
-
我们希望将一组原始值(即编码类型的内容)转变为完整的类,从而获得面向对象编程所提供的所有好处。
-
通过用类替换类型代码,我们允许在编程语言层面上对传递给方法和字段的值进行类型提示。
例如,当传递值到方法时,编译器以前无法区分你的数字常量和某个任意数字,但现在当传递不符合指定类型类的数据时,你会在 IDE 中收到错误警告。
-
因此,我们可以将代码移动到类型的类中。如果你需要在整个程序中对类型值进行复杂操作,现在这些代码可以“存在”于一个或多个类型类中。
什么时候不使用
如果编码类型的值在控制流结构(if
、switch
等)中使用,并控制类的行为,你应该使用两种类型代码重构技术之一:
-
用子类替换类型代码
-
用状态/策略替换类型代码
如何重构
-
创建一个新类,并给它一个与编码类型目的相对应的新名称。我们称之为类型类。
-
将包含类型代码的字段复制到类型类中,并将其设为私有。然后为该字段创建一个获取器。该字段的值将仅从构造函数中设置。
-
对于每个编码类型的值,在类型类中创建一个静态方法。它将创建一个对应于此编码类型值的新类型类对象。
-
在原始类中,将编码字段的类型替换为类型类。在构造函数和字段设置器中创建此类型的新对象。更改字段获取器,使其调用类型类获取器。
-
将编码类型的值的任何提及替换为相关类型类静态方法的调用。
-
从原始类中删除编码类型常量。
</images/refactoring/banners/tired-of-reading-banner-1x.mp4?id=7fa8f9682afda143c2a491c6ab1c1e56>
</images/refactoring/banners/tired-of-reading-banner.png?id=1721d160ff9c84cbf8912f5d282e2bb4>
您的浏览器不支持 HTML 视频。
读累了吗?
不奇怪,阅读我们这里所有文本需要 7 小时。
尝试我们的交互式重构课程。它提供了一种不那么乏味的学习新知识的方法。
我们来看看…
用子类替换类型代码
原文:
refactoringguru.cn/replace-type-code-with-subclasses
什么是类型代码? 类型代码是指,当你有一组数字或字符串,而不是单独的数据类型时,这些数字或字符串形成某个实体的可允许值列表。通常,这些具体的数字和字符串通过常量被赋予可理解的名称,这也是为何这种类型代码如此常见的原因。
问题
你有一个编码类型,它直接影响程序行为(该字段的值触发条件语句中的各种代码)。
解决方案
为编码类型的每个值创建子类。然后将原类中的相关行为提取到这些子类中。用多态性替换控制流代码。
之前!用子类替换类型代码 - 之前之后!用子类替换类型代码 - 之后
为什么要重构
这种重构技术是对用类替换类型代码的一种更复杂的变体。
与第一个重构方法一样,你有一组简单值,这些值构成字段的所有允许值。虽然这些值通常被指定为常量,并具有可理解的名称,但它们的使用会使你的代码非常容易出错,因为它们在本质上仍然是原始值。例如,你有一个方法接受这些值中的一个作为参数。在某个时刻,方法收到的字符串为小写形式("admin"
),而不是常量USER_TYPE_ADMIN
对应的值"ADMIN"
,这将导致执行与作者(你)原本意图不同的操作。
在这里,我们处理的控制流代码包括条件语句if
、switch
和?:
。换句话说,具有编码值的字段(例如$user->type === self::USER_TYPE_ADMIN
)在这些运算符的条件中被使用。如果在这里使用用类替换类型代码,所有这些控制流结构最好移动到一个负责数据类型的类中。最终,这当然会创建一个非常类似于原来的类型类,但同样存在原有的问题。
好处
-
删除控制流代码。将原类中笨重的
switch
代码移动到适当的子类中。这提高了对单一职责原则的遵循,并使程序整体上更具可读性。 -
如果需要为编码类型添加一个新值,你只需添加一个新的子类,而无需触及现有代码(参见开闭原则)。
-
通过用类替换类型代码,我们为编程语言层面的方法和字段提供了类型提示。这在使用简单的数字或字符串值构成的编码类型时是无法实现的。
何时不使用
-
如果你已经有了类层次结构,这种技术就不适用。在面向对象编程中,你无法通过继承创建双重层次结构。不过,你可以通过组合而非继承来替换类型代码。为此,请使用 用状态/策略替换类型代码。
-
如果类型代码的值在对象创建后可以更改,避免使用此技术。我们必须以某种方式在运行时替换对象本身的类,这是不可能的。不过,在这种情况下,替代方案也是 用状态/策略替换类型代码。
如何重构
-
使用 自封装字段 为包含类型代码的字段创建一个 getter。
-
使超类构造函数为私有。创建一个与超类构造函数具有相同参数的静态工厂方法。它必须包含一个参数,用于接收编码类型的起始值。根据这个参数,工厂方法将创建不同子类的对象。为此,在其代码中必须创建一个大型条件判断,但至少在确实必要时它是唯一的;否则,子类和多态性将会起作用。
-
为编码类型的每个值创建一个唯一的子类。在其中,重定义编码类型的 getter,使其返回对应的编码类型的值。
-
从超类中删除带有类型代码的字段。使其 getter 为抽象。
-
一旦你有了子类,就可以开始将字段和方法从超类移动到相应的子类中(借助 向下推送字段 和 向下推送方法)。
-
当所有可能的内容都已移动后,使用 用多态性替换条件 来彻底摆脱一次性使用类型代码的条件。
</images/refactoring/banners/tired-of-reading-banner-1x.mp4?id=7fa8f9682afda143c2a491c6ab1c1e56>
</images/refactoring/banners/tired-of-reading-banner.png?id=1721d160ff9c84cbf8912f5d282e2bb4>
你的浏览器不支持 HTML 视频。
厌倦阅读了吗?
难怪,阅读我们这里所有的文本需要 7 小时。
尝试我们的交互式重构课程。这提供了一种不那么乏味的学习新知识的方法。
让我们看看…