【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

相關文章
相關標籤/搜索