写点什么

拯救遗留系统:重构函数的 7 个小技巧

用户头像
Phoenix
关注
发布于: 刚刚
拯救遗留系统:重构函数的 7 个小技巧

重构的范围很大,有包括类结构、变量、函数、对象关系,还有单元测试的体系构建等等。

在这一章,我们主要分享重构函数的 7 个小技巧。🧰

在重构的世界里,几乎所有的问题都源于过长的函数导致的,因为:

  • 过长的函数包含太多信息,承担太多职责,无法或者很难复用

  • 错综复杂的逻辑,导致没人愿意去阅读代码,理解作者的意图


对于过长函数的处理方式,在 《重构》中作者推荐如下手法进行处理:

1:提炼函数

示例一

我们看先一个示例,原始代码如下:


void printOwing(double amout) {  printBanner();  // Print Details  System.out.println("name:" + _name);  System.out.println("amount:" + _amount);}
复制代码


Extract Method 的重构手法是将多个 println() 抽离到独立的函数中(函数需要在命名上,下点功夫),这里对抽离的函数命名有 2 个建议:


  • 保持函数尽可能的小,函数越小,被复用的可能性越大

  • 良好的函数命名,可以让调用方的代码看起来上注释(结构清晰的代码,其实并不是很需要注释)


将 2 个 println() 方法抽离到 printDetails() 函数中:


void printDetails(double amount) {  System.out.println("name:" + _name);  System.out.println("amount:" + _amount);}
复制代码


当我们拥有 printDetails() 独立函数后,那么最终 printOwing() 函数看起来像:


void printOwing(double amout) {  printBanner();  printDetails(double amount);}
复制代码

示例二

示例一可能过于简单,无法表示 Extract Method 的奇妙能力,我们通过一个更复杂的案例来表示,代码如下:


void printOwing() {  Enumeration e = _orders.elements();  double oustanding = 0.0
// print banner System.out.println("*******************") System.out.println("***Customer Owes***") System.out.println("*******************")
// calculate outstanding while(e.hasMoreElements()){ Order each = (Order)e.nextElement(); outstanding += each.getAmount(); }
// print details System.out.println("name:" + _name); System.out.println("amount:" + outstanding); }
复制代码


首先审视一下这段代码,这是一段过长的函数(典型的糟糕代码的代表),因为它企图去完成所有的事情。但通过注释我们可以将它的函数提炼出来,方便函数复用,而且 printOwing() 代码结构也会更加清晰,最终版本如下:


void printOwing(double previousAmount) {  printBaner();   // Extract print banner  double outstanding = getOutstanding(previousAmount * 1.2)   // Extract calculate outstanding  printDetails(outstanding)   // print details}
复制代码


printOwing() 看起来像注释的代码,对于阅读非常友好,然后看看被 Extract Method 被提炼的函数代码:


void printBanner() {  System.out.println("*******************")  System.out.println("***Customer Owes***")  System.out.println("*******************")  }
double getOutstanding(double initialValue) { double result = initialValue; // 赋值引用对象,避免对引用传递 Enumeration e = _orders.elements(); while(e.hasMoreElements()){ Order each = (Order)e.nextElement(); result += each.getAmount(); } return result;}
void printDetails(double outstanding) { System.out.println("name:" + _name); System.out.println("amount:" + outstanding); }
复制代码

总结

提炼函数是最常用的重构手法之一,就是将过长函数按职责拆分至合理范围,这样被拆解的函数也有很大的概率被复用到其他函数内

2:移除多余函数

当函数承担的职责和内容过小的时候,我们就需要将两个函数合并,避免系统产生和分布过多的零散的函数

示例一

假如我们程序中有以下 2 个函数,示例程序:


int getRating() {  return (moreThanFiveLateDeliveries()) ? 2 : 1;}
boolean moreThanFiveLateDeliveries() { return _numberOfLateDeliveries > 5;}
复制代码


moreThanFiveLateDeliveries() 似乎没有什么存在的必要,因为它仅仅是返回一个 _numberOfLateDeliveries 变量,我们就可以使用 Inline Method 内联函数 来重构它,修改后的代码如下:


int getRating() {  return (_numberOfLateDeliveries > 5) ? 2 : 1;}
复制代码


注意事项:


  • 如果 moreThanFiveLateDeliveries() 已经被多个调用方引用,则不要去修改它

总结

Inline Method 内联函数 就是逻辑和职责简单的,并且只被使用 1 次的函数进行合并和移除,让系统整体保持简单和整洁

3:移除临时变量

先看示例代码:

示例一

double basePrice = anOrder.basePrice();return basePrice > 1000;
复制代码


使用 Inline Temp Variable 来内联 basePrice 变量,代码如下:


return anOrder.basePrice() > 1000;
复制代码

总结

如果函数内的临时变量,只被引用和使用一次,那么它就应该被内联和移除,避免产生过多冗余代码,从而影响阅读

4:函数替代表达式

如果你的程序依赖一段表达式来进行逻辑判断,那么你可以利用一段函数封装表达式,来让计算过程更加灵活的被复用

示例一

double basePrice = _quantity * _itemPrice;if (basePrice > 1000) {  return basePrice * 0.95;} else {  return basePrice * 0.98;}
复制代码


在示例一,我们可以把 basePrice 的计算过程封装起来,这样其他函数调用也更方便,重构后示例如下:



if (basePrice() > 1000) { return basePrice() * 0.95;} else { return basePrice() * 0.98;}
// 抽取 basePrice() 计算过程double basePrice() { return _quantity * _itemPrice;}
复制代码


以上程序比较简单,不太能看出函数替代表达式的效果,我们换一个更负责的看看,先看一段获取商品价格的程序:


double getPrice() {  final int basePrice = _quantity * _itemPrice;  final double discountFactor;  if (basePrice > 1000) {    discountFactor = 0.95;  } else {    discountFactor = 0.98;  }  return basePrice * discountFactor;}
复制代码


如果我们使用 函数替代表达式 的重构手法,那么程序最终读起来可能就像:


double getPrice() {  // 读起来像不像注释 ? 这里的代码还需要写注释吗?  return basePrice() * discountFactor();}
复制代码


至于 basePrice()、discountFactor() 是怎么拆解的,这里回忆一下 提炼函数 的内容,以下放出提炼的代码:


int basePrice() {  return _quantity * _itemPrice;}
double discountFactor() { final double discountFactor; return basePrice() > 1000 ? 0.95 : 0.98;}
复制代码

总结

使用函数替代表达式替代表达式,对于程序来说有以下几点好处:


  1. 封装表达式的计算过程,调用方无需关心结果是怎么计算出来的,符合 OOP 原则

  2. 当计算过程发生改动,也不会影响调用方,只要修改函数本身即可

5:引入解释变量

当你的程序内部出现大量晦涩难懂的表达式,影响到程序阅读的时候,你需要 引入解释变量 来解决这个问题,不然代码容易变的腐烂,从而导致失控。另外引入解释变量也会让分支表达式更好理解。

示例一

我们先看一段代码(我敢保证这段代码你看的肯定会很头疼。。。💆)


if (platform.tpUpperCase().indexOf("MAC") > -1 && browser.toUpperCase().indexOf("IE") > -1 && wasInitialized() && resize > 0) {    // do something ....}
复制代码


使用 引入解释变量 的方法来重构它的话,会让你取起来有不同的感受,代码如下:


final boolean isMacOs = platform.tpUpperCase().indexOf("MAC") > -1;final boolean isIEBrowser = browser.toUpperCase().indexOf("IE") > -1;final boolean wasResized = resize > 0;
if (isMacOs && isIEBrowser && wasInitialized() && wasResized()) { // do something ...}
复制代码


这样做还有一个好处就是,在 Debug 程序的时候你可以提前知道每段表达式的结果,不必等到执行到 IF 的时候再推算

示例二

其实 引入解释变量 ,只是解决问题的方式之一,复习我们刚才提到的 提炼函数也能解决这个问题,我们再来看一段容易引起生理不适的代码 😤:


double price() {// price is base price - quantity discount + shipping return (_quantity * _itemPrice) -     Math.max(0, _quantity - 500) * _itemPrice * 0.05 +     Math.min(_quantity * _itemPrice * 0.1, 100.0);}
复制代码


我们使用 Extract Method 提炼函数处理代码后,那么它读起来就像是这样:


double price() {  return basePrice() - quantityDiscount() + shipping();}
复制代码


有没有感受到什么叫好的代码就像好的文章?👩‍🌾 这样的代码根本不用写注释了,当然把被提炼的函数也放出来:


private double quantityDiscount() {  return Math.max(0, _quantity - 500) * _itemPrice * 0.05;}
private double shipping() { return Math.min(_quantity * _itemPrice * 0.1, 100.0);}
private double basePrice() { return (_quantity * _itemPrice);}
复制代码

总结

当然大多数场景是可以使用 Extract Method 提炼函数来替代引入解释变量来解决问题,但这并不代表 引入解释变量 这种重构手法就毫无用处,我们还是可以根据一些特定的场景来找到它的使用场景:


  • 当 Extract Method 提炼函数使用成本比较高,并且难以进行时……

  • 当逻辑表达式过于复杂,并且只使用一次的时候(如果会被复用,推荐使用 提炼函数 方式)

6:避免修改函数参数

虽然不同的编程语言的函数参数传递会区分:“按值传递”、“按引用传递”的两种方式(Java 语言的传递方式是按值传递),这里不就讨论两种传递方式的区别,相信大家都知道。

示例一

我们不应该直接对 inputVal 参数进行修改,但是如果直接修改函数的参数会让人搞混乱这两种方式,如下以下代码:


int discount (int inputVal) {  if (inputVal > 50) {    intputVal -= 2;  }  return intputVal;}
复制代码


如果是在 引用传递 类型的编程语言里,discount() 函数对于 intputVal 变量的修改,甚至还会影响到调用方。所以我们正确的做法应该是使用一个临时变量来处理对参数的修改,代码如下:


int discount (int inputVal) {  int result = inputVal;  if (inputVal > 50) {    result -= 2;  }  return result;}
复制代码

辩证的看待按值传递

众所周知在按值传递的编程语言中,任何对参数的任何修改,都不会对调用端造成任何影响。但是如何不加以区分,这种特性依然会让你感到困惑😴,我们先看一段正常的代码:


public class Param {    public static void main(String[] args) {        int x = 5;        triple(x);        System.out.println("x after triple: " + x);    }
private static void triple (int arg) { arg = arg * 3; System.out.println("arg in triple: " + arg); }}
复制代码


这段代码不容易引起困惑,习惯按值传递的小伙伴,应该了解它的输出会如下:


arg in triple: 15x after triple: 5
复制代码


但是如果函数的参数是对象,你可能就会觉得困惑了,我们再看一下代码,把函数对象改为对象试试:


public class Param {    public static void main(String[] args) {        Date d1 = new Date("1 Apr 98");        nextDateUpdate(d1);        System.out.println("d1 after nextDay:" + d1);        Date d2 = new Date("1 Apr 98");        nextDateReplace(d2);        System.out.println("d2 after nextDay:" + d2);    }
private static void nextDateUpdate(Date arg) { // 不是说按值传递吗?怎么这里修改对象影响外部了。。 arg.setDate(arg.getDate() + 1);; System.out.println("arg in nextDay: " + arg); }
private static void nextDateReplace(Date arg) { // 尝试改变对象的引用,又不生效。。what the fuck ? arg = new Date(arg.getYear(), arg.getMonth(), arg.getDate() + 1); System.out.println("arg in nextDay: " + arg); }}
复制代码


最终输出如下,有没有被弄的很迷糊 ?🤣:


arg in nextDay: Thu Apr 02 00:00:00 CST 1998d1 after nextDay:Thu Apr 02 00:00:00 CST 1998arg in nextDay: Thu Apr 02 00:00:00 CST 1998d2 after nextDay:Wed Apr 01 00:00:00 CST 1998
复制代码

总结

对于要修改的函数变量,乖乖的使用临时变量,避免造成不必要的混乱

7:替换更优雅的函数实现

示例一

谁都有年少无知,不知天高地厚和轻狂的时候,那时候的我们就容易写下这样的代码:


String foundPerson(String[] people) {  for (int i = 0; i < perple.length; i++) {
if (peole[i].equals("Trevor")) { return "Trevor"; } if (peole[i].equals("Jim")) { return "Jim"; } if (peole[i].equals("Phoenix")) { return "Phoenix"; }
// 弊端:如果加入新人,又要写很多重复的逻辑和代码 // 这种代码写起来好无聊。。而且 CV 大法也容易出错 }}
复制代码


那时候我们代码写的不好,还不自知,但随着我们的能力和经验的增改,我们回头看看自己的代码,这简直是一坨 💩 但是年轻人嘛,总归要犯一些错误,佛说:知错能改善莫大焉。现在我们变牛逼 🐂 了,对于曾经的糟糕代码肯定不能不闻不问,所以的重构就是,在不更改输入和输出的情况下,给他替换一种更优雅的实现,代码如下:


String foundPerson(String[] people) {  // 加入新人,我们扩展数组就好了  List condidates = Arrays.asList(new String[] {"Trevor", "Jim", "Phoenix"});  // 逻辑代码不动,不容易出错  for (int i = 0; i <= people.length; i++) {    if (condidates.equals(people[i])) {      return people[i]    }  }}
复制代码

总结

建议:


  • 在我们回顾曾经的代码的时候,如果你有更好的实现方案(保证输入输出相同的前提下),就应该直接替换掉它

  • 记得通过单元测试后,再提交代码(不想被人打的话)


参考文献:


发布于: 刚刚阅读数: 2
用户头像

Phoenix

关注

独立开发者 2017.10.17 加入

一只成熟的程序猿

评论

发布
暂无评论
拯救遗留系统:重构函数的 7 个小技巧