Interface、Abstract Class、Concrete Class 大亂鬥

Z-xuan Hong
15 min readJul 26, 2020

--

Interface、Abstract Class、Concrete Class相信大家都聽過,也大家都說Interface可以讓程式更有彈性,但是你真的了解為何Interface能讓程式更有彈性嗎?甚至很多教科書也都有針對這幾個名詞進行介紹,但卻缺乏一個系統性的介紹,因此想要透過此文章嘗試達到以下目標:

  • 闡述Interface、Abstract Class、Concrete Class的各個特性
  • 闡述Interface、Abstract Class、Concrete Class在系統設計中扮演的角色
  • 闡述Interface、Abstract Class、Concrete Class的錯誤使用方式(Anti-Pattern),並且知道如何透過重構改善系統設計

此篇文章分為以下幾個部分(介紹包含個人對於開發軟體的觀念,有錯誤歡迎在下方留言):

  • Interface、Abstract Class、Concrete Class特性介紹
  • Interface、Abstract Class、Concrete Class在系統整體設計的角色
  • Interface、Abstract Class、Concrete Class錯誤使用方式 (Anti-Pattern)

Interface、Abstract Class、Concrete Class特性介紹

Interface特性介紹

  • What is interface:定義實作此介面的物件所需提供的功能(What),而不包含物件的實作細節(How),在Java中為一系列的Public Abstract Method,而介面因為只定義功能,而不是實作細節,因此不會有Instance Variable或是Private Member Function。
  • Why interface:因為介面能夠用來定義功能(What),而不是實作細節(How),我們知道功能的變動性是遠遠小於實作細節的變動性,因此當整體系統都是介面耦合時,系統因為需求改變時受到的影響會降低(因為實作細節變動的機會較大),讓整體更好維護。舉個例子 :我們需要針對圖形進行畫圖(功能),如果以介面的方式去設計,客戶端的程式碼為shape.draw(),我們知道圖形有非常多種實作細節,像是Circle、Rectangle等等,而因為系統只知道Shape介面,不同實作細節就算不斷的變化(新的圖形種類),也不會對整體系統造成影響。
  • Advantage:整體系統較有彈性、不會受到實作細節變化受到影響、系統較好理解,因為表達了程式要解決的問題(Intention Revealing),而不是實作細節。
  • Disadvantage:1. 修改介面所提供的功能,實作介面的類別需要跟著修改(最後會針對此點缺陷進行探討)。2. 在不需要替換實作細節的情況下使用介面,會導致系統複雜度上升。
  • When to use:1. 用來設計整體架構時透過介面的方式,能夠將低架構受到實作細節影響的機率。2.有多個不同實作時,透過定義介面的方式,能夠替換不同實作,也不會限制不同實作的實作方式(如果使用Abstract Class定義抽象化,會導致所有子類別需要滿足Abstract Class的Contract,這是實作細節的其中一種展現,我們不能預期未來子類別都能滿足此Contract,要求子類別都需要滿足特定行為不能算是良好的抽象化)
  • When not to use:1. 只有一種實作時,使用介面會造成此介面不是真的抽象化,只是此實作類別的Header Interface,建議使用Concrete Class即可。2. 定義所有子類別都需要實作某些步驟,使用介面會導致介面為實作細節,而不是抽象功能,應該使用Abstract Class搭配 Template Method定義抽象步驟。

Abstract Class特性介紹

  • What is abstract class:針對特定群集的子類別定義預設實作和部分Abstract method,定義了特定子類別的部分實作細節與抽象化。
  • Why abstract class:Abstract Class相較於Interface,能夠定義更多的實作細節,提供部分子類別抽象化函數,讓各個子類別擴充更簡單,讓開發者知道擴充新的子類別只需要實作部分函數即可。
  • Advantage:1. 提供預設實作,降低擴充類別的複雜度。2. 降低程式重複。
  • Disadvantage:1. 雖然提供抽象函數,但對於非特定群集的子類別來說沒辦法有效的為他們提供部分實作(因為功能可能完全不一樣)。2. 過度使用抽象類別會導致架構限制在特定實作,當細節修改時導致架構變動,降低維護程度。
  • When to use:1. 在有Interface的情況下使用Abstract Class可以降低開發者擴充的困難度,也降低程式碼重複。2. 在有Interface的情況下使用Abstract Class搭配Template Method,可以把抽象化步驟封裝在Abstract Class,讓子類別之間不會有流程重複與程式碼重複。
  • When not to use:1. 設計架構之間關係建議使用Interface,而非Abstract Class。2. 只有一種實作時建議使用Concrete Class,使用Abstract Class或是Interface都不能算是有用的抽象化,只會增加系統複雜度。

Concrete Class特性介紹

  • What is concrete class:定義實作細節所需要的資料結構,並且實作一系列的Member function,封裝實作細節資料結構,讓客戶端針對Member Function進行互動。
  • Why concrete class:當沒有多種實作,實作細節變動可能性很低時,使用Concrete Class可以降低系統複雜度,整體設計簡單好理解,降低開發者理解程式碼的複雜度。
  • Advantage:簡單,好理解,Interface與Abstract Class至少需要兩個類別,如果在只有一種實作的情況下,使用Concrete Class更為簡單。
  • Disadvantage:當有多種實作時,如果使用Concrete Class會導致系統耦合到特定實作細節,難以變動,應該使用Interface(也不建議使用Abstract Class)
  • When to use:系統只有一種實作時且變動機率低時。
  • When not to use:系統有多種實作時,變動機率高,使用Concrete Class會讓系統架構修改幅度範圍增加。

Interface、Abstract Class、Concrete Class在系統整體設計的角色

Interface:用來定義High Level System Design的介面,能夠降低系統受到實作細節影響的變動。

Abstract Class:針對High Level System Design提供多種Abstract Class提供不同取向的實作細節。

Concrete Class:實作Abstract Class或是Interface達到客製化邏輯擴充,當系統抽象化不明確時使用Concrete Class較為簡單,等待系統抽象化明顯時,使用Extract Interface等重構方式修改系統。如果一開始使用Interface或是Abstract Class,一次需要維護兩個累別(抽象與實作),而在實作細節改變時,還是需要修改Interface與Abstract Class,沒有完整的得到Interface與Abstract Class的好處(範例會詳細說明)。

Interface、Abstract Class、Concrete Class錯誤使用方式 (Anti-Pattern)

  • 為了介紹錯誤使用Interface、Abstract Class、Concrete Class的時機,會先介紹錯誤用法,接著使用重構修改系統,讓大家更能理解不同工具的使用時機

Replace Abstract Class to Interface

範例程式碼如下:

public abstract class Chef {
private final String name;

public Chef(String name) {
this.name = name;
}

public final void cook() {
prepareFood();
startCooking();
}

abstract void prepareFood();

abstract void startCooking();
}

從上述程式碼我們看到,Chef抽象類別需要完成Cook的動作,而步驟有兩種為prepareFood和startCooking,在上述介紹中我們提到,Abstract Class是針對特定群集子類別提供Default的實作,降低擴充的難度,但如果系統使用此抽象類別會有以下缺點:

  1. 限制所有Chef的實作都只有兩種步驟,如果需要三步驟的子類別會導致擴充困難。
  2. 系統耦合到此Chef,當實作細節變動時,系統會受到一定影響(需要重新Extract Interface,從新Compile)。

解決程式碼如下 :

public interface Chef {
void cook();
}
public abstract class TwoStepChef implements Chef {
private final String name;

public TwoStepChef(String name) {
this.name = name;
}

@Override
public final void cook() {
prepareFood();
startCooking();
}

abstract void prepareFood();

abstract void startCooking();
}

上述程式碼透過Extract Interface抽取Chef介面,針對原本的抽象類別名稱替換為TwoStepChef,修改後程式碼有以下好處:

  1. 系統從實作耦合轉換成抽象耦合
  2. 如果有需要三步驟的Chef,只需要新增新的抽象類別ThreeStepChef即可
  3. 維持原本封裝TwoStepChef的步驟在抽象類別,降低子類別擴充難度的優點

技術檢討:

  1. 使用Interface定義系統抽象功能(What)
  2. 使用Abstract Class封裝不同的Workflow降低子類別擴充困難度

Move Duplication Implementation and Workflow in Concrete Classes to Abstract Class

範例程式碼如下:

public interface Chef {
void cook();
}
public class ChefA implements Chef {
@Override
public final void cook() {
prepareFood();
startCooking();
}

void prepareFood() {
System.out.println("Chef A is preparing foods");
}

void startCooking() {
System.out.println("Chef A is cooking");
}
}
public class ChefB implements Chef {
@Override
public final void cook() {
prepareFood();
startCooking();
}

void prepareFood() {
System.out.println("Chef B is preparing foods");
}

void startCooking() {
System.out.println("Chef B is cooking");
}
}

從上述程式碼我們可以看到,系統使用Interface的方式設計,因此替換不同實作有較好的彈性,但是從Concrete Class我們可以看到以下缺點:

  1. 重複程式碼在ChefA與ChefB子類別中
  2. 重複抽象化步驟在ChefA與ChefB子類別中

改善程式碼如下:

public interface Chef {
void cook();
}
public abstract class TwoStepChef implements Chef {
private final String name;

public TwoStepChef(String name) {
this.name = name;
}

@Override
public final void cook() {
prepareFood();
startCooking();
}

abstract void prepareFood();

abstract void startCooking();
}
public class ChefA extends TwoSetpChef {
public ChefA(String name) {
super(name);
}

@Override
void prepareFood() {
System.out.println("Chef A is preparing foods");
}

@Override
void startCooking() {
System.out.println("Chef A is cooking");
}
}
public class ChefB extends TwoSetpChef {
public ChefB(String name) {
super(name);
}

@Override
void prepareFood() {
System.out.println("Chef B is preparing foods");
}

@Override
void startCooking() {
System.out.println("Chef B is cooking");
}
}

上述程式碼透針對兩個子類別使用Extract Class,並且使用Template Method封裝抽象化步驟,改善後程式碼有以下優點:

  1. 透過Abstract Class降低程式碼重複
  2. 透過Abstract Class封裝抽象步驟(Abstract Workflow)
  3. 透過Abstract Class提供部分實作(Template Method)
  4. 擴充新的子類別如果是TwoStepChef即可直接繼承,如果不是則實作Chef介面,系統更有彈性

Replace Leaky Abstract Interface to Concrete Class

範例程式碼如下:

public interface Chef {
void cook();
void prepareFood();
void startCooking();
}
public class ChefA implements Chef {

@Override
void cook() {
prepareFood();
startCooking();
}
@Override
void prepareFood() {
System.out.println("Chef A is preparing foods");
}

@Override
void startCooking() {
System.out.println("Chef A is cooking");
}
}

上述程式碼我們可以看到,在只有一個實作的情況底下針對ChefA使用Extract Interface出一個Chef介面,但是此介面有一下缺點:

  1. 只針對ChefA實作的功能,並非真正的抽象化(Header Interface)
  2. 無法支援ThreeStepChef,因為介面功能為特定實作,而不是真正抽象化
  3. 我們知道介面的缺點為修改功能會導致實作子類別變動(因為要實作新的功能),而範例程式碼的介面不是真正的抽象化,只是其中一種實作,違反Depedency Inversion Principle,導致系統不僅需要忍受介面帶來的複雜度(多個類別),也沒辦法讓系統穩定(因為介面非真正的抽象化)

改善程式碼如下:

public class ChefA {  

@Override
void cook() {
prepareFood();
startCooking();
}
@Override
void prepareFood() {
System.out.println("Chef A is preparing foods");
}

@Override
void startCooking() {
System.out.println("Chef A is cooking");
}
}

上述程式碼刪除掉了Header Interface,並且讓系統先依賴到ChefA,有以下優點:

  1. 簡單,不會過度複雜
  2. 當找出真正抽象化時,在使用Extract Interface搭配Abstract Class重構整個系統

上述全部討論我們發現了一件事情,介面的缺點是修改會影響到實作子類別,但很可能是使用方式錯誤(讓介面實作特定實作,而不是真正抽象化)但如果能夠遵守Dependency Inversion Principle,設計真正抽象化介面,能減少介面修改的機率,降低對實作子類別之影響。

如果確實是抽象化但是卻需要修改介面時,則是因為客戶端需要不同功能,就算是使用Abstract Class也是需要修改內部實作,導致繼承類別也需要修改,因此當介面良好的遵守DIP時,卻需要修改介面時,我們可以說是因為客戶端有不同需求,在這種情況Interface或是Abstract Class都會對系統帶來影響,因此在兩者都會影響的情況下,建議使用Interface來獲取更大的彈性。

結論:使用Interface來定義系統設計與抽象化功能,使用Abstract Class封裝抽象化步驟,使用Concrete Class來實作不同邏輯。

--

--

Z-xuan Hong

熱愛軟體開發、物件導向、自動化測試、框架設計,擅長透過軟工幫助企業達到成本最小化、價值最大化。單元測試企業內訓、導入請私訊信箱:t107598042@ntut.org.tw。