likes
comments
collection
share

强烈推荐三个重构技巧(再讲一个很新的东西)

作者站长头像
站长
· 阅读数 13

强烈推荐三个重构技巧(再讲一个很新的东西)

前言

随着项目的增长和代码的复杂性,保持代码的可读性和可维护性变得尤为重要。在本文中,我将向您介绍三个令人着迷的JavaScript重构技巧,从此对那些屎山say no!

1.使用多态替代条件语句

这个重构技巧的中文叫做「将条件式换成多型」。条件判断一直是在程序里产生复杂度的小坏蛋,虽然只有一两个的时候不会有什么影响,但如果一直放任重复的判断散落在代码中,那之后要再修改或增加条件时,就必须要找出所有要改的地方,少一个都不行

就拿下面这个例子来说,一笔交易的状态是否成功会影响页面如何向用户展示信息:

//transactionInfo.jsx

const TransactionInfo = (transaction) => (
  <div>
    <label>交易状态:</label>
    <span>{ transaction.status === 'SUCCESS' ? '成功' : '失败' }</span>
    <button disabled={transaction.status !== 'SUCCESS'}>
      退款
    </button>
  </div>
);

在上面的代码示例中,尽管两种判断逻辑是相反的,但对交易状态是否成功的判断却重复写了两次。如果以后需要再增加不同的交易状态,比如处理中之类的,就需要再次检查与状态相关的条件判断是否需要增加或修改

在下一次需要进行修改之前,我们可以采用"将条件语句替换为多态"的重构方法来进行改善

其实最简单的方法,就是根据传进来的交易数据,在同一个地方生成各种状态的交易记录所需显示在画面上的信息。就像这样:

//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等

//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也可以利用继承关系,把一些共用的判断放到父类别里:

//FailTransaction.js

import Transaction from './Transaction';

class FailTransaction extends Transaction {
  constructor(transaction) {
    super(transaction);
    this.statusString = '失败';
    this.isNonRefundable = true;
  }
}

export default FailTransaction;
//PendingTransaction.js
import Transaction from './Transaction';

class PendingTransaction extends Transaction {
  constructor(transaction) {
    super(transaction);
    this.statusString = '处理中';
    this.isNonRefundable = true;
  }
}

export default PendingTransaction;
//SuccessTransaction.js
import Transaction from './Transaction';

class SuccessTransaction extends Transaction {
  constructor(transaction) {
    super(transaction);
    this.statusString = '成功';
    this.isNonRefundable = false;
  }
}

export default SuccessTransaction;
//Transaction.js
class Transaction {
  constructor(transaction) {
    this.id = transaction.id;
  }
}

export default Transaction;

然后,也可以将 getTransactionInfo 组件移出,并在其中直接使用class:

//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 方法:

//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 里面:

//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 中,使用方的代码就不需要修改了

//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 移除了:

//getTransactionInfo.js
import Transaction from './Transaction';

const getTransactionInfo = (transaction) => new Transaction(transaction);

最后还要将原本在子类别的继承关系移除,也因为交易状态等类别的定位改变了,所以这里顺便修改了它们的命名:

//FailTransactionStatusDelegate.js
class FailTransactionStatusDelegate {
  constructor(transaction) {
    this.statusString = '失败';
    this.isNonRefundable = true;
  }
}

export default FailTransactionStatusDelegate;
//PendingTransactionStatusDelegate.js
class PendingTransactionStatusDelegate {
  constructor(transaction) {
    this.statusString = '处理中';
    this.isNonRefundable = true;
  }
}

export default PendingTransactionStatusDelegate;
//SuccessTransactioStatusnDelegate.js
class SuccessTransactioStatusnDelegate {
  constructor(transaction) {
    this.statusString = '成功';
    this.isNonRefundable = false;
  }
}

export default SuccessTransactioStatusnDelegate;
//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 存起来哦!

 特例对象处理

如果说用「以多态取代条件表达式」来处理一个对象的多种类型的重构这种方式没什么特别,那么遇到处理对象的特殊情况该怎么办。举个例子,当你经常需要在多个地方判断某个用户是否登录,并对未登录的情况进行相应处理时,未登录状态的用户就需要特例对象处理。

假设未登录的用户必须显示为游客,并且在浏览文章时不能回复文章,必须要跳转到登录页面。那么在代码中,我们可能会这样写:

//CommentButton.jsx
const CommentButton = () => {
  const { data: user } = useUser();
  return (
    <button onClick={user?.id ? user.submitComment : () => { /* 跳转登录页面 */ }}>
      留言
    </button>
  );
};
//Header.jsx
const Header = () => {
  const { data: user } = useUser();
  return (
    <div>
      { `${user?.name || '游客'}您好!` }
    </div>
  );
};

在代码中,类似这样的条件判断用于判断用户是否登录,并且由于在未登录的情况下, user 的值将为null,因此在使用user值时必须时刻小心,必须使用可选链。

而引入特殊对象就是为了改善这种情况的重构技巧,首先可以观察 user 是从哪里统一获取的。对于上述代码来说,是从 useUser 获取的。以下是简化的 useUser 内容,主要通过 apis.getUser 获取用户数据,如果未登录的话 user 将会是null:

//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 的值来判断要返回什么内容:

//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 就可以省去对特例的判断了:

//CommentButton.jsx
const CommentButton = () => {
  const { data: user } = useUser();
  return (
    <button onClick={ user.submitComment }>
      留言
    </button>
  );
};
//Header.jsx
const Header = () => {
  const { data: user } = useUser();
  return (
    <div>{ `${user.name}您好!` }</div>
  );
};

看起来不仅清晰许多,也不必担心 user 有可能会是 null 的情况,因为当它是 null 时,背后一定会有个安全的对象扛住(专业名词叫做Introduce Null Object)!

会不会突然想到 React 或 Vue 中都可以对 Props 设置一个默认的值,让 Component 在没有拿到 Props 时也有一个默认对象可以返回,现在看来预设值好像也是一种特例处理!

 总结

 最后针对文章介绍的三个重构技巧做个小整理:

  1. 使用多态来替代条件语句:当一个对象具有多种形态,并且需要经常根据这些形态进行不同的显示时,可以使用多态
  2. 替换子类继承为委托:当类别存在更适合的继承关系,或者希望解开当前的继承关系时,可以使用委托
  3. 特例对象处理:如果经常需要对某个状态或其某个属性进行判断,那么可以另外创建一个特例对象来处理这些判断

在这篇文章中介绍了三种重构的方法,但除了这三种之外,在文章中的重构步骤中,还包含了一些简单的重构技巧,比如使用工厂函数替换构造函数以创建统一获取相同对象的来源,或者使用重命名变量的方式来修改变量名称,这些都是非常常用的重构技巧

也不得不说这些重构技巧让代码的结构性增加的同时,文件数量也跟着暴增,不过在好几次要修改或增加功能时都会有种「哇!还好当初决定这么做!」的开心感,感觉自己好像预知了未来。 😂

如果大家对文章中的重构技巧有任何想法或对文章内容有任何问题,可以留言指出。非常感谢大家!