【2016-03-26】『コード修正のアート』:Sprout&Wrap

10592 ワード

実際、依存や書き込みテストを取り除くには少し時間がかかり、多くの場合、時間を節約する方法(テストを省く)を選択します.
テストの状況を書くのに時間がかかります.
  • 修正するコードのためにテストを書いて、2時間かかりました.
  • このコードを修正するのに15分かかりました.

  • 表面は2時間も無駄に見えますが、実際にはそうではありません.テストを書かずにバグを出すのにどれだけの時間がかかるか分からないからです(Pay now or pay more later).
    この場合、かかる時間は2つの部分から構成されています.
  • 位置決め問題の時間オーバーヘッド;
  • 問題を修復する時間のオーバーヘッド;

  • 回数は?後でこのコードを変更するかもしれません.
    今後のコストを下げるためには、このようにする必要があります.コードの修正の難しさは、コード量の指数級から線形になった可能性があります.
    もちろん、このことを実践するのは最初は難しいので、ラクダの峰を越える必要がありますが、その後、コードを直接変更する状況に戻りたくありません.
    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と繰返し検出の2つの機能を実現した.
  • は一時変数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に置くには1日かかります.これは私たちには受け入れられません.
    この修正は小さなクラス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()に次の2行を直接追加します.
    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 Couppling
    新しい方法を作成すると、その機能は単一です.
    その後、既存の機能と同じ時間に完了する機能を追加する必要がある場合があります.
    そして、手間を省いて、このコードを既存のコードの周りに直接追加します.このことは一度や二度やったほうがいいが,多くなると面倒になる.
    これらのコードは絡み合っていますが、彼らの依存関係は強くありません.一部のコードを変更すると、もう一部のコードが障害になり、分離が難しくなります.
    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() {
            ...
        } 
    }

    2つの違いが感じられます~
    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