網頁

2018/11/17

Java 設計模式 裝飾者模式 Decorator Pattern

本篇介紹裝飾者模式(Decorator Pattern)。

裝飾者模式屬於GoF中的Structural Pattern

裝飾者模式的使用時機為,當你想要增加類別的功能,但不想修改到現有類別的程式碼;或是要動態地添加新功能於類別上;或是該類別搭配不同的特性會有多種組合的情況。

在Java中使用裝飾者模式的典型例子為Java IO,其抽像類別InputStream的抽象方法read()分別由不同的裝飾者類別如BufferedInputStreamObjectInputStreamDataInputStream等來實作各種讀取串流(stream)的方法並提供額外的功能。而每一裝飾類別的建構式皆以其父抽像類別InputStream為輸入參數。

裝飾者模式的好處是可以動態地為某個類別提供多種額外的功能。如果以透過繼承的方式就沒有這樣的彈性,因為你必須一開始定義好擴增功能的子類別,如果可增加的功能一多或有多種組合,那就得建立非常多的子類別而難以管理。又或不透過繼承而透過在原有的介面新增方法,這樣又造成所有繼承的類別都要實作該方法。

例如有一個介面Weapon,提供基本功能如下

Weapon

public interface Weapon {

    public String getName(); // 取得武器名稱
    public int getHitPoint(); // 取得攻擊點數
    public void attack(); // 攻擊
    
}

下面為實作Weapon的具體類別Sword

Sword

public class Sword implements Weapon {

    private String name = "劍";
    private int hitPoint = 10;

    @Override
    public String getName() {
        return name;
    }

    @Override
    public int getHitPoint() {
        return hitPoint;
    }

    @Override
    public void attack() {
        System.out.println(name + "造成" + hitPoint + "點傷害");
    }

}

如果今天要有提供火屬性的劍,並提供特殊效果及額外的攻擊點數,若以繼承的方式來實現則新增一個FireSword類別

FireSword

public class FireSword extends Sword {
    
    @Override
    public String getName() {
        return "火" + super.getName();
    }

    @Override
    public int getHitPoint() {
        return 3 + hitPoint;
    }

    @Override
    public void attack() {
        System.out.println("火" + name + "造成" + (3 + hitPoint) + "點傷害");
        special();
    }
    
    @Override
    public void special() {
        System.out.println("特殊效果:火焰造成 5點傷害");
    }

}

然而上面的問題是,提供的special()方法必須先在Weapon介面中添加,這樣就修改到原本的程式碼。更大的問題是,如果今天不止只有一個屬性,而是有多種屬性的組合,例如火加聖屬性,或火加暗屬性,或火加聖加毒之類的情況,若以同樣的繼承手段則要新增的子類別就太多了,且有很多部份重覆的情況變得難以管理,而且在程式中無法依情況動態的添加功能。

若改以裝飾者模式來添加額外的功能則可以避免上述的狀況,我們可以以組件的方式隨意添加組合新的特性或功能,而不用更動程式或繼承原有的類別。

裝飾飾者模式有幾個關鍵的原件如下:

  • Component:原有類別實作的介面,定義標準方法。
  • ConcreteComponent:原有類別
  • Decorator:裝飾者類別,繼承原有類別的介面。
  • ComcreteDecorator:裝飾者類別的實作。



裝飾者模式的特徵中除了以上必要的原件外,裝飾者(Decorator)及其子類別(ComcreteDecorator)的建構式皆以Component介面為傳入參數來保有原有的類別。

在上例中,Weapon即為Component,Sword即為ComcreteComponent,而我們要新增額外的火屬性或聖屬性及附加的特殊效果,所以先設定一個裝飾者類別WeaponDecorator如下。

WeaponDecorator

public abstract class WeaponDecorator implements Weapon {

    protected String name;
    protected int hitPoint;
    protected Weapon weapon;

    public WeaponDecorator(String name, int hitPoint, Weapon weapon) {

        this.name = name;
        this.hitPoint = hitPoint;
        this.weapon = weapon;
    }

    @Override
    public String getName() {
        return name + weapon.getName();
    }

    @Override
    public int getHitPoint() {
        return hitPoint + weapon.getHitPoint();
    }

    protected void special() {
        if (weapon instanceof WeaponDecorator) {
            WeaponDecorator deco = (WeaponDecorator) weapon;
            deco.special();
        }
    }

    public void attack() {
        System.out.println(getName() + "造成" + (hitPoint + weapon.getHitPoint()) + "點傷害");
        this.special();
    }

    public Weapon getWeapon() {
        return this.weapon;
    }

}

WeaponDecorator加入了special()方法來提供額外的能力。


接著提供具體的裝飾類別火屬性FirePower及聖屬性HolyPower,皆繼承抽像類別WeaponDecorator,且同樣地以Weapon為建構式的參數並保存。

FirePower

public class FirePower extends WeaponDecorator {

    public FirePower(Weapon weapon) {
        super("火", 5, weapon);
    }

    @Override
    protected void special() {
        super.special();
        System.out.println("特殊效果:火焰造成 5點傷害");
    }
    
}

HolyPower

public class HolyPower extends WeaponDecorator {

    public HolyPower(Weapon weapon) {
        super("聖", 3, weapon);
    }

    @Override
    protected void special() {
        super.special();
        System.out.println("特殊效果:回復5點HP");
    }

}

實作一個使用武器的戰士類別Warrior

Warrior

public class Warrior {

    String name;
    Weapon weapon;

    public Warrior(String name, Weapon weapon) {
        this.name = name;
        this.weapon = weapon;
    }

    public void attack() {
        System.out.print(name + "使用");
        weapon.attack();
        System.out.print("\n");
    }

    public Weapon getWeapon() {
        return weapon;
    }

    public void setWeapon(Weapon weapon) {
        this.weapon = weapon;
    }

}

測試如下

public class App {
    
    public static void main(String[] args) {

        Weapon sword = new Sword();
        Weapon fireSword = new FirePower(sword); // 在劍上附加火屬性
        Weapon holySword = new HolyPower(sword); // 在劍上附加聖屬性
        Weapon holyFireSword = new HolyPower(fireSword); // 在火箭上附加聖屬性

        Warrior warrior = new Warrior("あああああ ", sword); //建立一個戰士並配與普通的劍
        warrior.attack(); // 攻擊

        warrior.setWeapon(fireSword); // 換成火劍
        warrior.attack();

        warrior.setWeapon(holySword); // 換成聖劍
        warrior.attack();

        warrior.setWeapon(holyFireSword); // 換成聖火劍
        warrior.attack();

    }

}

印出結果如下

あああああ 使用劍造成10點傷害

あああああ 使用火劍造成15點傷害
特殊效果:火焰造成 5點傷害

あああああ 使用聖劍造成13點傷害
特殊效果:回復5點HP

あああああ 使用聖火劍造成18點傷害
特殊效果:火焰造成 5點傷害
特殊效果:回復5點HP

裝飾者模式透過裝飾類別將原本的功能包裹起來,所以在呼叫原有的功能時,會層層向內執行裝飾類所包裹的功能,類似下面的感覺

+-------------------------+
| HolyPower               |
|    +-----------------+  |
|    | FirePower       |  |
|    |    +---------+  |  |
|    |    | Sword   |  |  |
|    |    |         |  |  |
|    |    +---------+  |  |
|    |                 |  |
|    +-----------------+  |
|                         |
+-------------------------+

參考:

沒有留言:

張貼留言