程序中有重複代碼?骨架實現(Skeletal Implementation)通過接口與抽象類配合,讓你擺脫重複,留下程序中有用的代碼。
骨架實現是一種設計,我們可以同時享受接口和抽象類的好處。
Java Collection API 已經採用了這種設計:AbstractSet、 AbstractMap 等都是骨架實現案例。Joshua Bloch 的"Effective Java"書中也提到了骨架接口。
本文我們將探討如何高效設計系統,使其能夠同時利用接口和抽象類的特性。
讓我們試着通過一個實際問題來理解。
假設我們想創建不同類型的自動售貨機。從自動售貨機購買產品,需要啓動售貨機、選擇產品、付款、然後取貨。
取貨完成之後,自動售貨機應該停止操作。
1. 方法一
我們可以爲不同的產品類型創建一個自動售貨機接口。爲了讓接口工作,我們還要爲自動售貨機提供具體實現。
1.1 代碼
Ivending.java
```java
package com.example.skeletal;
public interface Ivending {
void start();
void chooseProduct();
void stop();
void process();
}
```
CandyVending.java
```java
package com.example.skeletal;
public class CandyVending implements Ivending {
@Override
public void start()
{
System.out.println("Start Vending machine");
}
@Override
public void chooseProduct()
{
System.out.println("Produce different candies");
System.out.println("Choose a type of candy");
System.out.println("Pay for candy");
System.out.println("Collect candy");
}
@Override
public void stop()
{
System.out.println("Stop Vending machine");
}
@Override
public void process()
{
start();
chooseProduct();
stop();
}
}
```
DrinkVending.java
```java
package com.example.skeletal;
public class DrinkVending implements Ivending {
@Override
public void start()
{
System.out.println("Start Vending machine");
}
@Override
public void chooseProduct()
{
System.out.println("Produce diiferent soft drinks");
System.out.println("Choose a type of soft drinks");
System.out.println("pay for drinks");
System.out.println("collect drinks");
}
@Override
public void stop()
{
System.out.println("stop Vending machine");
}
@Override
public void process()
{
start();
chooseProduct();
stop();
}
}
```
VendingManager.java
```java
package com.example.skeletal;
public class VendingManager {
public static void main(String[] args) {
Ivending candy = new CandyVending();
Ivending drink = new DrinkVending();
candy.process();
drink.process();
}
}
```
輸出結果:
```shell
Start Vending machine
Produce different candies
Choose a type of candy
Pay for candy
Collect candy
Stop Vending machine
*********************
Start Vending machine
Produce diiferent soft drinks
Choose a type of soft drinks
Pay for drinks
Collect drinks
Stop Vending machine
```
簡單起見,我沒有將每個步驟定義一個單獨方法,在 `chooseProduct()` 中合併了這些步驟。
雖然看起來很好,但是上面的代碼”有一些問題“。如果我們仔細檢查一下,就會發現其中有很多重複代碼。 `start()`、 `stop()` 和 `process()` 方法在每個實現類中做了相同的事情。
當新增具體實現時,系統的代碼會複製三次。
這時我們可以新建工具類,將公共代碼放到工具類裏。然而這麼做會破壞”單一責任原則“,產生 Shotgun surgery 問題代碼。
譯註:[Shotgun surgery][1] 是軟件開發中的一種反模式,它發生在開發人員嚮應用程序代碼庫添加特性的地方,這些代碼庫會在一次更改中跨越多個實現。
[1]:https://en.wikipedia.org/wiki/Shotgun_surgery
1.2 接口的缺點
由於接口是一種約定且不包含方法體,因此每個實現都必須按照約定實現接口中的所有方法。在具體的實現中一些方法可能會重複。
2. 方法二
通過抽象類彌補接口的不足。
2.1 代碼
AbstractVending.java
```java
package com.example.skeletal;
public abstract class AbstractVending {
public void start()
{
System.out.println("Start Vending machine");
}
public abstract void chooseProduct();
public void stop()
{
System.out.println("Stop Vending machine");
}
public void process()
{
start();
chooseProduct();
stop();
}
}
```
CandyVending.java
```java
package com.example.skeletal;
public class CandyVending extends AbstractVending {
@Override
public void chooseProduct()
{
System.out.println("Produce diiferent candies");
System.out.println("Choose a type of candy");
System.out.println("Pay for candy");
System.out.println("Collect candy");
}
}
```
DrinkVending.java
```java
package com.example.skeletal;
public class DrinkVending extends AbstractVending {
@Override
public void chooseProduct()
{
System.out.println("Produce diiferent soft drinks");
System.out.println("Choose a type of soft drinks");
System.out.println("Pay for drinks");
System.out.println("Collect drinks");
}
}
```
VendingManager.java
```java
package com.example.skeletal;
public class VendingManager {
public static void main(String[] args) {
AbstractVending candy = new CandyVending();
AbstractVending drink = new DrinkVending();
candy.process();
System.out.println("*********************");
drink.process();
}
}
```
這裏我爲抽象類提供了通用的代碼,`CandyVending` 和 `DrinkVending` 都繼承了 `AbstractVending`。這麼做雖然消除了重複代碼,但引入了一個新問題。
`CandyVending` 和 `DrinkVending` 繼承了 `AbstractVending`,由於 Java 不支持多重集成因此不能繼承其他類。
假如要添加一個 `VendingServicing` 類,負責清潔和檢查自動售貨機。在這種情況下,由於已經繼承了 `AbstractVending`,因此不能繼承 `VendingServicing`。這裏可以新建組合(composition),但是必須把 `VendingMachine` 傳入該組合,這會讓 `VendingServicing` 和 `VendingMachine` 產生強耦合。
2.2 抽象類的缺點
由於菱形繼承問題,Java 不支持多重繼承。假如我們能夠同時利用接口和抽象類的優點就太好了。
還是有辦法的。
譯註:菱形繼承問題。兩個子類繼承同一個父類,而又有子類又分別繼承這兩個子類,產生二義性問題。
3. 抽象接口或骨架實現
要完成骨架實現:
創建接口。
創建抽象類來實現該接口,並實現公共方法。
在子類中創建一個私有內部類,繼承抽象類。現在把外部調用委託給抽象類,該類可以在使用通用方法同時繼承和實現任何接口。
3.1 代碼
Ivending.java
```java
package com.example.skeletal;
public interface Ivending {
void start();
void chooseProduct();
void stop();
void process();
}
```
VendingService.java
```java
package com.example.skeletal;
public class VendingService {
public void service()
{
System.out.println("Clean the vending machine");
}
}
```
AbstractVending.java
```java
package com.example.skeletal;
public abstract class AbstractVending implements Ivending {
public void start()
{
System.out.println("Start Vending machine");
}
public void stop()
{
System.out.println("Stop Vending machine");
}
public void process()
{
start();
chooseProduct();
stop();
}
}
```
CandyVending.java
```java
package com.example.skeletal;
public class CandyVending implements Ivending {
private class AbstractVendingDelegator extends AbstractVending
{
@Override
public void chooseProduct()
{
System.out.println("Produce diiferent candies");
System.out.println("Choose a type of candy");
System.out.println("Pay for candy");
System.out.println("Collect candy");
}
}
AbstractVendingDelegator delegator = new AbstractVendingDelegator();
@Override
public void start()
{
delegator.start();
}
@Override
public void chooseProduct()
{
delegator.chooseProduct();
}
@Override
public void stop()
{
delegator.stop();
}
@Override
public void process()
{
delegator.process();
}
}
```
DrinkVending.java
```java
package com.example.skeletal;
public class DrinkVending extends VendingService implements Ivending {
private class AbstractVendingDelegator extends AbstractVending
{
@Override
public void chooseProduct()
{
System.out.println("Produce diiferent soft drinks");
System.out.println("Choose a type of soft drinks");
System.out.println("pay for drinks");
System.out.println("collect drinks");
}
}
AbstractVendingDelegator delegator = new AbstractVendingDelegator();
@Override
public void start()
{
delegator.start();
}
@Override
public void chooseProduct()
{
delegator.chooseProduct();
}
@Override
public void stop()
{
delegator.stop();
}
@Override
public void process()
{
delegator.process();
}
}
```
VendingManager.java
```java
package com.example.skeletal;
public class VendingManager {
public static void main(String[] args) {
Ivending candy = new CandyVending();
Ivending drink = new DrinkVending();
candy.process();
System.out.println("*********************");
drink.process();
if(drink instanceof VendingService)
{
VendingService vs = (VendingService)drink;
vs.service();
}
}
}
```
```shell
Start Vending machine
Produce diiferent candies
Choose a type of candy
Pay for candy
Collect candy
Stop Vending machine
*********************
Start Vending machine
Produce diiferent soft drinks
Choose a type of soft drinks
Pay for drinks
Collect drinks
Stop Vending machine
Clean the vending machine
```
上面的設計中,首先創建了一個接口,然後創建了一個抽象類,在這個類中定義了所有通用的實現。然後,爲每個子類實現一個 delegator 類。通過 delegator 將調用轉給 `AbstractVending`。
3.2 骨架實現的好處
子類可繼承其他類,比如 `DrinkVending`。
通過將調用委託給抽象類消除重複代碼。
子類可根據需要實現其他的接口。
4. 總結
當接口有公用方法時可以創建抽象類,使用子類作爲委派器,建議使用骨架實現。