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

事实上,去掉依赖及写测试须要一点时间,不少状况下,人们会选择节省时间的方式(省去测试)。
html

写测试状况的时间花销:java

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

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

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

这种状况须要花掉的时间由2部分组成:this

  • 定位问题的时间开销;spa

  • 修复问题的时间开销;翻译

次数呢?之后可能也要改这段代码。设计

为了下降从此的成本,这样作是有必要的。修改代码的难度可能从代码量的指数量级变成了线性的。code

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

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

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


一、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;
    }
}

固然,修改以后临时变量仍是存在的。


二、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,书中举了个TaxCalculator的例子,由于税的减免是跟日期有关的,须要在TaxCalculator中加一个日期检测功能吗,这并非该类的主要职责,因此仍是增长一个类吧,


Sprout Method/Class步骤对比:

Sprout Method Steps Sprout 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.


三、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方法。


四、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:

一、Design Smell: Temporal Coupling by Mark Seemann

相关文章
相关标签/搜索