標籤:
概述
“分解方法”的思想和前面講到的“提取方法”、“提取方法對象”基本一致。
它是將較大個體的方法不斷的拆分,讓每個“方法”做單一的事情,從而提高每個方法的可讀性和可維護性。
分解方法可以看做是“提取方法”的遞迴版本,它是對方法反覆提煉的一種重構策略。
分解方法
表示了這個重構策略,第1次提煉和第2次提煉都採用了“提取方法”這個策略。
何時分解方法?
“分解方法”最終可以讓方法的可讀性極大地增強,通常我們可以依據以下幾點來辨別方法是否需要分解:
1. 每個方法應該只做一件事情(對事情的理解程度,決定了事情的粒度)
2. 方法應該盡量短小,方法最好不要超過20行(依不同情況,酌情考慮行數)
3. 方法的縮排層次不宜太多,最好不要超過兩級
4. 方法需要太多的注釋才能理解 樣本
在企業年度預算的情境中,使用者需要按照如下Excel模板填寫科目、部門、各月的預算資料,然後將Excel檔案匯入到“預算系統”。
為了表示使用者填寫的每一行預算資料,開發人員在系統中設計了兩個class:BudgetItem(預算項)和BudgetItemDetail(預算項明細)。
紅色方框標註的表示一個BudgetItem對象,每個藍色方框則對應一個BudgetItemDetail對象。
BudgetItem.cs和BudgetItemDetail.cs
/// <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件事情拆分出來。
隱藏代碼
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; } }二次重構
雖然拆分成了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行,我們甚至不需要為這些方法寫注釋了。
小結
在經曆過兩次重構後,我們得到了結構良好的代碼。回顧這個樣本的重構過程,我們可以用下面一副圖來表示。
寫代碼和寫別的東西很像。在寫文章時,你先想什麼就寫什麼,然後再打磨它。初稿也許醜陋無序,你就雕章琢句,直至達到你心目中的樣子。
我們並不能直接寫出結構和可讀性良好的方法,一開始我們的方法寫得複雜且冗長,包含了各種迴圈、判斷、縮排和注釋。
然後我們打磨這些代碼,通過分解方法逐一解決這些問題。
小酌重構系列[4]——分解方法