「MoreThanJava」Day 7:介面詳解

我沒有三顆心臟發表於2020-08-13

  • 「MoreThanJava」 宣揚的是 「學習,不止 CODE」,本系列 Java 基礎教程是自己在結合各方面的知識之後,對 Java 基礎的一個總回顧,旨在 「幫助新朋友快速高質量的學習」
  • 當然 不論新老朋友 我相信您都可以 從中獲益。如果覺得 「不錯」 的朋友,歡迎 「關注 + 留言 + 分享」,文末有完整的獲取連結,您的支援是我前進的最大的動力!

Part 1. 介面概述

Java 是單繼承的。這意味著子類僅從一個父類繼承。通常,這就是你需要的。有時候多繼承會提供方便,但也會造成混亂,例如,當繼承的兩個父類具有不同版本的簽名相同的兩個方法時該呼叫哪一個呢?

介面為 Java 提供了多繼承的一些優點,而沒有缺點。

介面的概念

在 Java 程式設計語言中,介面不是類,而是對希望符合這個介面的類的一組需求。

我們 之前 接觸的 抽象類,性格偏內向,描述的是一組相對具體的特徵,比如某品牌特定型號的汽車,底盤架構、控制電路、剎車系統等是抽象出來的共同特徵,但根據動感型、舒適型、豪華型的區分,內飾、車頭燈、螢幕等都可以存放不同版本的具體實現。

介面 是開放的,性格偏外向,它就像一份合同,定義了方法名、引數列表、返回值,甚至是丟擲異常的型別。誰都可以實現它,但如果想實現它的類就必須遵守這份介面約定的合同。

想一想比較熟悉的 USB 介面:它不僅僅約束了 U 盤 (實現類) 的大小和形狀,同樣也約束了電腦插槽 (使用類)。在程式設計中,介面類似。

介面的定義

在 Java 中使用 interface 關鍵字來定義介面。介面是頂級的 "類",雖然關鍵字是 interface,但編譯之後的位元組碼副檔名還是 .class。一個典型介面的結構如下:

public interface InterfaceName {
  	constant definitions
    method headers (without implementations).
}

比如,我們在 前面文章 討論「為什麼不推薦使用繼承?」中舉的鳥類的例子,任何能飛的鳥都必須實現如下介面:

public interface Flyable {
  	void fly();
}

介面中的所有方法都自動是 public。因此,在介面中宣告方法時,不必提供關鍵字 public(在 Java 9 中允許了介面定義宣告為 private 的方法,在這之前都是不允許的..)

想一想介面就像是合同一樣,所以任何不清晰的細節都是不允許的。因此,介面中只允許明確的方法定義和常量出現。(下方的例子中演示了一個不被允許的介面定義 —— 因為 y 變數沒有確定的值)

interface ErrorInterfaceDefine {
  public final int x = 32;
  public double y;   // No variables allowed

  public double addup();
}

這看起來有點兒像類的定義,但沒有任何物件能夠構建一個介面 (new一個介面.. 因為介面是絕對抽象的,不允許實現..),但你可以定義一個類實現 (關鍵字 impelents) 介面,一旦你這麼做了,你就可以構造這個 (實現介面的) 類的物件。

例如麻雀既能飛、也能叫、還能下蛋:(實現多個介面使用 , 分隔)

public class Sparrow impelents Flayable, Tweetable, EggLayable {//麻雀
  //... 省略其他屬性和方法...
  @Override
  public void fly() { //... }
  @Override
  public void tweet() { //... }
  @Override
  public void layEgg() { //... }
}

介面的屬性

介面不是類,不能使用 new 運算子例項化一個介面,但卻可以用來引用實現了這個介面的類物件:

Comparable x = new Employee(...); // OK provided Emloyee implements Comparable

與建立類的繼承層次一樣,也可以擴充套件介面!比如,假設這裡有一個名為 Moveable 的介面:

public interface Moveable {
  	void move(double x, double y);
}

然後,可以假設一個名為 Powered 的介面擴充套件了以上的 Moveable 介面:

public interface Powered extends Moveable {
  	double milesPerGallon();
}

雖然在介面中不能包含例項欄位,但是可以包含常量。比如:

public interface Powered extends Moveable {
  	double SPEED_LIMIT = 95;  // a public static final constant
  	double milesPerGallon();
}

另外有一些介面之定義了常量,而沒有定義方法。例如,標準庫中的 SwingConstants 就是這樣一個介面,其中只包含了 NORTHSOUTHHORIZONTAL 等常量。任何實現 SwingConstants 介面的類都自動地繼承了這些常量,並可以在方法中引用它們,而不必採用 SwingConstants.NORTH 這樣繁瑣的書寫形式。不過,這樣使用介面更像是退化,所以建議最好不要這樣使用...

SwingConstants 原始碼部分截圖

➡️ 一個類只能有一個父類,但可以實現很多個介面。這就為定義類的行為提供了極大的靈活性。(我們之前也討論過——在討論繼承的章節——這裡不再贅述)

靜態和私有方法

➡️ 在 Java 8 中,允許在介面中增加靜態方法 (允許不構建物件而直接使用的具體方法)。理論上講,沒有任何理由認為這是不合法的,只是這有違將介面作為抽象規範的初衷

目前為止,通常的做法都是將靜態方法放在 伴隨類 (可以理解為操作繼承介面的實用工具類) 中。在標準庫中,你可以看到成對出現的介面和實用工具類,如 Collection/ CollectionsPath/ Paths

Java 11 中,Path 介面就提供了一個與之工具類 Paths.get() 等價的方法 (該方法用於將一個 URI 或者字串序列構造成一個檔案或目錄的路徑)

public interface Path {
    public static Path of(String first, String... more) { ... }
    public static Path of(URI uri) { ... }
}

這樣一來,Paths 類就不再是必要的了。類似地,如果實現你自己的介面時,沒有理由再額外提供一個帶有實用方法的工具類。

➡️ 另外,在 Java 9 中,介面中的方法可以是 privateprivate 方法可以是靜態方法或例項方法。由於私有方法只能在介面本身的方法中使用,所以它們的用法很有限,只能作為介面中其他方法的輔助方法。

預設方法

Java 8 中,允許為介面方法提供一個預設的實現。必須用 default 修飾符標記這樣一個方法,例如 JDK 中的 Iterator 介面:

public interface Iterator<E> {
  	boolean hasNext();
  	E next();
  	default void remove() { throw new UnsupportedOperationExceition("remove"); }
}

這將非常有用!如果你要實現一個迭代器,就需要提供 hasNext()next() 方法。這些方法沒有預設實現——它們依賴於你要遍歷訪問的資料結構。不過,如果你的迭代器是 只讀 的,那麼就不用操心實現 remove() 方法。

預設方法也可以呼叫其他方法,例如,我們可以改造 Collection 介面,定義一個方便的 isEmpty() 方法:

public interface Collection {
  	int size(); // an abstract method
  	default boolean isEmpty() { return size() == 0; }
}

這樣,實現 Collection 的程式設計師就不用再操心實現 isEmpty() 方法了。

(事實上這也是 AbstractCollection 抽象類的定義——所有的集合具體實現幾乎都繼承了 AbstractCollection抽象類——但為什麼頂層的 Collection 介面不做這樣的修改呢?我起初是懷疑有一些特殊的集合為空的定義有特殊性,但我沒有找到..幾乎所有的集合為空判定都為自身的元素等於 0。所以答案是什麼呢?是解決預設方法衝突的 "類優先" 原則!?)

解決預設方法衝突

如果先在一個介面中將一個方法定義為預設方法,然後又在類或另一個介面中定義同樣的方法,會發生什麼?

// 測試介面 1
public interface TestInterface1 {
    default void sameMethod() { System.out.println("Invoke TestInterface1 method!"); }
}
// 測試介面 2
public interface TestInterface2 {
    default void sameMethod() { System.out.println("Invoke TestInterface2 method!"); }
}
// 繼承兩個介面的測試類
public class TestObject implements TestInterface1, TestInterface2 {

    @Override
    public void sameMethod() {
      	// 這裡也可以選擇兩個介面中的一個預設實現
      	// 如: TestInterface1.super.sameMethod();
        System.out.println("Invoke Object method!");
    }
}
// 測試類
public class Tester {

    public static void main(String[] args) {
        TestObject testObject = new TestObject();
        testObject.sameMethod();
    }
}

測試輸出:

Invoke Object method!

➡️ 對於 Scale 或者 C++ 這些語言來說,解決這種具有 二義性 的情況規則會很複雜,Java 的規則則簡單得多:

  1. 類優先。如果本類中提供了一個具體方法符合簽名,則同名且具有相同引數列表的介面中的預設方法會被忽略;
  2. 介面衝突。如果一個介面提供了一個預設方法,另一個介面提供了一個同名且引數列表相同的方法 (順序和型別都相同) ,則必須覆蓋這個方法來解決衝突 (就是?程式碼的情況,不覆蓋編譯器不會編譯..)

Java 設計者更強調一致性,讓程式設計師自己來解決這樣的二義性似乎也顯得很合理。如果至少有一個介面提供了一個實現,編譯器就會報告錯誤,程式設計師就必須解決這個二義性。(如果兩個介面都沒有為共享方法提供預設實現,則不存在衝突,要麼實現,要麼不實現..)

➡️ 我們只討論了兩個介面的命名衝突。現在來考慮另一種情況,一個類繼承自一個類,同時實現了一個介面,從父類繼承的方法和介面擁有同樣的方法簽名,又將怎麼辦呢?

// 測試介面
public interface TestInterface {
    default void sameMethod() { System.out.println("Invoke TestInterface Method!"); }
}
// 父類
public class Father {
    void sameMethod() { System.out.println("Invoke Father Method!"); }
}
// 子類
public class Son extends Father implements TestInterface {
    @Override
    public void sameMethod() {
        System.out.println("Invoke Son Method!");
    }
}
// 測試類
public class Tester {
    public static void main(String[] args) { new Son().sameMethod(); }
}

程式輸出:

Invoke Son Method!

還記得我們說過的方法呼叫的過程嗎 (先找本類的方法找不到再從父類找)?加上這裡提到的 "類優先" 原則 (本類中有方法則直接呼叫),這很容易理解!

千萬不要讓一個預設方法重新定義 Object 類中的某個方法。例如,不能為 toString()equals() 定義預設方法,儘管對於 List 之類的介面這可能很有吸引力,但由於 類優先原則,這樣的方法絕對無法超越 Object.toString() 或者 Object.equals()

(這裡就對應上方思考為什麼不在 Collection 中定義預設的 isEmpty() 方法的答案)

Part 2. 介面與工廠模式

這一部分節選自 極客時間 | 設計模式之美:https://time.geekbang.org/column/article/197254

原作者:王爭

介面是實現多重繼承的途徑,而生成遵循某個介面的物件的典型方式就是 工廠方法設計模式。這與直接呼叫構造器構造物件不同,我們在工廠物件上呼叫的是建立方法,而該工廠物件將生成介面的某個實現物件。

理論上,通過這種方式,我們的程式碼將完全與介面的實現分離,這就使得我們可以透明地將某個實現替換為另一個實現。下面我們來舉例演示一下。

簡單工廠模式

假設我們現在需要根據檔案的字尾名 (json、xml、yaml) 來選擇不同的解析器 (JsonRuleConfigParser、XmlRuleConfigParser),將儲存在檔案中的配置解析成記憶體物件 RuleConfig:

public class RuleConfigSource {
  public RuleConfig load(String ruleConfigFilePath) {
    String ruleConfigFileExtension = getFileExtension(ruleConfigFilePath);
    IRuleConfigParser parser = null;
    if ("json".equalsIgnoreCase(ruleConfigFileExtension)) {
      parser = new JsonRuleConfigParser();
    } else if ("xml".equalsIgnoreCase(ruleConfigFileExtension)) {
      parser = new XmlRuleConfigParser();
    } else if ("yaml".equalsIgnoreCase(ruleConfigFileExtension)) {
      parser = new YamlRuleConfigParser();
    } else {
      throw new InvalidRuleConfigException(
             "Rule config file format is not supported: " + ruleConfigFilePath);
    }

    String configText = "";
    //從ruleConfigFilePath檔案中讀取配置文字到configText中
    RuleConfig ruleConfig = parser.parse(configText);
    return ruleConfig;
  }

  private String getFileExtension(String filePath) {
    //...解析檔名獲取副檔名,比如rule.json,返回json
    return "json";
  }
}

➡️ 為了讓程式碼邏輯更加清晰,可讀性更好,我們要善於 將功能獨立的程式碼塊封裝成函式。按照這個設計思路,我們可以將程式碼中涉及 parser 建立的部分邏輯剝離出來,抽象成 createParser() 函式。重構之後的程式碼如下所示:

public RuleConfig load(String ruleConfigFilePath) {
    String ruleConfigFileExtension = getFileExtension(ruleConfigFilePath);
    IRuleConfigParser parser = createParser(ruleConfigFileExtension);
    if (parser == null) {
      throw new InvalidRuleConfigException(
              "Rule config file format is not supported: " + ruleConfigFilePath);
    }

    String configText = "";
    //從ruleConfigFilePath檔案中讀取配置文字到configText中
    RuleConfig ruleConfig = parser.parse(configText);
    return ruleConfig;
  }

  private String getFileExtension(String filePath) {
    //...解析檔名獲取副檔名,比如rule.json,返回json
    return "json";
  }

  private IRuleConfigParser createParser(String configFormat) {
    IRuleConfigParser parser = null;
    if ("json".equalsIgnoreCase(configFormat)) {
      parser = new JsonRuleConfigParser();
    } else if ("xml".equalsIgnoreCase(configFormat)) {
      parser = new XmlRuleConfigParser();
    } else if ("yaml".equalsIgnoreCase(configFormat)) {
      parser = new YamlRuleConfigParser();
    }
    return parser;
  }
}

➡️ 為了讓類的職責更加單一、程式碼更加清晰,我們還可以進一步將 createParser() 函式剝離到一個單獨的類中,讓這個類只負責物件的建立。而這個類就是我們現在要將的 簡單工廠 模式類。具體的程式碼如下所示:

public class RuleConfigSource {
  public RuleConfig load(String ruleConfigFilePath) {
    String ruleConfigFileExtension = getFileExtension(ruleConfigFilePath);
    IRuleConfigParser parser = RuleConfigParserFactory.createParser(ruleConfigFileExtension);
    if (parser == null) {
      throw new InvalidRuleConfigException(
              "Rule config file format is not supported: " + ruleConfigFilePath);
    }

    String configText = "";
    //從ruleConfigFilePath檔案中讀取配置文字到configText中
    RuleConfig ruleConfig = parser.parse(configText);
    return ruleConfig;
  }

  private String getFileExtension(String filePath) {
    //...解析檔名獲取副檔名,比如rule.json,返回json
    return "json";
  }
}

public class RuleConfigParserFactory {
  public static IRuleConfigParser createParser(String configFormat) {
    IRuleConfigParser parser = null;
    if ("json".equalsIgnoreCase(configFormat)) {
      parser = new JsonRuleConfigParser();
    } else if ("xml".equalsIgnoreCase(configFormat)) {
      parser = new XmlRuleConfigParser();
    } else if ("yaml".equalsIgnoreCase(configFormat)) {
      parser = new YamlRuleConfigParser();
    }
    return parser;
  }
}

(這樣的 Factory 程式碼暫稱為第一種實現)

在類的命名中體現設計模式是非常好的方式 (例如這裡的 RuleConfigParserFactory)。大部分工廠類都是以 “Factory” 這個單詞結尾的,但也不是必須的,比如 Java 中的 DateFormatCalender

除此之外,工廠類中建立物件的方法一般都是 create 開頭,比如程式碼中的 createParser(),但有的也命名為 getInstance()createInstance()newInstance(),有的甚至命名為 valueOf() (比如 Java String 類的 valueOf() 函式) 等等,這個我們根據具體的場景和習慣來命名就好。

➡️ 在上面的程式碼實現中,我們每次呼叫 RuleConfigParserFactory 的 createParser() 的時候,都要建立一個新的 parser。實際上,如果 parser 可以複用,為了節省記憶體和物件建立的時間,我們可以將 parser 事先建立好快取起來。當呼叫 createParser() 函式的時候,我們從快取中取出 parser 物件直接使用:

public class RuleConfigParserFactory {
  private static final Map<String, RuleConfigParser> cachedParsers = new HashMap<>();

  static {
    cachedParsers.put("json", new JsonRuleConfigParser());
    cachedParsers.put("xml", new XmlRuleConfigParser());
    cachedParsers.put("yaml", new YamlRuleConfigParser());
  }

  public static IRuleConfigParser createParser(String configFormat) {
    if (configFormat == null || configFormat.isEmpty()) {
      return null;//返回null還是IllegalArgumentException全憑你自己說了算
    }
    IRuleConfigParser parser = cachedParsers.get(configFormat.toLowerCase());
    return parser;
  }
}

(這樣的 Factory 程式碼暫稱為第二種實現)

這有點類似於單例模式和簡單工廠模式的結合。

但上面兩種實現的簡單工廠,都有違背 開閉原則 (對擴充套件開放,對修改關閉)。想象一下現在我們如果要新增一種 parser,那麼勢必會修改 RuleCOnfigParserFactory 裡面的程式碼!但好在就日常的使用來說,如果不是需要頻繁地新增新的 parser,只是偶爾修改一下 RuleConfigParserFactory 程式碼,稍微不符合開閉原則,也是完全可以接受的。

工廠方法

回看?我們上方的第一種實現,如果可能的話,我們的 if-else 程式碼會隨著檔案種類的增加列得越來越長,最終不僅可讀性很差,也變得更加難以維護 (複雜度增加),而且也不怎麼優雅。

如果我們非得去掉 if-else 分支邏輯的話,應該怎麼辦呢?比較經典處理方法就是利用多型。按照多型的實現思路,對上面的程式碼進行重構。重構之後的程式碼如下所示:

public interface IRuleConfigParserFactory {
  IRuleConfigParser createParser();
}

public class JsonRuleConfigParserFactory implements IRuleConfigParserFactory {
  @Override
  public IRuleConfigParser createParser() {
    return new JsonRuleConfigParser();
  }
}

public class XmlRuleConfigParserFactory implements IRuleConfigParserFactory {
  @Override
  public IRuleConfigParser createParser() {
    return new XmlRuleConfigParser();
  }
}

public class YamlRuleConfigParserFactory implements IRuleConfigParserFactory {
  @Override
  public IRuleConfigParser createParser() {
    return new YamlRuleConfigParser();
  }
}

實際上,這就是工廠方法模式的典型程式碼實現。這樣當我們新增一種 parser 的時候,只需要新增一個實現了 IRuleConfigParserFactory 介面的 Factory 類即可。所以,工廠方法模式比起簡單工廠模式更加符合開閉原則。

從上面的工廠方法的實現來看,一切都很完美,但是實際上存在挺大的問題。問題存在於這些工廠類的使用上。接下來,我們看一下,如何用這些工廠類來實現 RuleConfigSource 的 load() 函式。具體的程式碼如下所示:


public class RuleConfigSource {
  public RuleConfig load(String ruleConfigFilePath) {
    String ruleConfigFileExtension = getFileExtension(ruleConfigFilePath);

    IRuleConfigParserFactory parserFactory = null;
    if ("json".equalsIgnoreCase(ruleConfigFileExtension)) {
      parserFactory = new JsonRuleConfigParserFactory();
    } else if ("xml".equalsIgnoreCase(ruleConfigFileExtension)) {
      parserFactory = new XmlRuleConfigParserFactory();
    } else if ("yaml".equalsIgnoreCase(ruleConfigFileExtension)) {
      parserFactory = new YamlRuleConfigParserFactory();
    } else {
      throw new InvalidRuleConfigException("Rule config file format is not supported: " + ruleConfigFilePath);
    }
    IRuleConfigParser parser = parserFactory.createParser();

    String configText = "";
    //從ruleConfigFilePath檔案中讀取配置文字到configText中
    RuleConfig ruleConfig = parser.parse(configText);
    return ruleConfig;
  }

  private String getFileExtension(String filePath) {
    //...解析檔名獲取副檔名,比如rule.json,返回json
    return "json";
  }
}

從上面的程式碼實現來看,工廠類物件的建立邏輯又耦合進了 load() 函式中,跟我們最初的程式碼版本非常相似,引入工廠方法非但沒有解決問題,反倒讓設計變得更加複雜了。那怎麼來解決這個問題呢?

我們可以為工廠類再建立一個簡單工廠,也就是 工廠的工廠,用來建立工廠類物件。這段話聽起來有點繞,我把程式碼實現出來了,你一看就能明白了。其中,RuleConfigParserFactoryMap 類是建立工廠物件的工廠類,getParserFactory() 返回的是快取好的單例工廠物件。


public class RuleConfigSource {
  public RuleConfig load(String ruleConfigFilePath) {
    String ruleConfigFileExtension = getFileExtension(ruleConfigFilePath);

    IRuleConfigParserFactory parserFactory = RuleConfigParserFactoryMap.getParserFactory(ruleConfigFileExtension);
    if (parserFactory == null) {
      throw new InvalidRuleConfigException("Rule config file format is not supported: " + ruleConfigFilePath);
    }
    IRuleConfigParser parser = parserFactory.createParser();

    String configText = "";
    //從ruleConfigFilePath檔案中讀取配置文字到configText中
    RuleConfig ruleConfig = parser.parse(configText);
    return ruleConfig;
  }

  private String getFileExtension(String filePath) {
    //...解析檔名獲取副檔名,比如rule.json,返回json
    return "json";
  }
}

//因為工廠類只包含方法,不包含成員變數,完全可以複用,
//不需要每次都建立新的工廠類物件,所以,簡單工廠模式的第二種實現思路更加合適。
public class RuleConfigParserFactoryMap { //工廠的工廠
  private static final Map<String, IRuleConfigParserFactory> cachedFactories = new HashMap<>();

  static {
    cachedFactories.put("json", new JsonRuleConfigParserFactory());
    cachedFactories.put("xml", new XmlRuleConfigParserFactory());
    cachedFactories.put("yaml", new YamlRuleConfigParserFactory());
  }

  public static IRuleConfigParserFactory getParserFactory(String type) {
    if (type == null || type.isEmpty()) {
      return null;
    }
    IRuleConfigParserFactory parserFactory = cachedFactories.get(type.toLowerCase());
    return parserFactory;
  }
}

當我們需要新增新的規則配置解析器的時候,我們只需要建立新的 parser 類和 parser factory 類,並且在 RuleConfigParserFactoryMap 類中,將新的 parser factory 物件新增到 cachedFactories 中即可。程式碼的改動非常少,基本上符合開閉原則。

實際上,對於規則配置檔案解析這個應用場景來說,工廠模式需要額外建立諸多 Factory 類,也會增加程式碼的複雜性,而且,每個 Factory 類只是做簡單的 new 操作,功能非常單薄 (只有一行程式碼),也沒必要設計成獨立的類,所以,在這個應用場景下,簡單工廠模式簡單好用,比工廠方法模式更加合適。

什麼時候該用工廠方法模式呢?

我們前面提到,之所以將某個程式碼塊剝離出來,獨立為函式或者類,原因是這個程式碼塊的邏輯過於複雜,剝離之後能讓程式碼更加清晰,更加可讀、可維護。但是,如果程式碼塊本身並不複雜,就幾行程式碼而已,我們完全沒必要將它拆分成單獨的函式或者類。

所以讓我們有足夠理由使用工廠方法模式的情況大概有以下兩點:

  • 當物件的建立邏輯比較複雜,不只是簡單的 new 一下就可以,而是要組合其他類物件,做各種初始化操作的時候;
  • 避免煩人的 if-else 分支邏輯時;

抽象工廠(Abstract Factory)

在簡單工廠和工廠方法中,類只有一種分類方式。比如,在規則配置解析那個例子中,解析器類只會根據配置檔案格式 (Json、Xml、Yaml……) 來分類。但是,如果類有兩種分類方式,比如,我們既可以按照配置檔案格式來分類,也可以按照解析的物件 (Rule 規則配置還是 System 系統配置) 來分類,那就會對應下面這 6parser 類。

針對規則配置的解析器:基於介面IRuleConfigParser
JsonRuleConfigParser
XmlRuleConfigParser
YamlRuleConfigParser

針對系統配置的解析器:基於介面ISystemConfigParser
JsonSystemConfigParser
XmlSystemConfigParser
YamlSystemConfigParser

針對這種特殊的場景,如果還是繼續用工廠方法來實現的話,我們要針對每個 parser 都編寫一個工廠類,也就是要編寫 6 個工廠類。如果我們未來還需要增加針對業務配置的解析器 (比如 IBizConfigParser),那就要再對應地增加 4 個工廠類。而我們知道,過多的類也會讓系統難維護。這個問題該怎麼解決呢?

抽象工廠就是針對這種非常特殊的場景而誕生的。我們可以讓一個工廠負責建立多個不同型別的物件 (IRuleConfigParser、ISystemConfigParser 等),而不是隻建立一種 parser 物件。這樣就可以有效地減少工廠類的個數。具體的程式碼實現如下所示:

public interface IConfigParserFactory {
  IRuleConfigParser createRuleParser();
  ISystemConfigParser createSystemParser();
  //此處可以擴充套件新的parser型別,比如IBizConfigParser
}

public class JsonConfigParserFactory implements IConfigParserFactory {
  @Override
  public IRuleConfigParser createRuleParser() {
    return new JsonRuleConfigParser();
  }

  @Override
  public ISystemConfigParser createSystemParser() {
    return new JsonSystemConfigParser();
  }
}

public class XmlConfigParserFactory implements IConfigParserFactory {
  @Override
  public IRuleConfigParser createRuleParser() {
    return new XmlRuleConfigParser();
  }

  @Override
  public ISystemConfigParser createSystemParser() {
    return new XmlSystemConfigParser();
  }
}

// 省略YamlConfigParserFactory程式碼

重點回顧

  1. 介面的概念 / 介面的定義 / 介面的實現 / 介面的屬性;
  2. 介面的靜態和私有方法 / 如何解決預設方法的衝突;
  3. 介面和工廠模式;

練習

練習 1:實現一個圖形繪製工具

建立一個可以繪製不同形狀的繪圖工具,可以繪製圓形、矩形、三角形,每個圖形都會有一個 draw() 方法用於繪圖,而繪圖工具也有一個 draw() 方法,根據傳入型別的不同呼叫不同的方法。

建立 IShape 介面:

public interface IShape {
    void draw();
}

繼承 IShape 介面建立圓形、矩形、三角形:

// 圓形
public class Circle implements IShape {
    @Override
    public void draw() { System.out.println("Draw Circle..."); }
}
// 矩形
public class Rectangle implements IShape {
    @Override
    public void draw() { System.out.println("Draw Rectangle..."); }
}
// 三角形
public class Triangle implements IShape {
    @Override
    public void draw() { System.out.println("Draw Triangle..."); }
}

圖形繪製工具:

public class Paint {
    public static void draw(IShape shape) {
        shape.draw();
    }
}

測試類:

public class Tester {
    public static void main(String[] args) {
        Paint.draw(new Circle());
        Paint.draw(new Rectangle());
        Paint.draw(new Triangle());
    }
}

程式輸出:

Draw Circle...
Draw Rectangle...
Draw Triangle...

(ps:說實話這一篇文章雖然寫了兩天.. 但感覺總體質量挺差的.. 原因有許多,一來是發現存在很多知識點交叉的情況——也就是說知識是互相聯絡的,想要說清楚不容易——而且常常組織起來非常龐大。二來是發現光說清楚一個知識點也挺不容易的..所以在考慮新的組織形式.. 最近有接觸到一些雙向連結的工具.. 探索探索..)

參考資料

  1. 《Java 核心技術 卷 I》
  2. 《Java 程式設計思想》
  3. Introduction to Computer Science using Java - http://programmedlessons.org/Java9/index.html
  4. 極客時間 | 設計模式之美 - https://time.geekbang.org/column/article/177110
  • 本文已收錄至我的 Github 程式設計師成長系列 【More Than Java】,學習,不止 Code,歡迎 star:https://github.com/wmyskxz/MoreThanJava
  • 個人公眾號 :wmyskxz,個人獨立域名部落格:wmyskxz.com,堅持原創輸出,下方掃碼關注,2020,與您共同成長!

非常感謝各位人才能 看到這裡,如果覺得本篇文章寫得不錯,覺得 「我沒有三顆心臟」有點東西 的話,求點贊,求關注,求分享,求留言!

創作不易,各位的支援和認可,就是我創作的最大動力,我們下篇文章見!

(另外這些基礎的知識體系我打算自己偷偷慢慢在部落格搭建啦.. 等有確實的成果之後再分享吧.. 公眾號還是希望分享更多能對小夥伴們有用的實際的東西.. Respect~)

相關文章