問題背景#
這是 legacy 系統中最令人沮喪的事情之一:你需要做一個修改,以為「就這樣了」,然後發現有十幾個地方需要做同樣的修改。
如果你了解 refactoring,你就知道移除重複不需要是大工程——可以在日常工作中以小塊的方式進行。只要人們不持續引入新的重複,系統就會隨時間改善。
關鍵問題是:當我們徹底地把一個區域的重複消除,結果會怎樣?答案往往令人驚喜。
範例:Command 類別的重複#
一個 Java 網路系統需要發送 command 到 server。有兩個 command class:AddEmployeeCmd 和 LoginCommand。
初始狀態#
兩個 class 各自獨立,幾乎沒有共享程式碼:
public class AddEmployeeCmd {
String name;
String address;
String city;
String state;
String yearlySalary;
private static final byte[] header = {(byte)0xde, (byte)0xad};
private static final byte[] commandChar = {0x02};
private static final byte[] footer = {(byte)0xbe, (byte)0xef};
private static final int SIZE_LENGTH = 1;
private static final int CMD_BYTE_LENGTH = 1;
public void write(OutputStream outputStream)
throws Exception {
outputStream.write(header);
outputStream.write(getSize());
outputStream.write(commandChar);
outputStream.write(name.getBytes());
outputStream.write(0x00);
outputStream.write(address.getBytes());
outputStream.write(0x00);
// ... 更多欄位 ...
outputStream.write(footer);
}
}LoginCommand 的結構幾乎完全一樣,只是欄位不同(userName、passwd)且 commandChar 不同。

Figure 21.1: AddEmployeeCmd and LoginCommand
乍看之下,重複量不大。但讓我們一步一步地移除,看看最終結果。
First Steps#
原則:從小處開始#
面對重複時,作者的第一反應是退後一步看全局。但隨即意識到自己過度思考了。先移除小片段的重複,這會讓大片段的重複更容易被看見。
第一步:抽取 writeField 方法#
在 LoginCommand 的 write 方法中,每寫一個字串都跟著一個 null 字元(0x00)。把這個模式抽取為 writeField:
void writeField(OutputStream outputStream, String field) {
outputStream.write(field.getBytes());
outputStream.write(0x00);
}替換後 LoginCommand.write 變成:
public void write(OutputStream outputStream) throws Exception {
outputStream.write(header);
outputStream.write(getSize());
outputStream.write(commandChar);
writeField(outputStream, userName);
writeField(outputStream, passwd);
outputStream.write(footer);
}第二步:引入 Command 超類別#
因為兩個 class 都是 command,引入一個 Command 超類別,將 writeField 上拉:

Figure 21.2: Command hierarchy
// Command 超類別
class Command {
protected void writeField(OutputStream outputStream,
String field) {
outputStream.write(field.getBytes());
outputStream.write(0x00);
}
}
Figure 21.3: Pulling up writeField
第三步:抽取 writeBody#
兩個 class 的 write 方法有相同的結構:write header → write size → write commandChar → write 欄位 → write footer。差異只在「write 欄位」的部分。將這部分抽取為 writeBody:
// LoginCommand
private void writeBody(OutputStream outputStream) throws Exception {
writeField(outputStream, userName);
writeField(outputStream, passwd);
}當兩個方法看起來大致相同時,抽取差異的部分到其他方法中。這樣就能讓它們變得完全相同,然後消除其中一個。
第四步:上拉共同資料#
header、footer、SIZE_LENGTH、CMD_BYTE_LENGTH 在兩個 class 中的值完全相同。將它們上拉到 Command,改為 protected。
對於值不同的 commandChar,引入 abstract getter:
public class Command {
protected static final byte[] header = {(byte)0xde, (byte)0xad};
protected static final byte[] footer = {(byte)0xbe, (byte)0xef};
protected static final int SIZE_LENGTH = 1;
protected static final int CMD_BYTE_LENGTH = 1;
protected abstract char[] getCommandChar();
protected abstract void writeBody(OutputStream outputStream);
// ...
}第五步:上拉 write 方法#
現在 write 方法在兩個 class 中完全相同,可以上拉到 Command:
public void write(OutputStream outputStream) throws Exception {
outputStream.write(header);
outputStream.write(getSize());
outputStream.write(commandChar);
writeBody(outputStream);
outputStream.write(footer);
}第六步:統一 getSize#

Figure 21.4: Pulling up getSize
兩個 class 的 getSize 都是計算 header + SIZE_LENGTH + CMD_BYTE_LENGTH + footer + body size。抽取 getBodySize 讓差異更明顯,然後上拉 getSize:
// Command
private int getSize() {
return header.length + SIZE_LENGTH
+ CMD_BYTE_LENGTH + footer.length
+ getBodySize();
}第七步:消除 getBodySize 中的重複#
getBodySize 計算每個 field 的 getBytes().length + 1。抽取 getFieldSize:
protected int getFieldSize(String field) {
return field.getBytes().length + 1;
}第八步:泛化——使用 List#
兩個 class 都接受一組參數、計算大小、寫出去。唯一的差異是 commandChar。在 base class 中宣告一個 fields list,讓子類別在 constructor 中加入欄位:
class LoginCommand extends Command {
public LoginCommand(String userName, String passwd) {
fields.add(userName);
fields.add(passwd);
}
protected char[] getCommandChar() {
return new char[] { 0x01 };
}
}getBodySize 和 writeBody 都變成基於 list 的迴圈,可以上拉到 Command。
最終結果#
public class Command {
private static final byte[] header = {(byte)0xde, (byte)0xad};
private static final byte[] footer = {(byte)0xbe, (byte)0xef};
private static final int SIZE_LENGTH = 1;
private static final int CMD_BYTE_LENGTH = 1;
protected List fields = new ArrayList();
protected abstract char[] getCommandChar();
private void writeBody(OutputStream outputStream) { /* 迴圈 fields */ }
private int getFieldSize(String field) { return field.getBytes().length + 1; }
private int getBodySize() { /* 迴圈 fields */ }
private int getSize() { /* 計算總大小 */ }
private void writeField(OutputStream outputStream, String field) { /* ... */ }
public void write(OutputStream outputStream) throws Exception {
outputStream.write(header);
outputStream.write(getSize());
outputStream.write(commandChar);
writeBody(outputStream);
outputStream.write(footer);
}
}子類別變得極為精簡——只有 constructor(設定 fields)和 getCommandChar()。

Figure 21.5: Command hierarchy with duplication pulled up
移除重複的好處#
新增 Command 變得簡單#
要新增一個 command,只需繼承 Command、在 constructor 中加入 fields、實作 getCommandChar()。不再需要 copy/paste 大量重複程式碼。
彈性未減#
如果需要發送非字串的欄位怎麼辦?AddEmployeeCommand 已經展示了做法——在 constructor 中轉換成字串。如果需要完全不同格式的 command 怎麼辦?
public class AggregateCommand extends Command {
private List commands = new ArrayList();
protected char[] getCommandChar() {
return new char[] { 0x03 };
}
public void appendCommand(Command newCommand) {
commands.add(newCommand);
}
protected void writeBody(OutputStream out) {
out.write(commands.getSize());
for (Iterator it = commands.iterator(); it.hasNext(); ) {
Command innerCommand = (Command)it.next();
innerCommand.write(out);
}
}
}如果沒有先移除重複,實作這種 composite command 會困難得多。
Orthogonality(正交性)#
當你移除跨 class 的重複時,最終會得到非常小且聚焦的方法。每個方法做的事情沒有其他方法在做——這給了我們極大的優勢:正交性。改變一個方法的行為只影響一件事,不會波及其他。
命名一致性#
作者也提到要將 AddEmployeeCmd 重新命名為 AddEmployeeCommand——保持子類別命名一致,減少困惑。
Class 和 method 名稱中的縮寫是有問題的。它們可以接受,前提是使用一致。作者曾見過一個團隊同時使用
XXXXMgr和XXXXMngr來縮寫 manager,超過 50% 的時間你猜不到該用哪個後綴。
總結#
移除重複的核心策略:
- 從小處開始——先移除小片段的重複,讓大片段的重複顯現
- 抽取差異,統一相同——當兩個方法大致相同時,把不同的部分抽出來,讓它們完全相同
- 逐步上拉——將共同的方法和變數逐步上拉到超類別
- 泛化——尋找可以用更一般化的方式表達的模式(如使用 list 取代固定欄位)
- 享受成果——移除重複後,新增功能變得更簡單、更安全,程式碼獲得正交性