菜鳥譯文(二)——使用Java泛型構造模板方法模式

劉水鏡發表於2014-08-19

如果你發現你有很多重複的程式碼,你可能會考慮用模板方法消除容易出錯的重複程式碼。這裡有一個例子:下面的兩個類,完成了幾乎相同的功能:

  1. 例項化並初始化一個Reader來讀取CSV檔案;
  2. 讀取每一行並解析;
  3. 把每一行的字元填充到Product或Customer物件;
  4. 將每一個物件新增到Set裡;
  5. 返回Set。


正如你看到的,只有有註釋的地方是不一樣的。其他所有步驟都是相同的。

 

ProductCsvReader.java

public class ProductCsvReader {
 
    Set<Product> getAll(File file) throws IOException {
        Set<Product> returnSet = new HashSet<>();
        try (BufferedReader reader = new BufferedReader(new FileReader(file))){
            String line = reader.readLine();
            while (line != null && !line.trim().equals("")) {
                String[] tokens = line.split("\\s*,\\s*");
                //不同
                Product product = new Product(Integer.parseInt(tokens[0]), tokens[1], new BigDecimal(tokens[2]));
                returnSet.add(product);
                line = reader.readLine();
            }
        }
        return returnSet;
    }
}

 

CustomerCsvReader.java

public class CustomerCsvReader {
 
    Set<Customer> getAll(File file) throws IOException {
        Set<Customer> returnSet = new HashSet<>();
        try (BufferedReader reader = new BufferedReader(new FileReader(file))){
            String line = reader.readLine();
            while (line != null && !line.trim().equals("")) {
                String[] tokens = line.split("\\s*,\\s*");
                //不同
                Customer customer = new Customer(Integer.parseInt(tokens[0]), tokens[1], tokens[2], tokens[3]);
                returnSet.add(customer);
                line = reader.readLine();
            }
        }
        return returnSet;
    }
}

 

對於本例來說,只有兩個實體,但是一個真正的系統可能有幾十個實體,所以有很多重複易錯的程式碼。你可能會發現Dao層有著相同的情況,在每個Dao進行增刪改查的時候幾乎都是相同的操作,唯一與不同的是實體和表。讓我們重構這些煩人的程式碼吧。根據GoF設計模式第一部分提到的原則之一,我們應該“封裝不同的概念“ProductCsvReader和CustomerCsvReader之間,不同的是有註釋的程式碼。所以我們要做的是,把相同的放到一個類,不同的抽取到另一個類。我們先開始編寫ProductCsvReader,我們使用Extract Method提取帶註釋的部分:

 

ProductCsvReader.java after Extract Method

public class ProductCsvReader {
 
    Set<Product> getAll(File file) throws IOException {
        Set<Product> returnSet = new HashSet<>();
        try (BufferedReader reader = new BufferedReader(new FileReader(file))){
            String line = reader.readLine();
            while (line != null && !line.trim().equals("")) {
                String[] tokens = line.split("\\s*,\\s*");
                Product product = unmarshall(tokens);
                returnSet.add(product);
                line = reader.readLine();
            }
        }
        return returnSet;
    }

    Product unmarshall(String[] tokens) {
        Product product = new Product(Integer.parseInt(tokens[0]), tokens[1], 
                new BigDecimal(tokens[2]));
        return product;
    }
}

 

現在我們已經把相同(重複)的程式碼和不同(各自特有)的程式碼分開了,我們要建立一個父類AbstractCsvReader,它包括兩個類(ProductReader和CustomerReader)相同的部分。我們把它定義為一個抽象類,因為我們不需要例項化它。然後我們將使用Pull Up Method重構這個父類。

 

AbstractCsvReader.java

abstract class AbstractCsvReader {

    Set<Product> getAll(File file) throws IOException {
        Set<Product> returnSet = new HashSet<>();
        try (BufferedReader reader = new BufferedReader(new FileReader(file))){
            String line = reader.readLine();
            while (line != null && !line.trim().equals("")) {
                String[] tokens = line.split("\\s*,\\s*");
                Product product = unmarshall(tokens);
                returnSet.add(product);
                line = reader.readLine();
            }
        }
        return returnSet;
    }
}

 

ProductCsvReader.java after Pull Up Method

public class ProductCsvReader extends AbstractCsvReader {

    Product unmarshall(String[] tokens) {
       Product product = new Product(Integer.parseInt(tokens[0]), tokens[1], 
                new BigDecimal(tokens[2]));
        return product;
    }
}

 

如果在子類中沒有‘unmarshall’方法,該類就無法進行編譯(它呼叫unmarshall方法),所以我們要建立一個叫unmarshall的抽象方法

 

AbstractCsvReader.java with abstract unmarshall method

abstract class AbstractCsvReader {

    Set<Product> getAll(File file) throws IOException {
        Set<Product> returnSet = new HashSet<>();
        try (BufferedReader reader = new BufferedReader(new FileReader(file))){
            String line = reader.readLine();
            while (line != null && !line.trim().equals("")) {
                String[] tokens = line.split("\\s*,\\s*");
                Product product = unmarshall(tokens);
                returnSet.add(product);
                line = reader.readLine();
            }
        }
        return returnSet;
    }

    abstract Product unmarshall(String[] tokens);
}

 

現在,在這一點上,AbstractCsvReader是ProductCsvReader的父類,但不是CustomerCsvReader的父類。如果CustomerCsvReader繼承AbstractCsvReader編譯會報錯。為了解決這個問題我們使用泛型。

 

AbstractCsvReader.java with Generics

abstract class AbstractCsvReader<T> {

    Set<T> getAll(File file) throws IOException {
        Set<T> returnSet = new HashSet<>();
        try (BufferedReader reader = new BufferedReader(new FileReader(file))){
            String line = reader.readLine();
            while (line != null && !line.trim().equals("")) {
                String[] tokens = line.split("\\s*,\\s*");
                T element = unmarshall(tokens);
                returnSet.add(product);
                line = reader.readLine();
            }
        }
        return returnSet;
    }

    abstract T unmarshall(String[] tokens);
}

 

ProductCsvReader.java with Generics

public class ProductCsvReader extends AbstractCsvReader<Product> {

    @Override
    Product unmarshall(String[] tokens) {
       Product product = new Product(Integer.parseInt(tokens[0]), tokens[1], 
                new BigDecimal(tokens[2]));
        return product;
    }
}

 

CustomerCsvReader.java with Generics

public class CustomerCsvReader extends AbstractCsvReader<Customer> {

    @Override
    Customer unmarshall(String[] tokens) {
        Customer customer = new Customer(Integer.parseInt(tokens[0]), tokens[1], 
                tokens[2], tokens[3]);
        return customer;
    }
}

 

這就是我們要的!不再有重複的程式碼!父類中的方法是“模板”,它包含這不變的程式碼。那些變化的東西作為抽象方法,在子類中實現。記住,當你重構的時候,你應該有自動化的單元測試來保證你不會破壞你的程式碼。我使用JUnit,你可以使用我帖在這裡的程式碼,也可以在這個Github找一些其他設計模式的例子。在結束之前,我想說一下模板方法的缺點。模板方法依賴於繼承,患有 the Fragile Base Class Problem。簡單的說就是,修改父類會對繼承它的子類造成意想不到的不良影響。事實上,基礎設計原則之一的GoF設計模式提倡“多用組合少用繼承”,並且許多其他設計模式也告訴你如何避免程式碼重複,同時又讓複雜或容易出錯的程式碼儘量少的依賴繼承。歡迎交流,以便我可以提高我的部落格質量。


原文地址;Template Method Pattern Example Using Java Generics


翻譯的不好,歡迎拍磚!



相關文章