小酌复建系列[4]——分解方法
概述
“分解方法”的思想和前面讲到的“提取方法”、“提取方法对象”基本一致。
它是将较大个体的方法不断的拆分,让每个“方法”做单一的事情,从而提高每个方法的可读性和可维护性。
分解方法可以看做是“提取方法”的递归版本,它是对方法反复提炼的一种重构策略。
分解方法
下图表示了这个重构策略,第1次提炼和第2次提炼都采用了“提取方法”这个策略。
何时分解方法?
“分解方法”最终可以让方法的可读性极大地增强,通常我们可以依据以下几点来辨别方法是否需要分解:
2. 方法应该尽量短小,方法最好不要超过20行(依不同情况,酌情考虑行数)
3. 方法的缩进层次不宜太多,最好不要超过两级
4. 方法需要太多的注释才能理解
示例
场景说明
假设在企业制作年度预算的场景中,用户需要按照如下Excel模板填写科目、部门、各月的预算数据,然后将Excel文件导入到“预算系统”。
为了表示用户填写的每一行预算数据,开发人员在系统中设计了两个class:BudgetItem(预算项)和BudgetItemDetail(预算项明细)。
上图红色方框标注的表示一个BudgetItem对象,每个蓝色方框则对应一个BudgetItemDetail对象。
/// <summary> /// 预算项 /// </summary> public class BudgetItem { public string Dept { get; set; } public string Account { get; set; } public IList<BudgetItemDetail> BudgetItemDetails { get; set; } } /// <summary> /// 预算项明细 /// </summary> public class BudgetItemDetail { public string Month { get; set; } public decimal Amount { get; set; } }
重构前
在表示这段逻辑时,我们编写了一个BudgetItemImport类,用于读取Excel并返回IList<BudgetItem>集合
public class BudgetItemImport { private Regex _monthRegex = new Regex(@"\d{4}\\\d{2}"); public IList<BudgetItem> GetBudgetItems(string path) { // 读取Excel获取DataTable DataTable table = ExcelUtil.RenderFromExcel(path); // 获取表示月份的列名 IList<string> monthColumns = new List<string>(); for (var i = 0; i < table.Columns.Count; i++) { var columnName = table.Columns[i].ColumnName; if (_monthRegex.IsMatch(columnName)) { monthColumns.Add(columnName); } } // 遍历DataRow获取BudgetItems IList<BudgetItem> budgetItems = new List<BudgetItem>(); for (var i = 1; i < table.Rows.Count; i++) { // 获取DataRow DataRow dataRow = table.Rows[i]; // 创建BudgetItem对象,并设置部门和科目信息 BudgetItem budgetItem = new BudgetItem { Dept = dataRow[0].ToString(), Account = dataRow[1].ToString() }; // 创建BudgetItemDetail集合 IList<BudgetItemDetail> budgetItemDetails = new List<BudgetItemDetail>(); foreach (var column in monthColumns) { // 创建BudgetItemDetail对象,并设置预算月份和相应金额 BudgetItemDetail detail = new BudgetItemDetail { Month = column, Amount = Convert.ToDecimal(dataRow[column]) }; budgetItemDetails.Add(detail); } budgetItem.BudgetItemDetails = budgetItemDetails; budgetItems.Add(budgetItem); } return budgetItems; } }
以上这段代码,如果没有这些注释,GetBudgetItems()
方法是比较难以读懂的。
接下来,我们采用“分解方法”这个策略来对它重构。
第一次重构
我们粗略分析一下,可以得知GetBudgetItems()
方法一共做了3件事情,下图阐述了它的逻辑。
秉承着“一个方法只做一件事情”的原则,我们将这3件事情拆分出来,使其变成3个方法。
public class BudgetItemImport { private Regex _monthRegex = new Regex(@"\d{4}\\\d{2}"); public IList<BudgetItem> GetBudgetItems(string path) { // 读取Excel获取DataTable DataTable table = ExcelUtil.RenderFromExcel(path); // 获取表示月份的列名 IList<string> monthColumns = GetMonthColumns(table.Columns); // 读取DataTable获取BudgetItem集合 return GetBudgetItemsFromDataTable(table, monthColumns); } // 获取表示月份的列名 private IList<string> GetMonthColumns(DataColumnCollection collection) { IList<string> monthColumns = new List<string>(); for (var i = 0; i < collection.Count; i++) { var columnName = collection[i].ColumnName; if (_monthRegex.IsMatch(columnName)) { monthColumns.Add(columnName); } } return monthColumns; } // 读取DataTable获取BudgetItem集合 private IList<BudgetItem> GetBudgetItemsFromDataTable(DataTable table, IList<string> monthColumns) { // 遍历DataRow获取BudgetItems IList<BudgetItem> budgetItems = new List<BudgetItem>(); for (var i = 1; i < table.Rows.Count; i++) { DataRow dataRow = table.Rows[i]; // 创建BudgetItem对象,并设置部门和科目信息 BudgetItem budgetItem = new BudgetItem { Dept = dataRow[0].ToString(), Account = dataRow[1].ToString() }; // 创建BudgetItemDetail集合,并设置每个BudgetItemDetail对象的月份和金额 IList<BudgetItemDetail> budgetItemDetails = monthColumns.Select(column => new BudgetItemDetail { Month = column, Amount = Convert.ToDecimal(dataRow[column]) }).ToList(); budgetItem.BudgetItemDetails = budgetItemDetails; budgetItems.Add(budgetItem); } return budgetItems; } }
第二次重构
虽然GetBudgetItems()
拆分成了3个方法,但新追加的GetBudgetItemsFromDataTable()
方法还是不具备良好的可读性,这个方法我们仍然需要借助注释才能读懂。
我们再具体分析这个方法内部的逻辑,GetBudgetItemsFromDataTable()
这个方法也做了3件事情,见下图:
按照这个更加明细的逻辑流程,我们将这3件事情再拆分出来。
public class BudgetItemImport { private Regex _monthRegex = new Regex(@"\d{4}\\\d{2}"); public IList<BudgetItem> GetBudgetItems(string path) { // 读取Excel获取DataTable DataTable table = ExcelUtil.RenderFromExcel(path); // 获取表示月份的列名 IList<string> monthColumns = GetMonthColumns(table.Columns); // 读取DataTable获取BudgetItem集合 return GetBudgetItemsFromDataTable(table, monthColumns); } // 获取表示月份的列名 private IList<string> GetMonthColumns(DataColumnCollection collection) { IList<string> monthColumns = new List<string>(); for (var i = 0; i < collection.Count; i++) { var columnName = collection[i].ColumnName; if (_monthRegex.IsMatch(columnName)) { monthColumns.Add(columnName); } } return monthColumns; } // 读取DataTable获取BudgetItem集合 private IList<BudgetItem> GetBudgetItemsFromDataTable(DataTable table, IList<string> monthColumns) { IList<BudgetItem> budgetItems = new List<BudgetItem>(); foreach (DataRow dataRow in table.Rows) { BudgetItem budgetItem = GetBudgetItemFromDataRow(dataRow, monthColumns); budgetItems.Add(budgetItem); } return budgetItems; } // 创建BudgetItem对象,并设置部门和科目信息 private BudgetItem GetBudgetItemFromDataRow(DataRow dataRow, IList<string> monthColumns) { BudgetItem budgetItem = new BudgetItem { Dept = dataRow[0].ToString(), Account = dataRow[1].ToString(), BudgetItemDetails = GetBudgetItemDetailsFromDataRow(dataRow, monthColumns) }; return budgetItem; } // 创建BudgetItemDetail集合,并设置每个BudgetItemDetail对象的月份和金额 private IList<BudgetItemDetail> GetBudgetItemDetailsFromDataRow(DataRow dataRow, IList<string> monthColumns) { return monthColumns.Select(column => new BudgetItemDetail { Month = column, Amount = Convert.ToDecimal(dataRow[column]), }).ToList(); } }
经过这次重构后,BudgetItemImport类的可读性已经很好了。每个方法都只做一件事情,每个方法都很短小,都不超过20行,我们甚至不需要为这些方法写注释了。
小结
在经历过两次重构后,我们得到了结构良好的代码。回顾这个示例的重构过程,我们可以用下面一副图来表示。
写代码和写别的东西很像。在写文章时,你先想什么就写什么,然后再打磨它。初稿也许丑陋无序,你就雕章琢句,直至达到你心目中的样子。
我们并不能直接写出结构和可读性良好的方法,一开始我们的方法写得复杂且冗长,包含了各种循环、判断、缩进和注释。
然后我们打磨这些代码,通过分解方法逐一解决这些问题。
- 2楼小米干饭
- 把一个大方法分解成一系列小方法,特别方便别人看你的代码。,有时候别人只是想看看你的程序大概做些什么事情,程序的流程是什么样的,但你把所有的东西都放在一个方法中,别人就只能一行一行读你的代码了。如果把这个大方法分解成一系列小方法,别人看看你的方法调用就知道你的程序在干什么。如果方法名、参数名起的好的话,连注释都省了。,,在基类中分解方法的话,有时会演变成了模板方法模式,让人很有点成就感。,,我觉得小方法还有两个好处:,1. 被重用的可能性更高。,2. 方便在子类中重写。我们正在维护的程序中有一个比较基本的类,很多类都从这个类派生。这个类中有一个很大的方法。有时候这个方法中只有一、两行对派生类不适合,但在派生类中你只能重新整个方法,造成大量的 copy/paste,谁看见了谁头疼。如果这个大方法是由一系列小方法组成的话,在派生类中只要重写需要变化的那个小方法就行了。,,等着看你的下一篇呢。
- Re: keepfool
- @小米干饭,是的,“小方法”再加上一些合适的参数结构、命名,可以很清晰地知道程序是做什么事情的。,,基类中的一些“大方法”,如果有部分细节不能满足所有的派生类,你可以尝试做一些重载。,另外,你可以从类的职责角度深入分析,如果这个“大方法”可以再分离出来,使用聚合的方式会更佳。在一些更复杂的业务中,分离出来的可以是接口,根据不同场景提供接口不同的实现。,,谢谢支持!
- 1楼摆脱菜鸟
- 不错,小方法易维护,容易读,容易改。