“艺术源于生活”——代码也源于生活,你在生活中的一些行为习惯,可能会恰如其分地体如今代码中。
当实现较为复杂的功能时,因为它包含一系列的逻辑,咱们倾向于编写一个“大方法”来实现。
为了使项目便于维护,以及加强代码的可读性,咱们有必要对“大方法”的逻辑进行整理,并提取出分散的“小方法”。
这就是本文要讲的两种重构策略:提取方法、提取方法对象。html
在生活中,我是一个比较随意的人,平时也买了很多书去看。
个人书柜不够大,且已经装满了书,每当读完一本书时,我懒得花些时间整理,想着之后再去整理这些书籍,因此我一般都将这些书塞到一个大箱子里面。
每次要重读某些书时,我巴不得把这个箱子翻个底朝天,在花费“九牛二虎之力”以后,我终于找到了要看的书。
而后,我把其余翻出来的书再塞回去。编程
箱子那么大,全部的书都放在一个箱子里,一整箱书都没有分类,有些书藏得很深,找起书来的确不方便。架构
后来,我想到了一个方法——最近快递小哥送货的包装纸箱还在家里,这些箱子不会很大,但装书应该绰绰有余,何不把这些箱子利用起来?
因而,我就动手挑选了一些大小合适的小纸箱,用签字笔给每一个纸箱作了一个标记。this
1号纸箱,装ASP.NET编程相关的书
2号纸箱,装架构设计相关的书
3号纸箱,装管理相关的书
…
N号纸箱,装旅游相关的书spa
在生活中,不少读者可能也遇到过此类问题,为何找个东西就这么难呢?架构设计
当一个方法包含实现一个功能的全部逻辑时,不只方法会看起来比较臃肿(可读性差),也会给未来的维护形成困扰,每次改动都会让你如履薄冰,而且较大可能带来新的bug。这不符合咱们“未来的利益”,咱们可使用提取方法的重构策略来规避这个问题。设计
下面是我对提取方法的定义:3d
若是一个方法包含多个逻辑,咱们应将每一个逻辑提取出来,并确保每一个方法只作一件事情。htm
下面这段代码定义了一个Receipt类,用于描述收入信息,并计算总收入。
using System.Collections.Generic; namespace ExtractMethod.Before { public class Receipt { public IList<decimal> Discounts { get; set; } public IList<decimal> ItemTotals { get; set; } public decimal CalculateGrandTotal() { decimal subTotal = 0m; // 计算subTotal foreach (decimal itemTotal in ItemTotals) subTotal += itemTotal; // 计算折扣 if (Discounts.Count > 0) { foreach (decimal discount in Discounts) { subTotal -= discount; } } // 计算税额 decimal tax = subTotal*0.065m; subTotal += tax; return subTotal; } } }
CalculateGrandTotal()方法包含了多处逻辑:计算subTotal,计算折扣,计算税额。
这几处逻辑是相对独立的,咱们能够将其提取出来,重构为3个方法。
重构后,CalculateGrandTotal()方法只包含调用各个子方法的逻辑,这已经精简了不少,可读性也有所加强。
using System.Collections.Generic; using System.Linq; namespace ExtractMethod.After { public class Receipt { public IList<decimal> Discounts { get; set; } public IList<decimal> ItemTotals { get; set; } public decimal CalculateGrandTotal() { // 计算subTotal decimal subTotal = CalculateSubTotal(); // 计算折扣 subTotal = CalculateDiscounts(subTotal); // 计算税额 subTotal = CalculateTax(subTotal); return subTotal; } // 计算subTotal private decimal CalculateSubTotal() { return ItemTotals.Sum(); } // 计算折扣 private decimal CalculateDiscounts(decimal subTotal) { if (Discounts.Count > 0) { subTotal = Discounts.Aggregate(subTotal, (current, discount) => current - discount); } return subTotal; } // 计算税额 private decimal CalculateTax(decimal subTotal) { decimal tax = subTotal * 0.065m; subTotal += tax; return subTotal; } } }
我认为这仍然不够。CalculateGrandTotal() 方法所表现的“语义”,是为了计算收入总额。
但上面这段代码不能让咱们快速地知道这个语义,咱们须要经过3个子方法来理解这个语义。
“计算收入总额”本质上是有一个公式的,即“收入总额 = (各项子收入总和 - 折扣总和) * (1 + 税率)”,公式的右侧是一个简单的三项式。
这个方法没有体现”公式“这个概念,为了让这段代码OO的味道更浓厚一些。
咱们再次对其重构,将公式右侧的每一项提取为属性,每一项的计算逻辑都经过get属性体现。
using System.Collections.Generic; using System.Linq; namespace ExtractMethod.After { public class Receipt2 { private IList<decimal> Discounts { get; set; } private IList<decimal> ItemTotals { get; set; } public decimal CalculateGrandTotal() { // 收入总额 = (各项子收入总和 - 折扣总和) * (1 + 税率) decimal grandTotal = (SubTotal - TotalDiscounts) * (1 + TaxRate); return grandTotal; } // 获取subTotal private decimal SubTotal { get { return ItemTotals.Sum(); } } // 获取TotalDiscounts private decimal TotalDiscounts { get { return Discounts.Sum(); } } // 获取TaxRate private decimal TaxRate { get { return 0.065m; } } } }
再次重构后的代码,是否是一目了然?
这里可能有人会疑惑了,本文不是讲提取方法的吗?如今怎么去提取属性了呢?
在C#中,属性的本质是字段的get, set方法,因此它仍然算是提取方法。
请注意,并非全部状况下,都适合使用提取属性来代替提取方法。个人建议是,当提取的方法逻辑较少时,可使用提取属性代替。当提取的方法逻辑较多时,若是使用提取属性代替,也会让人以为困扰。由于属性是为了描述对象的特征,描述特征的过程若是较为复杂,会让人难以理解,咱们应该keep it simple!
以上示例描述了一个客观对象:“收入”,这个对象包含两个层面的“语义”——“收入相关的信息”和“计算收入的方法”。
“收入相关的信息”用名词来体现,它揭示了收入客观存在的特征(例如:全部的子收入、折扣和税率)。
“计算收入的方法”用动词来体现,它揭示了收入的计算过程。
这两层“语义”能够看作两种不一样的职责,为了将这两层“语义”隔离开来,咱们能够将“计算收入的方法”提取为一个新的对象。
using System.Collections.Generic; using System.Linq; namespace ExtractMethod.After { /// <summary> /// 描述收入相关的信息 /// </summary> public class Receipt { public IList<decimal> Discounts { get; set; } public IList<decimal> ItemTotals { get; set; } // 获取TaxRate public decimal TaxRate { get { return 0.065m; } } public decimal CalculateGrandTotal() { return new ReceiptCalculator(this).CalculateGrandTotal(); } } /// <summary> /// 描述收入的计算方法 /// </summary> public class ReceiptCalculator { private readonly Receipt _receipt; public ReceiptCalculator(Receipt receipt) { _receipt = receipt; } public decimal CalculateGrandTotal() { decimal grandTotal = (SubTotal - TotalDiscounts) * (1 + _receipt.TaxRate); return grandTotal; } // 获取subTotal private decimal SubTotal { get { return _receipt.ItemTotals.Sum(); } } // 获取TotalDiscounts private decimal TotalDiscounts { get { return _receipt.Discounts.Sum(); } } } }
这则代码将Receipt对象的“计算收入的方法”提取到了ReceiptCalculator对象,Receipt对象则只保留了属性和精简的CalculateGrandTotal()方法。
“提取方法对象”也是一个不错的重构策略,“提取方法对象”有什么做用呢?它能够精确类的职责,控制类的粒度。
一开始,咱们用Receipt来描述“收入”这件事情;后来咱们发现这件事情能够拆分为两个细节,“收入相关的信息”和“计算收的方法”,因而咱们将这两个细节拆分开来。
到这里,也许你们又能看出一点点”OO”的味道了,它体现了咱们看待客观事物的角度,以及对客观事物的理解程度。OO的过程是咱们对客观事物的探索和认知过程,它也会随着咱们了解到更多的事物细节而进化。