歡迎光臨
每天分享高質量文章

在 Java 中應用骨架實現

(給ImportNew加星標,提高Java技能)

 

編譯:唐尤華

連結:dzone.com/articles/favour-skeletal-interface-in-java

 

程式中有重覆程式碼?骨架實現(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. 抽象介面或骨架實現

 

要完成骨架實現:

 

  1. 建立介面。
  2. 建立抽象類來實現該介面,並實現公共方法。
  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 骨架實現的好處

 

  1. 子類可繼承其他類,比如 `DrinkVending`。
  2. 透過將呼叫委託給抽象類消除重覆程式碼。
  3. 子類可根據需要實現其他的介面。

 

4. 總結

 

當介面有公用方法時可以建立抽象類,使用子類作為委派器,建議使用骨架實現。

    贊(0)

    分享創造快樂