Fork me on GitHub

Programming Design Notes

Design Pattern - Decorator

| Comments

Decorator Pattern 可以令到不用改動原本物件的程式碼,而又可以擴充一些功能在這個物件上, 就好像 Plugin 一樣。你可以想像一下,WordPress 的 Plugin 並不需要改動核心的程式, 但又可以増加一些功能。 Java 上的 InputStream 一樣採用這種模式去實現。

看看以下例子應該有助你明白這個模式:


公司要你開發一個手機用戶計費系統,因應用戶使用時間而去收費, 使用時間愈多折扣亦愈多。而用戶亦有分普通用戶或 VIP 用戶, VIP 用戶亦有更多折扣。

一開始你可能會設計成這樣:

(User.java) download
public class User {

    private String name;
    private boolean vip;

    public User(String name, boolean vip) {
        super();
        this.name = name;
        this.vip = vip;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public boolean isVip() {
        return vip;
    }

    public void setVip(boolean vip) {
        this.vip = vip;
    }

}
(MobilePayment.java) download
public class MobilePayment {

    final double moneyPerMinute;

    public MobilePayment(double moneyPerMinute){

        this.moneyPerMinute = moneyPerMinute;

    }

    public double charge(User user, int minute) {

        double fee = minute * moneyPerMinute;

        if (minute >= 10000) {
            fee *= 0.8;
        } else if (minute >= 7000) {
            fee *= 0.85;
        } else if (minute >= 4000) {
            fee *= 0.9;
        } else if (minute >= 2000) {
            fee *= 0.95;
        }

        if (user.isVip()) {
            fee *= 0.9;
        }

        return fee;

    }

}
(Main.java) download
public class Main {

    public static void main(String[] args) {

        // $1.2 per minute
        MobilePayment mobilePayment = new MobilePayment(1.2);

        User user1 = new User("Lawrence", true);
        User user2 = new User("Tony", false);

        System.out.println(user1.getName() + " usage: 10000 minute, charge: "
                + mobilePayment.charge(user1, 10000));
        System.out.println(user2.getName() + " usage: 4000 minute, charge: "
                + mobilePayment.charge(user2, 4000));

    }

}
執行結果
Lawrence usage: 10000 minute, charge: 8640.0
Tony usage: 4000 minute, charge: 4320.0

你可以看見所有計費的程式碼也在 MobilePayment.charge 內,如果再要增加計費條件 就要修改這個地方的程式碼。


現在公司又要增加一些收費條件,因為提供額外的服務給公司客戶,需要收取額外 10% 服務費。 如果你只改動 MobilePayment.charge 內的程式碼,那愈來愈多條件時就難以維護, 現在我們使用 Decorator Pattern 去解決這些問題,收費條件亦可以隨時抽換。

(User.java) download
public class User {

    private String name;
    private boolean vip;
    private boolean company;

    public User(String name, boolean vip, boolean company) {
        super();
        this.name = name;
        this.vip = vip;
        this.company = company;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public boolean isVip() {
        return vip;
    }

    public void setVip(boolean vip) {
        this.vip = vip;
    }

    public boolean isCompany() {
        return company;
    }

    public void setCompany(boolean company) {
        this.company = company;
    }

}
(Payment.java) download
public interface Payment {

    public double charge(User user, int minute);

}
(BasicMobilePayment.java) download
public class BasicMobilePayment implements Payment {

    final double moneyPerMinute;

    public BasicMobilePayment(double moneyPerMinute){

        this.moneyPerMinute = moneyPerMinute;

    }

    public double charge(User user, int minute) {

        return minute * moneyPerMinute;

    }

}
(AdditionalPayment.java) download
public abstract class AdditionalPayment implements Payment {

    protected Payment payment;

    public AdditionalPayment(Payment payment){

        this.payment = payment;

    }

    public abstract double charge(User user, int minute);

}
(UsageAmountPayment.java) download
public class UsageAmountPayment extends AdditionalPayment {

    public UsageAmountPayment(Payment payment) {
        super(payment);
    }

    @Override
    public double charge(User user, int minute) {

        if (minute >= 10000) {
            return payment.charge(user, minute) * 0.8;
        } else if (minute >= 7000) {
            return payment.charge(user, minute) * 0.85;
        } else if (minute >= 4000) {
            return payment.charge(user, minute) * 0.9;
        } else if (minute >= 2000) {
            return payment.charge(user, minute) * 0.95;
        }

        return payment.charge(user, minute);

    }

}
(VIPPayment.java) download
public class VIPPayment extends AdditionalPayment {

    private final double discount;

    public VIPPayment(Payment payment, double discount) {

        super(payment);
        this.discount = discount;

    }

    @Override
    public double charge(User user, int minute) {

        if (user.isVip()){
            return payment.charge(user, minute) * discount;
        }

        return payment.charge(user, minute);

    }

}
(CompanyPayment.java) download
public class CompanyPayment extends AdditionalPayment {

    private final double serviceFee;

    public CompanyPayment(Payment payment, double serviceFee) {

        super(payment);
        this.serviceFee = 1 + serviceFee;

    }

    @Override
    public double charge(User user, int minute) {

        if (user.isCompany()){
            return payment.charge(user, minute) * serviceFee;
        }

        return payment.charge(user, minute);

    }

}
(Main.java) download
public class Main {

    public static void main(String[] args) {

        // $1.2 per minute
        Payment mobilePayment = new BasicMobilePayment(1.2);
        Payment usageAmountPayment = new UsageAmountPayment(mobilePayment);
        Payment vipPayment = new VIPPayment(usageAmountPayment, 0.9);
        Payment companyPayment = new CompanyPayment(vipPayment, 0.1);

        Payment payment = companyPayment;

        User user1 = new User("Lawrence", true, false);
        User user2 = new User("Tony", false, false);

        User user3 = new User("ABC Company", false, true);

        System.out.println(user1.getName() + " usage: 10000 minute, charge: "
                + payment.charge(user1, 10000));
        System.out.println(user2.getName() + " usage: 4000 minute, charge: "
                + payment.charge(user2, 4000));
        System.out.println(user3.getName() + " usage: 10000 minute, charge: "
                + payment.charge(user3, 10000));

    }

}
執行結果
Lawrence usage: 10000 minute, charge: 8640.0
Tony usage: 4000 minute, charge: 4320.0
ABC Company usage: 10000 minute, charge: 10560.0

這樣就可以隨時新增條件或移除條件而又不會影響到核心的程式碼。


以下的圖片是描述 Singleton Pattern 的 Class Diagram。(From Wiki)