分解方法

分解方法

概述

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

分解方法

下图表示了这个重构策略,第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件事情拆分出来,使其变成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行,我们甚至不需要为这些方法写注释了。

小结

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

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

时间: 2024-11-05 05:52:50

分解方法的相关文章

小酌重构系列[4]&mdash;&mdash;分解方法

概述 "分解方法"的思想和前面讲到的"提取方法"."提取方法对象"基本一致.它是将较大个体的方法不断的拆分,让每个"方法"做单一的事情,从而提高每个方法的可读性和可维护性.分解方法可以看做是"提取方法"的递归版本,它是对方法反复提炼的一种重构策略. 分解方法 下图表示了这个重构策略,第1次提炼和第2次提炼都采用了"提取方法"这个策略. 何时分解方法? "分解方法"最终

基于One-Class的矩阵分解方法

在矩阵分解中. 有类问题比較常见,即矩阵的元素仅仅有0和1. 相应实际应用中的场景是:用户对新闻的点击情况,对某些物品的购买情况等. 基于graphchi里面的矩阵分解结果不太理想.调研了下相关的文献,代码主要实现了基于PLSA的分解方法,具体请參考后面的參考文献 #!/usr/local/bin/python #-*-coding:utf-8-*- import sys import math import numpy as np import string import random "&q

MPI程序的任务分解方法

用MPI编写并行程序时,任务分解是很重要的一部分,如何把T个任务(T块数据,T行矩阵等)分给P个进程,实现负载均衡,是需要好好考量的问题.分解任务时需要解决两个问题: 1.给出一个进程p,如何得知要处理的任务是哪些 2.给出一个任务t,如何得知它是由哪个进程处理的 (这里的p和t都是从0开始计数.) 一个好的任务分配,应该能够保证这两种计算都能高效完成.下面讨论三种分配方式.这里只讨论T>P的情况,T<=P时的分解方式是显而易见的. 一,交叉分解 对于进程p,它所处里的任务号为t=p+kP,其

重构第22天 分解方法(Break Method)

理解:如果一个功能,里面比较复杂,代码量比较多,我们就可以把这个功能分解成多个小的method,每个方法实现该功能的一个小小的部分,并且方法命名成容易理解,和方法内容相关的名称,更有助于维护和可读性提高. 详解: 重构前代码: 1 using System; 2 using System.Collections; 3 using System.Collections.Generic; 4 using System.Linq; 5 using System.Text; 6 7 namespace

BCNF范式及其分解方法(对一次Lab作业的总结)

一次Lab的作业题目如下: A luxury car-rental company has been tracking their rental data in a simple spreadsheet, but now wants to migrate to a proper database. Consider the table given below, containing some sample data. (a) Identify at least 10 functional dep

机器学习中的矩阵方法03:QR 分解

1. QR 分解的形式 QR 分解是把矩阵分解成一个正交矩阵与一个上三角矩阵的积.QR 分解经常用来解线性最小二乘法问题.QR 分解也是特定特征值算法即QR算法的基础.用图可以将分解形象地表示成: 其中, Q 是一个标准正交方阵, R 是上三角矩阵. 2. QR 分解的求解 QR 分解的实际计算有很多方法,例如 Givens 旋转.Householder 变换,以及 Gram-Schmidt 正交化等等.每一种方法都有其优点和不足.上一篇博客介绍了 Givens 旋转和 Householder

hdu 4704 Sum (整数和分解+快速幂+费马小定理降幂)

题意: 给n(1<n<),求(s1+s2+s3+...+sn)mod(1e9+7).其中si表示n由i个数相加而成的种数,如n=4,则s1=1,s2=3.                         (全题文末) 知识点: 整数n有种和分解方法. 费马小定理:p是质数,若p不能整除a,则 a^(p-1) ≡1(mod p).可利用费马小定理降素数幂. 当m为素数,(m必须是素数才能用费马小定理) a=2时.(a=2只是题中条件,a可以为其他值) mod m =  *      //  k=

分解整数

Description 作为Acmer的你现在接到任务,需要将整数n写成n=x1+x2+-..xk这种形式,其中xi(i>=1&&i<=k)要为奇数.例如n=5,可以写成 5=1+1+1+1+1,5=1+1+3,5=1+3+1,5=3+1+1,5=5这5中形式,求出n有多少种分解方法 Input 有多组输入,输入以文件结尾,每组输入一个整数n Output 输出分解方案的种数 Sample Input Original Transformed 4 5 Sample Output

基于两两交互张量分解模型的个性化标签推荐

基于PITF的个性化标签推荐 摘要 关键词 引言 相关工作 个性化标签推荐 非个性化标签推荐 张量分解模型 成对交互模型 个性化标签推荐 形式化定义 数据分析 标签推荐的贝叶斯个性化排序BPR BPR最优化准则 BPR学习算法 张量分解模型 塔克分解模型TDTF 规范化分解模型CDTF 成对交互张量分解模型PITF TDCD和PITF之间的关系 实验评价 数据集 评价方法 实验结果 学习运行时间 预测质量 ECMLPKDD 2009知识发现挑战赛 结论和未来工作 基于PITF的个性化标签推荐 摘