/ OOD

[365 วันแห่งโปรแกรม #day55] Decorator pattern

วันที่ห้าสิบห้าของ ‪#‎365วันแห่งโปรแกรม วันนี้เราจะคุยกันเรื่อง Decorator pattern


Decorator pattern

Decorator pattern หรือ Wrapper (ในบางครั้งคำว่า Wrapper ก็หมายถึง Adapter pattern ได้เช่นกัน) เป็นแพทเทิร์นแบบ structural เช่นเดิม แพทเทิร์นนี้ออกแบบมาสำหรับช่วยให้เราสามารถเพิ่มความสามารถ (Behavior) ให้กับ object ได้ในตอนรันไทม์

Decorator pattern จะต่างจาก Brigde pattern ตรงที่ object เดิมของเราก็ยังทำงานเหมือนเดิม แต่มี Behavior ใหม่มาครอบไว้อีกที แทนที่จะเปลี่ยน Implementation ไปเลย แบบ Brigde pattern

Class Diagram

ดู Class Diagram แล้วจะตกจครับ เพราะมันหน้าตาเหมิอน Composite pattern เป๊ะเลย

Decorator pattern

จะเห็นว่าต่างจาก Diagram ของ Composite pattern ตรงที่ตัว Decorator จะ decorate (compose) object (ที่มี interface เดียวกัน) เพียงตัวเดียวเท่านั้น แต่เราก็ยังสามารถ decorate ซ้อนกันไปเรื่อยๆ เป็นสายได้ (แต่ของ Composite pattern ทำเป็น tree ได้เลย)

ตัวอย่างการใช้งาน

ไม่น่าเชื่อครับว่าแพทเทิร์นนี้เราแทบทุกคนที่เขียนโปรแกรมนั้นใช้กันเป็นประจำอยู่แล้ว เพราะว่าแนวคิดของ Stream ใช้แพทเทิร์นนี้ครับ เราสามารถต่อ Stream หนึ่งเข้ากับอีก Stream หนึ่ง เพื่อเพิ่มความสามารถได้เรื่อยๆ เช่นเรามี Stream สำหรับอ่านไฟล์ซึ่งคืนค่าเป็น byte[] เราก็เอาไปต่อกับ Stream อื่นที่แปลงข้อมูลเป็น text ได้ แค่นี้เราก็อ่านไฟล์เป็น text ได้แล้วครับ

อีกตัวอย่างนึงที่ผมชอบคือ coffee making scenario จาก วิกิพีเดีย

scenario นี้บอกไว้ว่าเรามี interface และ class ของการทำกาแฟอยู่ ดังนี้

public abstract class Coffee {
    public abstract double getCost();
    public abstract String getIngredients();
}

public class SimpleCoffee extends Coffee {
    public double getCost() {
        return 1;
    }

    public String getIngredients() {
        return "Coffee";
    }
}

กาแฟทุกชนิดก็จะมีพื้นฐานเหมือนๆ กันคือต้องใส่กาแฟ (ก็แน่หละ ><) ส่วนที่ต่างจะเป็นเรื่องขององค์ประกอบอื่น เช่น นม วิปครีม หรืออื่นๆ (ผมอาจจะบอกผิดบอกถูกต้องขออภัยนะครับ ผมไม่ดื่มกาแฟ) ถ้าเราจะใส่อย่างอื่นเพิ่มเราก็ต้อง inherit คลาสพื้นฐานไปเป็นอย่างๆ ซึ่งมันดูยุ่งยาก ดังนั้นเราจะมาใช้ Docorator pattern กันครับ

public abstract class CoffeeDecorator extends Coffee {
    protected final Coffee decoratedCoffee;
    protected String ingredientSeparator = ", ";

    public CoffeeDecorator (Coffee decoratedCoffee) {
        this.decoratedCoffee = decoratedCoffee;
    }

    public double getCost() {
        return decoratedCoffee.getCost();
    }

    public String getIngredients() {
        return decoratedCoffee.getIngredients();
    }
}

ข้างบนคือโค้ดของ Decorator ครับ เป็น abstraction ที่เราจะนำไปสร้าง concrete decorator ต่อไป โดยภายใน Decorator ของเราก็จะมี Coffee และ String อีกตัวนึงที่ใช้เก็บส่วนประกอบของกาแฟ

*Decorator จะต้อง derived จาก base class ของ component ที่เราจะ Decorate เสมอ เพื่อรักษา interface เดิมไว้

ต่อมาเราจะสร้าง concrete decorator สำหรับใช้ใส่นม และวิปครีม ลงในกาปฟครับ

class Milk extends CoffeeDecorator {
    public Milk (Coffee decoratedCoffee) {
        super(decoratedCoffee);
    }

    public double getCost() {
        return super.getCost() + 0.5;
    }

    public String getIngredients() {
        return super.getIngredients() + ingredientSeparator + "Milk";
    }
}

class Whip extends CoffeeDecorator {
    public Whip (Coffee decoratedCoffee) {
        super(decoratedCoffee);
    }

    public double getCost() {
        return super.getCost() + 0.7;
    }

    public String getIngredients() {
        return super.getIngredients() + ingredientSeparator + "Whip";
    }
}

ก็ง่ายๆ เลย แค่สร้างคลาสใหม่จาก CoffeeDecorator แล้วก็ implement สิ่งที่จะเพิ่มลงไป ในตัวอย่างจะเป็นการเพิ่มราคา (แน่อยู่แล้ว ใส่อย่างอื่นเพิ่มก็ต้องคิดเงิน ><) กับเพิ่มวัตถุดิบ (เอาวัตถุดิบเดิมบวกด้วยสิ่งที่เราจะใส่เพิ่ม)

*อย่าลืมว่าเราต้อง inject instance ของ Coffee เข้ามาด้วยนะครับ ในตัวอย่าง inject ที่ constructor)

ต่อมาจะเป็นการนำไปใช้ครับ

ถ้าเราต้องการกาแฟใส่นม ก็จะต้องสร้าง SimpleCoffee กับ Milk ขึ้นมา ดังนี้

Coffee c = new SimpleCoffee();
System.out.println("Cost: " + c.getCost() + "; Ingredients: " + c.getIngredients());

c = new Milk(new SimpleCoffee());
System.out.println("Cost: " + c.getCost() + "; Ingredients: " + c.getIngredients());

===output===
Cost: 1.0; Ingredients: Coffee
Cost: 1.5; Ingredients: Coffee, Milk

จะเห็นว่าพอเราใส่นมปุ๊บ ราคาก็จะเพิ่มขึ้นทันทีเลย

ต่อมาเราจะใส่วิปครีมครับ

c = new Sprinkles(new Whip(new Sprinkles(new Milk(new SimpleCoffee()))));
System.out.println("Cost: " + c.getCost() + "; Ingredients: " + c.getIngredients());

===output===
Cost: 2.2; Ingredients: Coffee, Milk, Whip

แค่นี้เราก็ได้กาแฟที่ใส่นมและวิปครีมแล้วครับ

Reference

วันนี้ Reference อันเดียวคือ Decorator pattern - Wikipedia

#‎day55 #365วันแห่งโปรแกรม ‪#‎โครงการ365วันแห่ง‬...