前言
随着项目的增长和代码的复杂性,保持代码的可读性和可维护性变得尤为重要。在本文中,我将向您介绍三个令人着迷的JavaScript重构技巧,从此对那些屎山say no!
1.使用多态替代条件语句
这个重构技巧的中文叫做「将条件式换成多型」。条件判断一直是在程序里产生复杂度的小坏蛋,虽然只有一两个的时候不会有什么影响,但如果一直放任重复的判断散落在代码中,那之后要再修改或增加条件时,就必须要找出所有要改的地方,少一个都不行
就拿下面这个例子来说,一笔交易的状态是否成功会影响页面如何向用户展示信息:
jsx
//transactionInfo.jsx
const TransactionInfo = (transaction) => (
<div>
<label>交易状态:</label>
<span>{ transaction.status === 'SUCCESS' ? '成功' : '失败' }</span>
<button disabled={transaction.status !== 'SUCCESS'}>
退款
</button>
</div>
);
在上面的代码示例中,尽管两种判断逻辑是相反的,但对交易状态是否成功的判断却重复写了两次。如果以后需要再增加不同的交易状态,比如处理中之类的,就需要再次检查与状态相关的条件判断是否需要增加或修改
在下一次需要进行修改之前,我们可以采用"将条件语句替换为多态"的重构方法来进行改善
其实最简单的方法,就是根据传进来的交易数据,在同一个地方生成各种状态的交易记录所需显示在画面上的信息。就像这样:
jsx
//TransactionInfo.jsx
const TransactionInfo = (transaction) => {
const getTransactionInfo = () => {
if (transaction.status === 'SUCCESS') {
return {
statusString: '成功',
isNonRefundable: false,
};
}
return {
statusString: '失败',
isNonRefundable: true,
};
};
return (
<div>
<label>交易状态:</label>
<span>{ getTransactionInfo().statusString }</span>
<button disabled={getTransactionInfo().isNonRefundable}>
退款
</button>
</div>
);
};
将需要在页面上显示的交易信息集中到 getTransactionInfo
中,原本的两个条件判断也变成了只有一个。这样一来,如果要添加新的状态显示,只需要专注于修改 getTransactionInfo
即可,比如将其改为switch case等
jsx
//getTransactionInfo.js
const getTransactionInfo = () => {
switch (transaction.status) {
case 'SUCCESS':
return {
statusString: '成功',
isNonRefundable: false,
};
case 'FAIL':
return {
statusString: '失败',
isNonRefundable: true,
};
case 'PENDING':
return {
statusString: '处理中',
isNonRefundable: true,
};
default:
throw new Error(`Have no transaction status: ${transaction.status}`);
}
};
如果 getTransactionInfo
里面塞了太多东西的话,也可以考虑使用class来产生对应状态的条件,如果换成class也可以利用继承关系,把一些共用的判断放到父类别里:
jsx
//FailTransaction.js
import Transaction from './Transaction';
class FailTransaction extends Transaction {
constructor(transaction) {
super(transaction);
this.statusString = '失败';
this.isNonRefundable = true;
}
}
export default FailTransaction;
jsx
//PendingTransaction.js
import Transaction from './Transaction';
class PendingTransaction extends Transaction {
constructor(transaction) {
super(transaction);
this.statusString = '处理中';
this.isNonRefundable = true;
}
}
export default PendingTransaction;
jsx
//SuccessTransaction.js
import Transaction from './Transaction';
class SuccessTransaction extends Transaction {
constructor(transaction) {
super(transaction);
this.statusString = '成功';
this.isNonRefundable = false;
}
}
export default SuccessTransaction;
jsx
//Transaction.js
class Transaction {
constructor(transaction) {
this.id = transaction.id;
}
}
export default Transaction;
然后,也可以将 getTransactionInfo
组件移出,并在其中直接使用class:
jsx
//getTransactionInfo.js
import SuccessTransaction from './Transaction/SuccessTransaction';
import FailTransaction from './Transaction/FailTransaction';
import PendingTransaction from './Transaction/PendingTransaction';
const getTransactionInfo = (transaction) => {
switch (transaction.status) {
case 'SUCCESS':
return new SuccessTransaction(transaction);
case 'FAIL':
return new FailTransaction(transaction);
case 'PENDING':
return new PendingTransaction(transaction);
default:
throw new Error(`Have no transaction status: ${transaction.status}`);
}
};
export default getTransactionInfo;
尽管这样做会使本来简单的代码变得更复杂,文件也变得更多,但实际上只是将原本存在于用户界面上的复杂度转移到了 getTransactionInfo
,并增加了处理各种交易状态要显示的信息的结构,使用户界面成为纯粹显示数据的地方
当然这种用多态替代条件判断的重构方法不仅仅适用于移除UI上的条件判断,如果你发现某个判断特别频繁的东西,也可以将其分离到不同的类型上,这个重构方法就非常适合使用。而且如果相同类型的判断越多,就越推荐使用。
将"Subclass"替换为"Delegate"
第二个重构技巧是将子类替换为委托
。这个重构技巧适用于当一个类出现另一组更适合继承的情况,或者仅仅是想要将当前的继承关系拆分开来,减少两个类之间的紧密联系
就拿第一个例子的交易信息来说,我们刚刚将交易信息作为父类别,而关于交易信息的状态,比如"成功的交易"、"失败的交易"、"等待中的交易"作为子类别。但是如果有一天我们认为"可退款的交易"和"不可退款的交易"更适合作为交易信息的子类别时,就必须将原本作为子类别的交易状态们转换成委派处理
好的,那该怎么做呢?首先一样找出获取交易信息的地方,依上方的例子就是 getTransactionInfo
方法:
jsx
//getTransactionInfo.js
import SuccessTransaction from './Transaction/SuccessTransaction';
import FailTransaction from './Transaction/FailTransaction';
import PendingTransaction from './Transaction/PendingTransaction';
const getTransactionInfo = (transaction) => {
switch (transaction.status) {
case 'SUCCESS':
return new SuccessTransaction(transaction);
case 'FAIL':
return new FailTransaction(transaction);
case 'PENDING':
return new PendingTransaction(transaction);
default:
throw new Error(`Have no transaction status: ${transaction.status}`);
}
};
export default getTransactionInfo;
但就目前来说我们先不管它,只是要把决定交易状态行为的逻辑,也就是上方的 switch case 移动到 Transaction
类别的 constructor 里面:
jsx
//Transaction.js
import SuccessTransaction from './SuccessTransaction';
import FailTransaction from './FailTransaction';
import PendingTransaction from './PendingTransaction';
class Transaction {
constructor(transaction) {
this.id = transaction.id;
this.transactionStatusDelegate = this.getTransactionStatusDelegate(transaction);
}
getTransactionStatusDelegate(transaction) {
switch (transaction.status) {
case 'SUCCESS':
return new SuccessTransaction(transaction);
case 'FAIL':
return new FailTransaction(transaction);
case 'PENDING':
return new PendingTransaction(transaction);
default:
throw new Error(`Have no transaction status: ${transaction.status}`);
}
}
}
export default Transaction;
接下来我们需要检查交易状态内的数据有哪些,我们会需要通过 getter 从委托对象中获取,例如 transaction
和 statusString
。因为将来不会使用交易状态的子类创建对象,而是使用可退款交易的子类,所以通过将这些属性转换到 Transaction
的 getter 中,使用方的代码就不需要修改了
jsx
//Transaction.js
import SuccessTransaction from './SuccessTransaction';
import FailTransaction from './FailTransaction';
import PendingTransaction from './PendingTransaction';
class Transaction {
constructor(transaction) {
this.id = transaction.id;
this.transactionStatusDelegate = this.getTransactionStatusDelegate(transaction);
}
get statusString() {
return this.transactionStatusDelegate.statusString;
}
get isNonRefundable() {
return this.transactionStatusDelegate.isNonRefundable;
}
getTransactionStatusDelegate(transaction) {
// ...
}
}
export default Transaction;
然后回到 getTransactionInfo
中,就可以将原本生成交易状态子类别的 switch case 移除了:
jsx
//getTransactionInfo.js
import Transaction from './Transaction';
const getTransactionInfo = (transaction) => new Transaction(transaction);
最后还要将原本在子类别的继承关系移除,也因为交易状态等类别的定位改变了,所以这里顺便修改了它们的命名:
jsx
//FailTransactionStatusDelegate.js
class FailTransactionStatusDelegate {
constructor(transaction) {
this.statusString = '失败';
this.isNonRefundable = true;
}
}
export default FailTransactionStatusDelegate;
jsx
//PendingTransactionStatusDelegate.js
class PendingTransactionStatusDelegate {
constructor(transaction) {
this.statusString = '处理中';
this.isNonRefundable = true;
}
}
export default PendingTransactionStatusDelegate;
jsx
//SuccessTransactioStatusnDelegate.js
class SuccessTransactioStatusnDelegate {
constructor(transaction) {
this.statusString = '成功';
this.isNonRefundable = false;
}
}
export default SuccessTransactioStatusnDelegate;
jsx
//Transaction.js
import SuccessTransactioStatusnDelegate from './SuccessTransactioStatusnDelegate';
import FailTransactionStatusDelegate from './FailTransactionStatusDelegate';
import PendingTransactionStatusDelegate from './PendingTransactionStatusDelegate';
class Transaction {
constructor(transaction) {
this.id = transaction.id;
this.transactionStatusDelegate = this.getTransactionStatusDelegate(transaction);
}
//...
getTransactionStatusDelegate(transaction) {
switch (transaction.status) {
case 'SUCCESS':
return new SuccessTransactioStatusnDelegate(transaction);
case 'FAIL':
return new FailTransactionStatusDelegate(transaction);
case 'PENDING':
return new PendingTransactionStatusDelegate(transaction);
default:
throw new Error(`Have no transaction status: ${transaction.status}`);
}
}
}
export default Transaction;
重构完成后, Transaction
已经没有任何子类别了,现在就可以根据新需求开始建立可退款交易的子类别了!
另外一点就是,虽然上方的例子都没在委派类别取得 transaction
资料后做任何事情,但如果有操作需要原本的资料的话,也是可以在委派类别中新增一个 host
属性把送进来的 transaction
存起来哦!
特例对象处理
如果说用「以多态取代条件表达式」来处理一个对象的多种类型的重构这种方式没什么特别,那么遇到处理对象的特殊情况该怎么办。举个例子,当你经常需要在多个地方判断某个用户是否登录,并对未登录的情况进行相应处理时,未登录状态的用户就需要特例对象处理。
假设未登录的用户必须显示为游客,并且在浏览文章时不能回复文章,必须要跳转到登录页面。那么在代码中,我们可能会这样写:
jsx
//CommentButton.jsx
const CommentButton = () => {
const { data: user } = useUser();
return (
<button onClick={user?.id ? user.submitComment : () => { /* 跳转登录页面 */ }}>
留言
</button>
);
};
jsx
//Header.jsx
const Header = () => {
const { data: user } = useUser();
return (
<div>
{ `${user?.name || '游客'}您好!` }
</div>
);
};
在代码中,类似这样的条件判断用于判断用户是否登录,并且由于在未登录的情况下, user
的值将为null,因此在使用user
值时必须时刻小心,必须使用可选链。
而引入特殊对象就是为了改善这种情况的重构技巧,首先可以观察 user
是从哪里统一获取的。对于上述代码来说,是从 useUser
获取的。以下是简化的 useUser
内容,主要通过 apis.getUser
获取用户数据,如果未登录的话 user
将会是null:
jsx
//useUser.js
import { useQuery } from 'react-query';
import apis from '../apis';
const useUser = () => useQuery(
['user'],
async () => {
const { data: user } = await apis.getUser();
return user;
}
);
export default useUser;
接下来,我们可以进入 useUser
,将特殊对象引入。最简单的方法是添加一个方法,使该方法能够返回未登录用户需要显示或执行的操作。例如,在 Header
和 Comment
之间的区别是显示名称 name
和提交留言的 submitComment
事件。然后,在 useUser
返回时,根据 user
的值来判断要返回什么内容:
jsx
//useUser.js
import { useQuery } from 'react-query';
import apis from '../apis';
const useUser = () => useQuery(
['user'],
async () => {
const createGuestUser = () => ({
name: '游客',
submitComment: () => { /* 跳转登录页面 */ },
});
const { data: user } = await apis.getUser();
return user || createGuestUser();
}
);
export default useUser;
这样一来,在 Header
和 Comment
就可以省去对特例的判断了:
jsx
//CommentButton.jsx
const CommentButton = () => {
const { data: user } = useUser();
return (
<button onClick={ user.submitComment }>
留言
</button>
);
};
jsx
//Header.jsx
const Header = () => {
const { data: user } = useUser();
return (
<div>{ `${user.name}您好!` }</div>
);
};
看起来不仅清晰许多,也不必担心 user
有可能会是 null 的情况,因为当它是 null 时,背后一定会有个安全的对象扛住(专业名词叫做Introduce Null Object)!
会不会突然想到 React 或 Vue 中都可以对 Props 设置一个默认的值,让 Component 在没有拿到 Props 时也有一个默认对象可以返回,现在看来预设值好像也是一种特例处理!
总结
最后针对文章介绍的三个重构技巧做个小整理:
- 使用多态来替代条件语句:当一个对象具有多种形态,并且需要经常根据这些形态进行不同的显示时,可以使用多态
- 替换子类继承为委托:当类别存在更适合的继承关系,或者希望解开当前的继承关系时,可以使用委托
- 特例对象处理:如果经常需要对某个状态或其某个属性进行判断,那么可以另外创建一个特例对象来处理这些判断
在这篇文章中介绍了三种重构的方法,但除了这三种之外,在文章中的重构步骤中,还包含了一些简单的重构技巧,比如使用工厂函数替换构造函数以创建统一获取相同对象的来源,或者使用重命名变量的方式来修改变量名称,这些都是非常常用的重构技巧
也不得不说这些重构技巧让代码的结构性增加的同时,文件数量也跟着暴增,不过在好几次要修改或增加功能时都会有种「哇!还好当初决定这么做!」的开心感,感觉自己好像预知了未来。 😂
如果大家对文章中的重构技巧有任何想法或对文章内容有任何问题,可以留言指出。非常感谢大家!