1. 程式人生 > >處理概括關係之十 :Form Template Method(塑造模板函式)

處理概括關係之十 :Form Template Method(塑造模板函式)

你有一些subclasses ,其中相應的某些函式以相同順序執行類似的措施,但各措施實際上有所不同。

將各個措施分別放進獨立函式中,並保持它們都有相同的簽名式(signature),於是原函式也就變得相同了。然後將原函式上移至superclass 。

動機(Motivation)

繼承是「避免重複行為」的一個強大工具。無論何時,只要你看見兩個subclasses 之中有類似的函式,就可以把它們提升到superclass 。但是如果這些函式並不完全相同呢?此時的你應該怎麼辦?我們仍有必要儘量避免重複,但又必須保持這些函 數之間的實質差異。

常見的一種情況是:兩個函式以相同序列(sequence)執行大致相近的措施,但是各措施不完全相同。這種情況下我們可以將「執行各措施」的序列移至superclass , 並倚賴多型(polymorphism )保證各措施仍得以保持差異性。這樣的函式被稱為Template Method (模板函式)[Gang of Four]。

作法(Mechanics)

· 在各個subclass 中分解目標函式,使分解後的各個函式要不完全相同,要不完全不同。
· 運用Pull Up Method 將各subclass 內寒全相同的函式上移至superclass 。
· 對於那些(剩餘的、存在於各subclasses 內的)完全不同的函式,實施Rename Method,使所有這些函式的簽名式(signature)完全相同。
Ø 這將使得原函式變為完全相同,因為它們都執行同樣一組函式呼叫; 但各subclass 會以不同方式響應這些呼叫。
· 修改上述所有簽名式後,編譯並測試。
· 運用Pull Up Method 將所有原函式上移至superclass 。在superclass 中將那些「有所不同、代表各種不同措施」的函式定義為抽象函式。
· 編譯,測試。
· 移除其他subclass 中的原函式,每刪除一個,編譯並測試。

範例:(Example)

現在我將完成第一章遺留的那個範例。在此範例中,我有一個Customer ,其中有兩個用於列印的函式。statement() 函式以ASCII 碼列印報表(statement):

   public String statement() {

      Enumeration rentals = _rentals.elements();

       String result = "Rental Record for " + getName() + "\n";

       while (rentals.hasMoreElements()) {

           Rental each = (Rental) rentals.nextElement();

           //show figures for this rental

           result += "\t" + each.getMovie().getTitle()+ "\t" +

               String.valueOf(each.getCharge()) + "\n";

       }

       //add footer lines

       result +=  "Amount owed is " + String.valueOf(getTotalCharge()) + "\n";

       result += "You earned " + String.valueOf(getTotalFrequentRenterPoints()) +

            " frequent renter points";

       return result;

   }

函式htmlStatement() 則以HTML 格式輸出報表:

   public String htmlStatement() {

       Enumeration rentals = _rentals.elements();

       String result = "<H1>Rentals for <EM>" + getName() + "</EM></H1><P>\n";

       while (rentals.hasMoreElements()) {

           Rental each = (Rental) rentals.nextElement();

           //show figures for each rental

           result += each.getMovie().getTitle()+ ": " +

               String.valueOf(each.getCharge()) + "<BR>\n";

       }

       //add footer lines

       result +=  "<P>You owe <EM>" + String.valueOf(getTotalCharge()) + "</EM><P>\n";

       result += "On this rental you earned <EM>" +

           String.valueOf(getTotalFrequentRenterPoints()) +

           "</EM> frequent renter points<P>";

       return result;

   }

使用 Form Template Method 之前,我需要對上述兩個函式做一些整理,使它們成為「某個共同superclass 」下的subclass 函式。為了這一目的,我使用函式物件(method object)[Beck] 針對「報表列印工作」建立一個「獨立的策略繼承體系」(separate strategy hierarchy ),如圖11.1。

圖11.1  針對「報表輸出」使用Stategy 模式

class Statement {}

class TextStatement extends Statement {}

class HtmlStatement extends Statement {}

現在,通過Move Method,我將兩個負責輸出報表的函式分別搬移到對應的subclass  中:

class Customer...

public String statement() {

   return new TextStatement().value(this);

}

public String htmlStatement() {

   return new HtmlStatement().value(this);

}

class TextStatement {

  public String value(Customer aCustomer) {

        Enumeration rentals = aCustomer.getRentals();

        String result = "Rental Record for " + aCustomer.getName() + "\n";

        while (rentals.hasMoreElements()) {

            Rental each = (Rental) rentals.nextElement();

            //show figures for this rental

            result += "\t" + each.getMovie().getTitle()+ "\t" +

                String.valueOf(each.getCharge()) + "\n";

        }

        //add footer lines

        result +=  "Amount owed is " + String.valueOf(aCustomer.getTotalCharge()) + "\n";

        result += "You earned " + String.valueOf(aCustomer.getTotalFrequentRenterPoints()) +

          " frequent renter points";

        return result;

   }

class HtmlStatement {

  public String value(Customer aCustomer) {

        Enumeration rentals = aCustomer.getRentals();

        String result = "<H1>Rentals for <EM>" + aCustomer.getName() + "</EM></H1><P>\n";

        while (rentals.hasMoreElements()) {

            Rental each = (Rental) rentals.nextElement();

            //show figures for each rental

            result += each.getMovie().getTitle()+ ": " +

                        String.valueOf(each.getCharge()) + "<BR>\n";

        }

        //add footer lines

        result +=  "<P>You owe <EM>" + String.valueOf(aCustomer.getTotalCharge()) +

             "</EM><P>\n";

        result += "On this rental you earned <EM>"

                String.valueOf(aCustomer.getTotalFrequentRenterPoints()) +

                "</EM> frequent renter points<P>";

        return result;

}

搬移之後,我還對這兩個函式的名稱做了一些修改,使它們更好地適應Strategy 模式的要求。我之所以為它們取相同名稱,因為兩者之間的差異不在於函式,而在於函式所屬的class 。如果你想試著編譯這段程式碼,還必須在Customer class 中新增一個getRentals() 函式,並放寬getTotalCharge() 函式和getTotalFrequentRenterPoints() 函式的可視性(visibility )。

面對兩個subclass 中的相似函式,我可以開始實施Form Template Method 了。本重構的關鍵在於:運用 Extract Method 將兩個函式的不同部分提煉出 來,從而將相像的程式碼(similar code)和變動的程式碼( varying code )分開。每次提煉後,我就建立一個簽名式(signature)相同但本體(bodies)不同的函式。

第一個例子就是列印報表表頭(headers)。上述兩個函式都通過Customer 物件獲取資訊,但對運算結果(字串)的格式化方式不同。我可以將「對字串的格式化動作」提煉到獨立函式中,並將提煉所得命以相同的簽名式(signature):

class TextStatement...

  String headerString(Customer aCustomer) {

    return "Rental Record for " + aCustomer.getName() + "\n";

  }

  public String value(Customer aCustomer) {

        Enumeration rentals = aCustomer.getRentals();

        String result =headerString(aCustomer);

        while (rentals.hasMoreElements()) {

            Rental each = (Rental) rentals.nextElement();

            //show figures for this rental

            result += "\t" + each.getMovie().getTitle()+ "\t" +

                String.valueOf(each.getCharge()) + "\n";

        }

        //add footer lines

        result +=  "Amount owed is " + String.valueOf(aCustomer.getTotalCharge()) + "\n";

        result += "You earned " + String.valueOf(aCustomer.getTotalFrequentRenterPoints()) +

             " frequent renter points";

        return result;

   }

class HtmlStatement...

  String headerString(Customer aCustomer) {

        return "<H1>Rentals for <EM>" + aCustomer.getName() + "</EM></H1><P>\n";

}

public String value(Customer aCustomer) {

      Enumeration rentals = aCustomer.getRentals();

      String result = headerString(aCustomer);

      while (rentals.hasMoreElements()) {

          Rental each = (Rental) rentals.nextElement();

          //show figures for each rental

          result += each.getMovie().getTitle()+ ": " +

                      String.valueOf(each.getCharge()) + "<BR>\n";

      }

      //add footer lines

    result +=  "<P>You owe <EM>" + String.valueOf(aCustomer.getTotalCharge()) + "</ EM><P>\n";

     result += "On this rental you earned <EM>" +

         String.valueOf(aCustomer.getTotalFrequentRenterPoints()) +

         "</EM> frequent renter points<P>";

     return result;

}

編譯並測試,然後繼續處理其他元素。我將逐一對各個元素進行上述過程。下面是整個重構完成後的結果:

class TextStatement …

  public String value(Customer aCustomer) {

      Enumeration rentals = aCustomer.getRentals();

      String result = headerString(aCustomer);

      while (rentals.hasMoreElements()) {

          Rental each = (Rental) rentals.nextElement();

          result += eachRentalString(each);

      }

      result += footerString(aCustomer);

      return result;

   }

String eachRentalString (Rental aRental) {

      return "\t" + aRental.getMovie().getTitle()+ "\t" +

          String.valueOf(aRental.getCharge()) + "\n";

    }

String footerString (Customer aCustomer) {

        return "Amount owed is " + String.valueOf(aCustomer.getTotalCharge()) + "\n" +

           "You earned " + String.valueOf(aCustomer.getTotalFrequentRenterPoints()) +

           " frequent renter points";

    }

class HtmlStatement…

  public String value(Customer aCustomer) {

        Enumeration rentals = aCustomer.getRentals();

        String result = headerString(aCustomer);

        while (rentals.hasMoreElements()) {

            Rental each = (Rental) rentals.nextElement();

            result += eachRentalString(each);

        }

        result += footerString(aCustomer);

        return result;

  }

  String eachRentalString (Rental aRental) {

      return aRental.getMovie().getTitle()+ ": " +

          String.valueOf(aRental.getCharge()) + "<BR>\n";

  }

  String footerString (Customer aCustomer) {

        return "<P>You owe <EM>" + String.valueOf(aCustomer.getTotalCharge()) +

        "</EM><P>

" + "On this rental you earned <EM>" +

        String.valueOf(aCustomer.getTotalFrequentRenterPoints()) +

          "</EM> frequent renter points<P>";

  }

所有這些修改都完成後,兩個value() 函式看上去已經非常相似了,因此我可以使用Pull Up Method 將它們提升到superclass 中。提升完畢後,我需要在superclass 中把subclass 函式宣告為抽象函式。

class Statement...

  public String value(Customer aCustomer) {

        Enumeration rentals = aCustomer.getRentals();

        String result = headerString(aCustomer);

        while (rentals.hasMoreElements()) {

            Rental each = (Rental) rentals.nextElement();

            result += eachRentalString(each);

        }

        result +=  footerString(aCustomer);

        return result;

    }

  abstract String headerString(Customer aCustomer);

  abstract String eachRentalString (Rental aRental);

  abstract String footerString (Customer aCustomer);

然後我把TextStatement.value() 函式拿掉,編譯並測試。完成之後再把HtmlStatement.value() 也刪掉,再次編譯並測試。最後結果如圖11.2。

完成本重構後,處理其他種類的報表就容易多了:你只需為Statement 再建一個subclass ,並在其中覆寫(overrides)三個抽象函式即可。

圖11.2  Templae Method(模板函式)塑造完畢後的classes