小酌重构系列[4]——分解方法

概述

“分解方法”的思想和前面讲到的“提取方法”、“提取方法对象”基本一致。
它是将较大个体的方法不断的拆分,让每个“方法”做单一的事情,从而提高每个方法的可读性和可维护性。
分解方法可以看做是“提取方法”的递归版本,它是对方法反复提炼的一种重构策略。

分解方法

下图表示了这个重构策略,第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行,我们甚至不需要为这些方法写注释了。

小结

在经历过两次重构后,我们得到了结构良好的代码。回顾这个示例的重构过程,我们可以用下面一副图来表示。

写代码和写别的东西很像。在写文章时,你先想什么就写什么,然后再打磨它。初稿也许丑陋无序,你就雕章琢句,直至达到你心目中的样子。
我们并不能直接写出结构和可读性良好的方法,一开始我们的方法写得复杂且冗长,包含了各种循环、判断、缩进和注释。
然后我们打磨这些代码,通过分解方法逐一解决这些问题。

时间: 2024-10-18 12:37:57

小酌重构系列[4]——分解方法的相关文章

小酌重构系列[19]——分解大括号

概述 if else, for, while等是程序中最常用的语句,这些语句有一个共同点——它们的逻辑都封装在一对“{}”包围的代码块中.在实现复杂的业务逻辑时,会较多地用到这些语句,可能会形成多层的代码嵌套.代码的嵌套层数越大,代码的缩进层次就越深,这将会降低代码的可读性.如下图所示,如果我们想理解绿色if代码块的逻辑,需要先了解前3个代码块是如何工作的. N层嵌套的代码不仅可读性差,也难以维护.当需要变更某一层的代码时,因前后层次的逻辑制约,很容易改出有问题的代码.本文要讲的“分解大括号”策

小酌重构系列[9]&mdash;&mdash;分解依赖

概述 编写单元测试有助于改善代码的质量,在编写单元测试时,某些功能可能依赖了其他代码(比如调用了其他组件).通常我们只想测试这些功能本身,而不想测试它所依赖的代码. 为什么呢?单元测试的目标是验证该功能是否正确,然而功能所依赖的代码是处于功能范围外的,这些代码可能是一些外部的组件,单元测试无法验证这些外部组件的准确性.单元测试因调用"依赖的代码"出错而失败时,会影响测试结果的判断,我们无法确定功能本身是否是正确的.也许功能是正确的,但调用依赖的代码出错时,这个单元测试仍然会被认为是失败

小酌重构系列[11]&mdash;&mdash;提取基类、提取子类、合并子类

概述 继承是面向对象中的一个概念,在小酌重构系列[7]--使用委派代替继承这篇文章中,我"父子关系"描述了继承,这是一种比较片面的说法.后来我又在UML类图的6大关系,描述了继承是一种"is a kind of"关系,它更偏向于概念层次,这种解释更契合继承的本质.本篇要讲的3个重构策略提取基类.提取子类.合并子类都是和继承相关的,如果大家对继承的理解已经足够深刻了,这3个策略用起来应该会得心应手. 提取基类 定义:如果有超过一个类有相似的功能,应该提取出一个基类,并

小酌重构系列[3]&mdash;&mdash;方法、字段的提升和降低

本文要介绍的是4种重构策略,它们分别是提升方法.降低方法.提升字段和降低字段.由于这4种重构策略具有一定的相通性,所以我将它们放到一篇来讲解. 定义 以下是这4种策略的定义 提升方法:当子类的方法描述了相同的行为时,应将这样的方法提升到基类.降低方法:在基类中的行为仅和个别子类相关时,应将这样的行为降低到子类.提升字段:当子类中的字段描述着相同的信息时,应将这样的字段提升到基类.降低字段:当基类中的字段仅仅用于个别子类时,应将这样的字段降低到子类. 以上的定义是较为为枯燥无趣的,各位读者大可不必

小酌重构系列目录汇总

为了方便大家阅读这个系列的文章,我弄了个目录汇总. 方法.字段重构 移动方法 (2016-04-24) 提取方法.提取方法对象 (2016-04-26) 方法.字段的提升和降低 (2016-05-01) 分解方法 (2016-05-02) 为布尔方法命名 (2016-05-03) 引入对象参数 (2016-05-04) 类.接口重构 使用委派代替继承 (2016-05-07) 提取接口 (2016-05-08) 解除依赖 (2016-05-09) 分离职责 (2016-05-11) 提取基类.提

小酌重构系列[2]&mdash;&mdash;提取方法、提取方法对象

前言 "艺术源于生活"--代码也源于生活,你在生活中的一些行为习惯,可能会恰如其分地体现在代码中.当实现较为复杂的功能时,由于它包含一系列的逻辑,我们倾向于编写一个"大方法"来实现.为了使项目便于维护,以及增强代码的可读性,我们有必要对"大方法"的逻辑进行整理,并提取出分散的"小方法".这就是本文要讲的两种重构策略:提取方法.提取方法对象. 如何快速地找到想读的书? 在生活中,我是一个比较随意的人,平时也买了不少书去看.我的书

小酌重构系列[22]——尽快返回

概述 阅读文章时,如果某个段落已经传达了关键信息,我们可能就不会逐字逐句地将文章读完,因为我们已经知道了这篇文章的核心内容.与此类似,如果方法中某些条件判断可以得到结果,我们应该尽快返回该结果. 尽快返回可以带来三个好处 节省阅读代码的时间——如果方法能够尽快返回,后面的代码逻辑可以不必阅读.见下图,如果①已经返回了,就不必阅读②部分的代码 避免执行无效的逻辑——如果方法能够尽快返回,后面的代码逻辑就不会被执行.见下图,如果①已经返回了,②部分的逻辑不会被执行 增强代码的可读性 在分解大括号这篇

小酌重构系列[6]&mdash;&mdash;引入参数对象

简述 如果方法有超过3个以上的参数,调用方法时就会显得冗词赘句.这时将多个参数封装成一个对象,调用方法会显得干净整洁.这就是本文要讲的重构策略"引入参数对象"--将方法的参数封为类,并用这个类的对象替换方法中原有的参数. 引入参数对象 下图演示了这个重构策略,OrderSerivce表示订单服务,GetOrders()方法根据一些条件获取订单信息.在重构前,GetOrders()方法看起来像长篇大论,调用时也颇为麻烦:在重构后,GetOrders()方法看起来言简意赅,调用时仅需要传入

小酌重构系列[15]&mdash;&mdash;策略模式代替分支

前言 在一些较为复杂的业务中,客户端需要依据条件,执行相应的行为或算法.在实现这些业务时,我们可能会使用较多的分支语句(switch case或if else语句).使用分支语句,意味着"变化"和"重复",每个分支条件都代表一个变化,每个分支逻辑都是相似行为或算法的重复.当追加新的条件时,我们需要追加分支语句,并追加相应的行为或算法. 上一篇文章"使用多态代替条件判断"中,我们讲到它可以处理这些"变化"和"重复&qu