如果你發現你有很多重複的程式碼,你可能會考慮用模板方法消除容易出錯的重複程式碼。這裡有一個例子:下面的兩個類,完成了幾乎相同的功能:
- 例項化並初始化一個Reader來讀取CSV檔案;
- 讀取每一行並解析;
- 把每一行的字元填充到Product或Customer物件;
- 將每一個物件新增到Set裡;
- 返回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
翻譯的不好,歡迎拍磚!