重构大师-四-
重构大师(四)
引入参数对象
问题
您的方法中包含一组重复的参数。
解决方案
用一个对象替换这些参数。
之前!引入参数对象 - 之前之后!引入参数对象 - 之后
为什么重构
相同的参数组常常在多个方法中出现。这会导致参数本身及相关操作的代码重复。通过将参数合并到一个类中,您还可以将处理这些数据的方法移到那里,从而使其他方法摆脱这段代码。
好处
-
更具可读性的代码。您看到的不是一堆混乱的参数,而是一个具有可理解名称的单一对象。
-
到处散落的相同参数组会创造出一种代码重复:尽管相同的代码没有被调用,但相同的参数和参数组却不断被遇到。
缺点
- 如果您只将数据移动到新类中,并且不打算将任何行为或相关操作移到那里,这开始有数据类的味道。
如何重构
-
创建一个新的类来表示您的参数组。使该类不可变。
-
在您想重构的方法中,使用添加参数,这就是您的参数对象将被传递的地方。在所有方法调用中,将从旧方法参数创建的对象传递给此参数。
-
现在开始逐个从方法中删除旧参数,在代码中用参数对象的字段替换它们。每替换一个参数后测试程序。
-
完成后,查看是否有必要将方法的一部分(有时甚至整个方法)移到参数对象类中。如果有,请使用 移动方法 或 提取方法。
</images/refactoring/banners/tired-of-reading-banner-1x.mp4?id=7fa8f9682afda143c2a491c6ab1c1e56>
</images/refactoring/banners/tired-of-reading-banner.png?id=1721d160ff9c84cbf8912f5d282e2bb4>
您的浏览器不支持 HTML 视频。
厌倦阅读吗?
难怪,阅读我们这里所有文本需要 7 小时。
尝试我们的交互式重构课程。这提供了一种不那么乏味的学习新知识的方法。
让我们看看……
删除设置方法
问题
字段的值应在创建时设置,之后不应更改。
解决方案
因此,删除设置字段值的方法。
之前!删除设置方法 - 之前之后!删除设置方法 - 之后
为什么重构
您希望防止字段值的任何更改。
如何重构
-
字段的值只能在构造函数中更改。如果构造函数中没有设置值的参数,请添加一个。
-
查找所有 setter 调用。
-
如果 setter 调用位于当前类构造函数调用后面,请将其参数移动到构造函数调用中并删除 setter。
-
在构造函数中用直接访问字段替换 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 个小时。
尝试我们的交互式重构课程。这是一种更轻松的学习新知识的方法。
让我们看看…
隐藏方法
问题
一个方法未被其他类使用,或仅在其自身的类层次内使用。
解决方案
将方法设为私有或受保护。
前后
为什么重构
通常,隐藏获取和设置值的方法的需求是由于开发出更丰富的接口,提供额外的行为,尤其是当你开始时的类仅添加了简单的数据封装。
随着新行为融入类中,你可能会发现公共获取器和设置器方法不再必要,可以隐藏。如果将获取器或设置器方法设为私有并直接访问变量,可以删除该方法。
好处
-
隐藏方法使你的代码更易于演变。当你更改私有方法时,只需担心如何不破坏当前类,因为你知道该方法无法在其他地方使用。
-
通过将方法设为私有,你强调了类的公共接口及其保留的公共方法的重要性。
如何重构
-
定期寻找可以设为私有的方法。静态代码分析和良好的单元测试覆盖可以提供很大的帮助。
-
尽可能将每个方法设为私有。
</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-constructor-with-factory-method
问题
您有一个复杂的构造函数,除了设置对象字段中的参数值外还做其他事情。
解决方案
创建一个工厂方法,并用它替换构造函数调用。
之前
class Employee {
Employee(int type) {
this.type = type;
}
// ...
}
之后
class Employee {
static Employee create(int type) {
employee = new Employee(type);
// do some heavy lifting.
return employee;
}
// ...
}
之前
public class Employee
{
public Employee(int type)
{
this.type = type;
}
// ...
}
之后
public class Employee
{
public static Employee Create(int type)
{
employee = new Employee(type);
// Do some heavy lifting.
return employee;
}
// ...
}
之前
class Employee {
// ...
public function __construct($type) {
$this->type = $type;
}
// ...
}
之后
class Employee {
// ...
static public function create($type) {
$employee = new Employee($type);
// do some heavy lifting.
return $employee;
}
// ...
}
之前
class Employee {
constructor(type: number) {
this.type = type;
}
// ...
}
之后
class Employee {
static create(type: number): Employee {
let employee = new Employee(type);
// Do some heavy lifting.
return employee;
}
// ...
}
为什么重构
使用此重构技术的最明显原因与用子类替换类型代码有关。
您的代码中之前创建了一个对象,并将编码类型的值传递给它。使用重构方法后,出现了多个子类,您需要根据编码类型的值创建对象。改变原始构造函数以返回子类对象是不可能的,因此我们创建一个静态工厂方法,它将返回所需类的对象,之后用它替换所有原始构造函数的调用。
工厂方法也可以在其他情况下使用,当构造函数无法胜任时。它们在尝试将值更改为引用时可能很重要。它们还可以用于设置超出参数数量和类型的各种创建模式。
优势
-
工厂方法不一定返回调用它的类的对象。通常,这些可以是其子类,基于传递给该方法的参数进行选择。
-
工厂方法可以有一个更好的名称,描述它返回什么以及如何返回,例如
Troops::GetCrew(myTank)
。 -
工厂方法可以返回已经创建的对象,而构造函数总是创建一个新的实例。
如何重构
-
创建一个工厂方法。在其中调用当前构造函数。
-
用对工厂方法的调用替换所有构造函数调用。
-
将构造函数声明为私有。
-
调查构造函数代码,尝试隔离与当前类对象构造无关的代码,将这些代码移动到工厂方法中。
</images/refactoring/banners/tired-of-reading-banner-1x.mp4?id=7fa8f9682afda143c2a491c6ab1c1e56>
</images/refactoring/banners/tired-of-reading-banner.png?id=1721d160ff9c84cbf8912f5d282e2bb4>
您的浏览器不支持 HTML 视频。
读累了吗?
不奇怪,阅读我们这里的所有文本需要 7 小时。
尝试我们的交互式重构课程。它提供了一种不那么乏味的学习新知识的方法。
让我们看看……
用异常替代错误代码
问题
方法返回一个指示错误的特殊值吗?
解决方案
抛出一个异常。
之前
int withdraw(int amount) {
if (amount > _balance) {
return -1;
}
else {
balance -= amount;
return 0;
}
}
之后
void withdraw(int amount) throws BalanceException {
if (amount > _balance) {
throw new BalanceException();
}
balance -= amount;
}
之前
int Withdraw(int amount)
{
if (amount > _balance)
{
return -1;
}
else
{
balance -= amount;
return 0;
}
}
之后
///<exception cref="BalanceException">Thrown when amount > _balance</exception>
void Withdraw(int amount)
{
if (amount > _balance)
{
throw new BalanceException();
}
balance -= amount;
}
之前
function withdraw($amount) {
if ($amount > $this->balance) {
return -1;
} else {
$this->balance -= $amount;
return 0;
}
}
之后
function withdraw($amount) {
if ($amount > $this->balance) {
throw new BalanceException;
}
$this->balance -= $amount;
}
之前
def withdraw(self, amount):
if amount > self.balance:
return -1
else:
self.balance -= amount
return 0
之后
def withdraw(self, amount):
if amount > self.balance:
raise BalanceException()
self.balance -= amount
之前
withdraw(amount: number): number {
if (amount > _balance) {
return -1;
}
else {
balance -= amount;
return 0;
}
}
之后
withdraw(amount: number): void {
if (amount > _balance) {
throw new Error();
}
balance -= amount;
}
为什么重构
返回错误代码是程序设计的过时遗留物。在现代编程中,错误处理由特殊类执行,这些类被称为异常。如果发生问题,你会“抛出”一个错误,然后由其中一个异常处理程序“捕获”它。在正常条件下被忽略的特殊错误处理代码被激活以作出响应。
好处
-
使代码摆脱大量检查各种错误代码的条件语句。异常处理程序是一种更加简洁的方式来区分正常执行路径和异常路径。
-
异常类可以实现自己的方法,从而包含部分错误处理功能(例如发送错误消息)。
-
与异常不同,错误代码不能在构造函数中使用,因为构造函数必须仅返回一个新对象。
缺点
- 异常处理程序可能变成一种类似于 goto 的拐杖。避免这样做!不要使用异常来管理代码执行。异常应仅用于通知错误或关键情况。
如何重构
尝试一次仅为一个错误代码执行这些重构步骤。这将更容易让你将所有重要信息牢记在心,避免错误。
-
查找所有调用返回错误代码的方法,而不是检查错误代码,将其包装在
try
/catch
块中。 -
在方法内部,抛出异常,而不是返回错误代码。
-
更改方法签名,使其包含有关抛出异常的信息(
@throws
部分)。
</images/refactoring/banners/tired-of-reading-banner-1x.mp4?id=7fa8f9682afda143c2a491c6ab1c1e56>
</images/refactoring/banners/tired-of-reading-banner.png?id=1721d160ff9c84cbf8912f5d282e2bb4>
你的浏览器不支持 HTML 视频。
读累了吗?
不奇怪,阅读我们这里所有文本需要 7 小时。
尝试我们的互动重构课程。这提供了一种不那么乏味的学习新知识的方法。
让我们看看……
用测试替换异常
问题
你在一个简单测试可以完成工作的地方抛出了异常吗?
解决方案
用条件测试替换异常。
之前
double getValueForPeriod(int periodNumber) {
try {
return values[periodNumber];
} catch (ArrayIndexOutOfBoundsException e) {
return 0;
}
}
之后
double getValueForPeriod(int periodNumber) {
if (periodNumber >= values.length) {
return 0;
}
return values[periodNumber];
}
之前
double GetValueForPeriod(int periodNumber)
{
try
{
return values[periodNumber];
}
catch (IndexOutOfRangeException e)
{
return 0;
}
}
之后
double GetValueForPeriod(int periodNumber)
{
if (periodNumber >= values.Length)
{
return 0;
}
return values[periodNumber];
}
之前
function getValueForPeriod($periodNumber) {
try {
return $this->values[$periodNumber];
} catch (ArrayIndexOutOfBoundsException $e) {
return 0;
}
}
之后
function getValueForPeriod($periodNumber) {
if ($periodNumber >= count($this->values)) {
return 0;
}
return $this->values[$periodNumber];
}
之前
def getValueForPeriod(periodNumber):
try:
return values[periodNumber]
except IndexError:
return 0
之后
def getValueForPeriod(self, periodNumber):
if periodNumber >= len(self.values):
return 0
return self.values[periodNumber]
之前
getValueForPeriod(periodNumber: number): number {
try {
return values[periodNumber];
} catch (ArrayIndexOutOfBoundsException e) {
return 0;
}
}
之后
getValueForPeriod(periodNumber: number): number {
if (periodNumber >= values.length) {
return 0;
}
return values[periodNumber];
}
为什么重构
异常应该用于处理与意外错误相关的不规则行为。它们不应该替代测试。如果可以通过在运行之前验证条件来避免异常,那么就这么做。异常应该留给真正的错误。
例如,你进入了雷区并触发了一枚地雷,导致了一个异常;这个异常被成功处理,你被抬到安全的地方。但你本可以通过一开始就阅读雷区前的警告标志来避免这一切。
好处
- 有时,简单的条件比异常处理代码更明显。
如何重构
-
为边缘案例创建条件,并将其移动到 try/catch 块之前。
-
将代码从
catch
部分移动到这个条件内。 -
在
catch
部分,放置抛出普通未命名异常的代码,并运行所有测试。 -
如果在测试中没有抛出任何异常,去掉
try
/catch
操作符。
</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/dealing-with-generalization
抽象有自己的一组重构技术,主要与沿类继承层次移动功能、创建新类和接口、以及用委托替代继承和反之相关。
提取字段
问题: 两个类有相同的字段。
解决方案: 从子类中移除字段并将其移动到超类中。
提取方法
问题: 你的子类有执行相似工作的 方法。
解决方案: 使方法相同,然后将它们移动到相关的超类中。
提取构造函数主体
问题: 你的子类的构造函数中的代码大部分是相同的。
解决方案: 创建一个超类构造函数,将子类中相同的代码移动到其中,并在子类构造函数中调用超类构造函数。
推送方法
问题: 在超类中实现的行为仅被一个(或少数几个)子类使用吗?
解决方案: 将该行为移动到子类中。
推送字段
问题: 一个字段仅在少数子类中使用吗?
解决方案: 将字段移动到这些子类中。
提取子类
问题: 一个类的特性仅在某些情况下使用。
解决方案: 创建一个子类并在这些情况下使用它。
提取超类
问题: 你有两个类有共同的字段和方法。
解决方案: 为它们创建一个共享的超类,并将所有相同的字段和方法移动到其中。
提取接口
问题: 多个客户端正在使用类接口的相同部分。另一种情况:两个类中的接口部分相同。
解决方案: 将这部分相同的内容移动到自己的接口中。
合并层次结构
问题: 你有一个类层次结构,其中子类几乎与其超类相同。
解决方案: 合并子类和超类。
形成模板方法
问题: 你的子类实现的算法包含相似步骤且顺序相同。
解决方案: 将算法结构和相同步骤移到超类中,将不同步骤的实现留在子类中。
用委托替换继承
问题: 你有一个子类仅使用超类方法的一部分(或无法继承超类数据)。
解决方案: 创建一个字段并放入超类对象,将方法委托给超类对象,并去掉继承。
用继承替换委托
问题: 一个类包含许多简单的方法,这些方法委托给另一个类的所有方法。
解决方案: 使该类成为一个委托继承者,从而使得委托方法变得不必要。
提升字段
问题
两个类有相同的字段。
解决方案
从子类中移除该字段并将其移至父类。
之前!提升字段 - 之前之后!提升字段 - 之后
为什么要重构
子类各自独立发展,导致出现相同(或几乎相同)的字段和方法。
优势
-
消除子类中的字段重复。
-
如果存在重复的方法,便于后续将其从子类迁移到父类。
如何重构
-
确保子类中的字段用于相同的需求。
-
如果字段名称不同,将它们重命名为相同的名称,并替换现有代码中所有对这些字段的引用。
-
在父类中创建一个同名字段。请注意,如果这些字段是私有的,父类字段应该是受保护的。
-
从子类中移除字段。
-
你可能想考虑使用 自我封装字段 来为新字段提供访问方法,从而隐藏它。
</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。
-
对于方法:使用提升方法或者在超类中声明抽象方法(请注意,如果类之前不是抽象的,它将变为抽象类)。
-
-
从子类中移除方法。
-
检查方法被调用的位置。在某些地方,你可能能够用超类替代子类。
</images/refactoring/banners/tired-of-reading-banner-1x.mp4?id=7fa8f9682afda143c2a491c6ab1c1e56>
</images/refactoring/banners/tired-of-reading-banner.png?id=1721d160ff9c84cbf8912f5d282e2bb4>
你的浏览器不支持 HTML 视频。
厌倦了阅读?
难怪,阅读我们这里所有的文本需要 7 小时。
尝试我们的交互式重构课程。它提供了一种更不乏味的学习新知识的方法。
让我们看看……
提升构造函数主体
问题
你的子类具有大部分相同的构造函数代码。
解决方案
创建一个超类构造函数,并将子类中相同的代码移到其中。在子类构造函数中调用超类构造函数。
之前
class Manager extends Employee {
public Manager(String name, String id, int grade) {
this.name = name;
this.id = id;
this.grade = grade;
}
// ...
}
之后
class Manager extends Employee {
public Manager(String name, String id, int grade) {
super(name, id);
this.grade = grade;
}
// ...
}
之前
public class Manager: Employee
{
public Manager(string name, string id, int grade)
{
this.name = name;
this.id = id;
this.grade = grade;
}
// ...
}
之后
public class Manager: Employee
{
public Manager(string name, string id, int grade): base(name, id)
{
this.grade = grade;
}
// ...
}
之前
class Manager extends Employee {
public function __construct($name, $id, $grade) {
$this->name = $name;
$this->id = $id;
$this->grade = $grade;
}
// ...
}
之后
class Manager extends Employee {
public function __construct($name, $id, $grade) {
parent::__construct($name, $id);
$this->grade = $grade;
}
// ...
}
之前
class Manager(Employee):
def __init__(self, name, id, grade):
self.name = name
self.id = id
self.grade = grade
# ...
之后
class Manager(Employee):
def __init__(self, name, id, grade):
Employee.__init__(name, id)
self.grade = grade
# ...
之前
class Manager extends Employee {
constructor(name: string, id: string, grade: number) {
this.name = name;
this.id = id;
this.grade = grade;
}
// ...
}
之后
class Manager extends Employee {
constructor(name: string, id: string, grade: number) {
super(name, id);
this.grade = grade;
}
// ...
}
为什么要重构
这个重构技术与提升方法有什么不同?
-
在 Java 中,子类不能继承构造函数,因此你不能简单地将提升方法应用于子类构造函数,并在将所有构造函数代码移到超类后删除它。除了在超类中创建构造函数外,子类中还需要有构造函数,以便简单地委托给超类构造函数。
-
在 C++和 Java 中(如果你没有显式调用超类构造函数),超类构造函数会在子类构造函数之前自动调用,这使得只需要从子类构造函数的开头移动公共代码(因为你不能在子类构造函数的任意位置调用超类构造函数)。
-
在大多数编程语言中,子类构造函数可以有与超类不同的参数列表。因此,你应该仅创建一个真正需要的超类构造函数。
如何重构
-
在超类中创建一个构造函数。
-
将每个子类构造函数开头的公共代码提取到超类构造函数中。在这样做之前,尽量将尽可能多的公共代码移动到构造函数的开头。
-
在子类构造函数的第一行放置对超类构造函数的调用。
</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 小时。
尝试我们的交互式重构课程。它提供了一种不那么乏味的学习新知识的方法。
让我们看看…
提取子类
问题
一个类的特性只在特定情况下使用。
解决方案
创建一个子类并在这些情况下使用它。
之前之后
为什么重构
你的主类有用于实现某个罕见用例的方法和字段。虽然这种情况很罕见,但这个类对此负责,将所有相关字段和方法移动到一个完全独立的类是错误的。然而,它们可以被移动到一个子类,这正是我们将通过这种重构技术实现的。
好处
-
快速简便地创建子类。
-
如果你的主类当前实现多个特殊情况,可以创建几个独立的子类。
缺点
-
尽管看似简单,继承可能会导致死胡同,如果你必须分离几个不同的类层次。例如,如果你有
Dogs
类,其行为根据狗的大小和毛发不同,你可以提炼出两个层次:-
按大小:
Large
、Medium
和Small
-
按毛发:
Smooth
和Shaggy
一切似乎都很好,除了当你需要创建一个既是
Large
又是Smooth
的狗时,问题就会出现,因为你只能从一个类创建对象。也就是说,你可以通过使用组合而不是继承来避免这个问题(见策略模式)。换句话说,Dog
类将有两个组件字段,大小和毛发。你将从必要的类中插入组件对象到这些字段中。因此,你可以创建一个拥有LargeSize
和ShaggyFur
的Dog
。 -
如何重构
-
从感兴趣的类创建一个新的子类。
-
如果你需要额外的数据从子类创建对象,请创建一个构造函数并将必要的参数添加到其中。不要忘记调用构造函数的父类实现。
-
找到对父类构造函数的所有调用。当需要子类的功能时,用子类构造函数替换父构造函数。
-
将必要的方法和字段从父类移动到子类。通过向下推送方法和向下推送字段来完成这一过程。先移动方法通常更简单。这样,字段在整个过程中仍然可以访问:在移动之前来自父类,在移动完成后来自子类。
-
在子类准备好后,找到所有控制功能选择的旧字段。使用多态性删除这些字段,以替代所有使用过这些字段的操作符。一个简单的例子:在 Car 类中,你有字段
isElectricCar
,并且根据这个字段,在refuel()
方法中,汽车要么加油,要么充电。重构后,isElectricCar
字段被移除,Car
和ElectricCar
类将拥有各自的refuel()
方法实现。
</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 小时。
尝试我们的交互式重构课程。它提供了一种不那么乏味的学习新知识的方法。
让我们看看……
合并层次结构
问题
您有一个类层次结构,其中子类几乎与其超类相同。
解决方案
合并子类和超类。
之前!合并层次结构 - 之前之后!合并层次结构 - 之后
为什么要重构
您的程序随着时间的推移而增长,子类和超类几乎变得相同。一个功能从子类中删除,一个方法移到超类……现在您有两个相似的类。
好处
-
程序复杂性降低。更少的类意味着您头脑中需要理清的事物更少,并且在将来代码更改时需要担心的可破坏的活动部分也更少。
-
当方法在一个类中早期定义时,浏览您的代码会更容易。您无需遍历整个层次结构来找到特定方法。
什么时候不使用
-
您正在重构的类层次结构是否有多个子类?如果是这样,重构完成后,剩余的子类应成为合并层次结构的类的继承者。
-
但请记住,这可能导致违反里斯科夫替换原则。例如,如果您的程序模拟城市交通网络,您不小心将
Transport
超类合并到Car
子类中,那么Plane
类可能会成为Car
的继承者。哎呀!
如何重构
-
选择哪个类更容易删除:超类还是其子类。
-
如果您决定去掉子类,请使用提升字段和提升方法。如果您选择删除超类,请使用下推字段和下推方法。
-
将您要删除的类的所有用法替换为字段和方法要迁移到的类。通常这将涉及创建类的代码、变量和参数类型定义,以及代码注释中的文档。
-
删除空类。
</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 个小时。
尝试我们的互动重构课程。它提供了一种更轻松的学习新知识的方法。
我们来看看…
用委托替代继承
问题
您有一个子类只使用超类的一部分方法(或无法继承超类数据)。
解决方案
创建一个字段并放入一个超类对象,委托方法给超类对象,去掉继承。
之前之后
为什么重构
用组合替代继承可以大幅改善类设计,如果:
-
您的子类违反了里氏替换原则,即如果继承只是为了组合公共代码,而不是因为子类是超类的扩展。
-
子类只使用超类的一部分方法。在这种情况下,总会有人调用本不应该调用的超类方法,这只是时间问题。
本质上,这种重构技术将两个类分开,使超类成为子类的助手,而不是其父类。子类将只拥有委托给超类对象的方法,而不是继承所有超类方法。
好处
-
一个类不包含从超类继承的任何不必要的方法。
-
可以将各种不同实现的对象放入委托字段中。实际上,您获得了策略设计模式。
缺点
- 您必须编写许多简单的委托方法。
如何重构
-
在子类中创建一个字段以持有超类。在初始阶段,将当前对象放入其中。
-
更改子类方法,使其使用超类对象而不是
this
。 -
对于从超类继承的方法在客户端代码中被调用的情况,请在子类中创建简单的委托方法。
-
从子类中移除继承声明。
-
通过创建一个新对象来更改存储前超类的字段的初始化代码。
</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 小时。
尝试我们的互动重构课程。它提供了一种不那么乏味的学习新知识的方法。
让我们看看…