文档章节

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

rathan0
 rathan0
发布于 2016/03/27 13:20
字数 2075
阅读 59
收藏 0

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

写测试情况的时间花销:

  • 为要修改的代码写测试,花掉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 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.


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


© 著作权归作者所有

共有 人打赏支持
rathan0
粉丝 4
博文 66
码字总数 36282
作品 0
程序员
Fly的狐狸/Jflyfox

个人博客 博客地址:http://www.jflyfox.com/blog 本网站后台基于Jfinal开发,模板基于beetl,数据库为Mysql。 老版本地址如下 演示地址:http://jflyfox.oschina.mopaas.com/ 管理地址:htt...

Fly的狐狸
2014/11/04
0
0
spring4+jdk8+tomcat8启动报错,求解

2016-06-03 21:39:26,712 [localhost-startStop-1] DEBUG [org.springframework.beans.factory.support.DefaultListableBeanFactory] - Creating instance of bean 'org.springframework.con......

hou_1993
2016/06/03
1K
2
tail,head,grep

1 tail tail -n 2 1.txt 表示显示文件后两行 [root@myvm tmp]# cat 1.txt|tail -n 2 manages managen 2 head -n 3 1.txt表示显示文件前3行 [root@myvm tmp]# cat 1.txt|head -n 3 Fri Sep 3......

wzl_up
2016/09/30
21
0
老男孩51CTO博客博文列表整理版20170620更新

老男孩51CTO博客博文列表整理版 (本文原自于一道考试题http://oldboy.blog.51cto.com/2561410/1860985) 老男孩教育运维脱产班35期 刘同学 2017-06-14 17:44:41 老男孩的MySQL私房菜新书视频1...

老男孩oldboy
2016/10/14
0
0
2016大数据唯一完整版教程

大数据教程(2016版) 链接: https://pan.baidu.com/s/1qXT9WwG 密码: qrht 2015-12-22_linux 2015-12-24_linux 2015-12-29_MapReduce 2015-12-31_MapReduce 2016-01-05_MapReduce 2016-01-0......

大象分享
2017/02/15
1K
0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

弹性工作制的魔咒

简评:你找到了一份完美的工作 —— 可以提前离开公司,还可以在晚上从家里回复邮件。既然如此,你为什么还会有那么强的负罪感呢? 或许是弹性工作制魔咒在作祟。 很多享受弹性工作制的人会始...

极光推送
7分钟前
0
0
KAFKA介绍(分布式架构)

Kafka是一个分布式的、可分区的、可复制的消息系统。它提供了普通消息系统的功能,但具有自己独特的设计。这个独特的设计是什么样的呢? 首先让我们看几个基本的消息系统术语: Kafka将消息以...

明理萝
13分钟前
0
1
os::NodeHandle::subscribe回调函数绑定对象

void Foo::callback(const std_msgs::Empty::ConstPtr& message){}Foo foo_object;ros::Subscriber sub = handle.subscribe("my_topic", 1, &Foo::callback, &foo_object); 参考: ht......

itfanr
15分钟前
0
0
React16.4 开发简书项目 从零基础入门到实战

React16.4 开发简书项目 从零基础入门到实战 关注我的订阅号下载 React16.4 开发简书项目 从零基础入门到实战

蜗牛奔跑
17分钟前
0
0
day57-20180815-流利阅读笔记-待学习

社恐怎么办?这个漫画或许能治愈你 毛西 2018-08-15 1.今日导读 近日,芬兰漫画家卡罗利娜·科尔霍宁创作的绘本《芬兰人的噩梦》在中国大火。一时间书中的主人公马蒂成为了人们茶余饭后热议的...

aibinxiao
21分钟前
3
0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部