【2016-03-26】《修改代码的艺术》:Sprout & Wrap

事实上,去掉依赖及写测试需要一点时间,很多情况下,人们会选择节省时间的方式(省去测试)。

写测试情况的时间花销:

  • 为要修改的代码写测试,花掉2小时;

  • 修改这部分代码,花掉15分钟;

表面看起来浪费了2个小时,实际上不是这样的,因为你不会知道不写测试然后出bug了要花掉多少时间(Pay now or pay more later)。

这种情况需要花掉的时间由2部分组成:

  • 定位问题的时间开销;

  • 修复问题的时间开销;

次数呢?以后可能也要改这段代码。

为了降低今后的成本,这样做是有必要的。修改代码的难度可能从代码量的指数量级变成了线性的。

当然,要实践这件事情开始的时候是有难度的,需要跨越一个驼峰(hump),但是之后,你就不会愿意回到原来直接改代码的情形了。

Remember, code is your house, and you have to live in it.

本章前半部分作者想说明写测试代码的必要性,剩下的部分用来介绍方法。


1、Sprout Method(萌芽方法)

原代码:

public class TransactionGate
{
    public void postEntries(List entries) {
        for (Iterator it = entries.iterator(); it.hasNext(); ) {
            Entry entry = (Entry)it.next();
            entry.postDate();
        }
        transactionBundle.getListManager().add(entries);
    }
    ... 
}

现在要做的改变:

需要在把entity加到transactionBundle里之前校验下该entity是否已经在transactionBundle中,不要重复添加

修改后的代码看起来是这样的:

public class TransactionGate
{
    public void postEntries(List entries) {
        List entriesToAdd = new LinkedList();
        for (Iterator it = entries.iterator(); it.hasNext(); ) {
            Entry entry = (Entry)it.next();
            // 新增 start
            if (!transactionBundle.getListManager().hasEntry(entry) {
                 entry.postDate();
                 entriesToAdd.add(entry);
            }
            // 新增 end
        }
        transactionBundle.getListManager().add(entriesToAdd);
    }
    ... 
}

修改很简单,但问题有以下几点:

  • 新代码和旧代码是混合在for循环里的,并没有隔开

  • 循环实现了两个功能:postDate和重复性检测。

  • 引入临时变量entriesToAdd。

如果下次需要修改代码,对非重复的entity做一些操作,那这些代码就只能放在这个方法中了,方法会越来越大,越来越复杂。

我们可以TDD新增一个方法uniqueEntries实现重复性检测功能,修改后的代码如下:

public class TransactionGate
{
    ...
    public void postEntries(List entries) {
        List entriesToAdd = uniqueEntries(entries);
        for (Iterator it = entriesToAdd.iterator(); it.hasNext(); ) {
            Entry entry = (Entry)it.next();
            entry.postDate();
        }
        transactionBundle.getListManager().add(entriesToAdd);
    }
    ... 
    List uniqueEntries(List entries) {
        List result = new ArrayList();
        for (Iterator it = entries.iterator(); it.hasNext(); ) {
            Entry entry = (Entry)it.next();
            if (!transactionBundle.getListManager().hasEntry(entry) {
                result.add(entry);
            }
        }
        return result;
    }
}

当然,修改之后临时变量还是存在的。


2、Sprout Class:

原代码(C++):

std::string QuarterlyReportGenerator::generate()
{
    std::vector<Result> results = database.queryResults(beginDate, endDate);
    std::string pageText;
    pageText += "<html><head><title>"
            "Quarterly Report"
            "</title></head><body><table>";
    if (results.size() != 0) {
        for (std::vector<Result>::iterator it = results.begin();it != results.end();++it) {
            pageText += "<tr>";
            pageText += "<td>" + it->department + "</td>";
            pageText += "<td>" + it->manager + "</td>";
            char buffer [128];
            sprintf(buffer, "<td>$%d</td>", it->netProfit / 100);
            pageText += std::string(buffer);
            sprintf(buffer, "<td>$%d</td>", it->operatingExpense / 100);
            pageText += std::string(buffer);
            pageText += "</tr>";
        }
    } else {
        pageText += "No results for this period";
    }
    pageText += "</table>";
    pageText += "</body>";
    pageText += "</html>";
    return pageText;
}

我们现在要做的是给HTML table加一个header,像这样:

<tr><td>Department</td><td>Manager</td><td>Profit</td><td>Expenses</td></tr>

假设QuarterlyReportGenerator是个超大的类,要把它放到test harness需要一天的时间,这是我们不能接受的。

我们可以在一个小的类QuarterlyReportTableHeaderProducer实现这个修改。

using namespace std;
class QuarterlyReportTableHeaderProducer
{
public:
    string makeHeader();
};
string QuarterlyReportTableProducer::makeHeader()
{
    return "<tr><td>Department</td><td>Manager</td>"
        "<td>Profit</td><td>Expenses</td>";
}

然后直接在QuarterlyReportGenerator::generate()中增加以下两行:

QuarterlyReportTableHeaderProducer producer;
pageText += producer.makeHeader();

到这就该有疑问了,真的要为这个小改动加一个类吗?这并不会改善设计!

作者的回答是:我们做了这么多就是为了去掉不好的依赖情况。让我们在仔细想一下,如果把QuarterlyReportTableHeaderProducer重命名为QuarterlyReportTableHeaderGenerator,并提供这样一个接口:

class QuarterlyReportTableHeaderGenerator
{
    public:
        string generate();
};

这时,就会有2个Generator的实现类,代码结构会变成这样:

class HTMLGenerator
{
    public:
        virtual ~HTMLGenerator() = 0;
        virtual string generate() = 0;
};
class QuarterlyReportTableHeaderGenerator : public HTMLGenerator
{
    public:
        ...
        virtual string generate();
        ...
};
class QuarterlyReportGenerator : public HTMLGenerator
{
    public:
        ...
        virtual string generate();
        ...
};

随着我们对做更多的工作,也许将来就可以对QuarterlyReportGenerator进行测试了。

Sprout Class的优势:

In C++, Sprout Class has the added advantage that you don't have to modify any existing header files to get your change in place. You can include the header for the new class in the implementation file for the source class. 

这就是为啥作者要举一个C++的例子吧。

Sprout Class的最大的缺点是会使程序更复杂,需要增加更多的抽象。


使用Sprout Class的场景:

1、要在现有类里加一个全新的职责;

2、就是本例中的情况,很难对现有类做测试。

对于1,书中举了个TaxCalculator的例子,因为税的减免是跟日期有关的,需要在TaxCalculator中加一个日期检测功能吗,这并不是该类的主要职责,所以还是增加一个类吧,


Sprout Method/Class步骤对比:

Sprout Method StepsSprout Class Steps
1. Identify where you need to make your code change.
2. If the change can be formulated as a single sequence of statements in one place in a method, write down a call for a new method that will do the work involved and then comment it out. (I like to do this before I even write the method so that I can get a sense of what the method call will look like in context.)2. If the change can be formulated as a single sequence of statements in one place in a method, think of a good name for a class that could do that work. Afterward, write code that would create an object of that class in that place, and call a method in it that will do the work that you need to do; then comment those lines out.
3. Determine what local variables you need from the source method, and make them arguments to the call/classes' constructor.

4. Determine whether the sprouted method will need to return values to source method. 

If so, change the call so that its return value is assigned to a variable.

4. Determine whether the sprouted class will need to return values to the source method. 

If so, provide a method in the class that will supply those values, and add a call in the source method to receive those values.

5. Develop the sprout method/class using test-driven development (88).
6. Remove the comment in the source method to enable the call/the object creation and calls.


3、Wrap Method:

设计的坏味道:Temporal Coupling

当你新建一个方法的时候,它的功能是很单一的。

之后,可能需要添加一些功能,这些功能恰好与现有功能在同一时间完成。

然后你就会图省事儿,直接把这段code添加到现有code周围。这件事做一次两次还好,多了就会引起麻烦。

这些代码纠缠在一起,但是他们的依赖关系并不强,因为一旦你要对一部分代码做改变,另一部分代码就会变成障碍,分开他们会变得困难。

我们可以使用Sprout Method来改进它,当然也可以使用其他的方式,比如Wrap Method。

我们来看一个例子,苦逼的员工晚上要加班,白天还要打卡,pay薪水的代码如下:

public class Employee
{
    ...
    public void pay() {
        Money amount = new Money();
        for (Iterator it = timecards.iterator(); it.hasNext(); ) {
            Timecard card = (Timecard)it.next();
            if (payPeriod.contains(date)) {
                amount.add(card.getHours() * payRate);
            }
        }
        payDispatcher.pay(this, date, amount);
    }
}

当我们需要在算薪水的时候需要将员工名更新一个到file,以便出发送给报表软件。最简单的方式是把代码加到pay方法里,但是本书推荐使用下面这种方式:

public class Employee
{
    private void dispatchPayment() {    // 重命名为dispatchPayment,并设为private
        Money amount = new Money();
        for (Iterator it = timecards.iterator(); it.hasNext(); ) {
            Timecard card = (Timecard)it.next();
            if (payPeriod.contains(date)) {
                amount.add(card.getHours() * payRate);
            }
        }
        payDispatcher.pay(this, date, amount);
    }
    public void pay() {
        logPayment();
        dispatchPayment();
    }
    private void logPayment() {
    ...
    } 
}

这个就叫做Wrap Method. We create a method with the name of the original method and have it delegate to our old code.(还是觉得不翻译的比较好)

以下是另外一种实现形式:

public class Employee
{
    public void makeLoggedPayment() {
        logPayment();
        pay(); 
    }
    public void pay() {
        ...
    }
    private void logPayment() {
        ...
    } 
}

两种的区别可以感受下~

dispatchPayment方法其实还做了calculatePay的事,我们可以进一步做如下修改:

public void pay() {
    logPayment();
    Money amount = calculatePay();
    dispatchPayment(amount);
}

当然,如果你的方法没有那么复杂,可以使用后文提到的Extract Method方法。


4、Wrap Class

Wrap Method上升到类级别就是Wrap Class。如果要对系统增加一个功能,可以加到另外一个类里。

刚才的Employee问题可以这样实现:

class LoggingEmployee extends Employee
{
    public LoggingEmployee(Employee e) {
        employee = e;
    }
    public void pay() {
        logPayment();
        employee.pay();
    }
    private void logPayment() {
        ...
    }
    ... 
}

这就叫做decorator pattern。


The Decorator Pattern:装饰模式

TO BE CONTINUED……


refer:

1、Design Smell: Temporal Coupling by Mark Seemann


转载于:https://my.oschina.net/rathan/blog/648106

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值