#เพิ่มความสามารถให้ Object ด้วยการห่อเป็นชั้น ๆ

info-decorator-pattern-with-java

Decorator Pattern เป็นรูปแบบการออกแบบเชิงโครงสร้าง (Structural Design Pattern) ที่ใช้เพิ่มความรับผิดชอบให้ object แต่ละตัว โดยนำ object เดิมไปห่อด้วย object อีกตัวที่ใช้งานผ่านสัญญาเดียวกัน แล้วส่งต่อการทำงานพร้อมเพิ่มพฤติกรรมที่ต้องการ

ตัวอย่างในบทความนี้คือระบบสั่งพิซซ่า: เริ่มจากพิซซ่าพื้นฐาน เพิ่มชีส เพิ่มเห็ด หรือเพิ่มชีสสองครั้งได้ โดยไม่ต้องสร้างคลาสใหม่สำหรับทุกชุดท็อปปิง

แนวคิดสำคัญ: ผู้เรียกใช้ทำงานผ่าน interface เดิม ส่วนความสามารถเพิ่มเติมอยู่ใน object ที่ห่อซ้อนกัน

กลุ่มผู้อ่าน: ผู้เรียน Java ที่เข้าใจ interface, inheritance และ composition เบื้องต้น
เครื่องมือ: JDK 17 ขึ้นไป ไม่ต้องติดตั้งไลบรารีเพิ่มเติม
ผลลัพธ์: โปรแกรมคำนวณราคาพิซซ่า พร้อมอธิบายโครงสร้าง การเรียกซ้อน และข้อควรระวัง


#1. ปัญหาที่พบเมื่อใช้ Inheritance กับทุกชุดตัวเลือก

สมมติว่าเรามีพิซซ่าพื้นฐานและท็อปปิงหลายชนิด หากสร้าง subclass สำหรับทุกชุด จะเริ่มมีชื่อคลาสเช่น

  • CheesePizza
  • MushroomPizza
  • CheeseMushroomPizza
  • DoubleCheesePizza
  • CheeseMushroomOlivePizza

เมื่อมีตัวเลือกอิสระ n ชนิด และแต่ละชนิดเลือกได้เพียงมีหรือไม่มี จะมีชุดตัวเลือกได้ถึง 2^n ชุด รวมชุดที่ไม่เพิ่มอะไรเลย ถ้ารองรับปริมาณซ้ำและลำดับด้วย จำนวนกรณีก็เพิ่มขึ้นอีก

Decorator แยกความสามารถแต่ละอย่างเป็นคลาสของตนเอง แล้วประกอบ object ตามคำสั่งซื้อ ทำให้ไม่จำเป็นต้องสร้าง subclass ให้ทุกชุดตัวเลือก

วิธีออกแบบ สิ่งที่สร้างเพิ่มเมื่อมีท็อปปิงใหม่ ผลต่อการประกอบตัวเลือก
Subclass สำหรับทุกชุด คลาสของชุดที่มีท็อปปิงนั้น ชุดตัวเลือกผูกอยู่กับชนิดคลาส
Decorator คลาสท็อปปิงที่ห่อ Pizza ได้ ประกอบแต่ละคำสั่งซื้อขณะโปรแกรมทำงาน
ข้อมูลท็อปปิงในรายการ ข้อมูลหรือกฎคำนวณ เหมาะเมื่อความแตกต่างเป็นเพียงชื่อและราคา

ข้อสังเกตในการออกแบบ: ถ้าท็อปปิงมีเพียงชื่อและราคา การใช้รายการข้อมูลอาจเรียบง่ายกว่า ตัวอย่างพิซซ่าในบทความนี้มีไว้ช่วยให้เข้าใจกลไกของ Pattern ส่วน Decorator จะมีคุณค่ามากขึ้นเมื่อแต่ละชั้นมีพฤติกรรมจริง เช่น buffering, logging หรือการแปลงข้อมูล


#2. Decorator ทำงานอย่างไร

Decorator ที่ใช้ในบทความนี้มีคุณสมบัติหลักสามอย่าง

  1. ใช้ interface เดียวกับ object ที่ถูกห่อ
  2. เก็บ reference ไปยัง object นั้น
  3. ส่งต่อการเรียก แล้วเพิ่มพฤติกรรมก่อน หลัง หรือรอบการทำงานเดิม

ตัวอย่าง:

Pizza order = new Mushroom(
        new Cheese(
                new MargheritaPizza()));

อ่านนิพจน์จากด้านในออกมาได้ว่า สร้างพิซซ่าพื้นฐาน ห่อด้วยชีส แล้วห่อด้วยเห็ด

เมื่อเรียก order.price() การเรียกจะเริ่มจากชั้นนอกสุดคือ Mushroom ส่งต่อไปยัง Cheese และ MargheritaPizza จากนั้นผลลัพธ์จะถูกบวกเพิ่มระหว่างส่งกลับออกมา

ชั้นที่ส่งผลลัพธ์กลับ การคำนวณ ราคา
MargheritaPizza ราคาพื้นฐาน 100.00 บาท
Cheese 100.00 + 25.00 125.00 บาท
Mushroom 125.00 + 15.00 140.00 บาท

order ยังคงเป็นชนิด Pizza ผู้เรียกใช้จึงไม่ต้องตรวจว่าเพิ่มท็อปปิงชนิดใดไว้บ้างเพื่อเรียก price()


#3. องค์ประกอบหลักของ Pattern

บทบาท คลาสในตัวอย่าง หน้าที่
Component Pizza กำหนดสัญญาร่วม ได้แก่ description() และ price()
Concrete Component MargheritaPizza ให้พฤติกรรมพื้นฐาน
Base Decorator PizzaDecorator ใช้สัญญาเดียวกัน เก็บ object ที่ถูกห่อ และส่งต่อการทำงาน
Concrete Decorator Cheese และ Mushroom เพิ่มรายละเอียดและราคา

Base Decorator เป็นวิธีลดโค้ดซ้ำที่นิยมใช้ แต่ไม่ใช่ข้อบังคับของ Pattern สามารถให้ Concrete Decorator implement Pizza และเก็บ Pizza โดยตรงได้เช่นกัน

#แผนภาพโครงสร้างคลาส

classDiagram
    direction TB
    class Pizza {
        <<interface>>
        +description() String
        +price() BigDecimal
    }
    class MargheritaPizza
    class PizzaDecorator {
        <<abstract>>
        #Pizza wrapped
        +description() String
        +price() BigDecimal
    }
    class Cheese
    class Mushroom

    Pizza <|.. MargheritaPizza
    Pizza <|.. PizzaDecorator
    PizzaDecorator o--> Pizza : wraps
    PizzaDecorator <|-- Cheese
    PizzaDecorator <|-- Mushroom

เส้น implements ทำให้เรียกผ่านสัญญาเดียวกัน ส่วนความสัมพันธ์ wraps ทำให้ Decorator ส่งต่อการทำงานไปยัง object อีกตัวได้ จึงเป็นการใช้ inheritance ร่วมกับ composition โดยให้ composition เป็นกลไกประกอบความสามารถ


#4. Workshop: ระบบสั่งพิซซ่า

กำหนดราคาเพื่อใช้ทดลองดังนี้

รายการ ราคา
Margherita 100.00 บาท
เพิ่มชีสหนึ่งชั้น 25.00 บาท
เพิ่มเห็ดหนึ่งชั้น 15.00 บาท

ในตัวอย่างใช้ BigDecimal และสร้างค่าจากข้อความ เช่น new BigDecimal("25.00") เพื่อระบุค่าทศนิยมโดยตรง หลีกเลี่ยงการนำค่าประมาณจาก double เข้ามาโดยไม่ตั้งใจ ตามพฤติกรรม constructor ที่ระบุใน Java API [3]

#ขั้นตอนที่ 1: สร้างไฟล์ DecoratorDemo.java

คัดลอกโค้ดทั้งหมดต่อไปนี้ลงในไฟล์เดียว โค้ดมี public class เพียงตัวเดียว ส่วนคลาสอื่นมีขอบเขตระดับ package เพื่อให้เริ่มทดลองได้ง่าย

import java.math.BigDecimal;
import java.util.Objects;

public class DecoratorDemo {
    public static void main(String[] args) {
        Pizza base = new MargheritaPizza();
        Pizza cheese = new Cheese(base);
        Pizza loaded = new Mushroom(cheese);
        Pizza doubleCheese = new Cheese(new Cheese(base));
        Pizza reversed = new Cheese(new Mushroom(base));

        print("Base", base);
        print("Cheese", cheese);
        print("Loaded", loaded);
        print("Double cheese", doubleCheese);
        print("Reversed", reversed);
    }

    private static void print(String label, Pizza pizza) {
        System.out.println(label + ": " + pizza.description()
                + " = " + pizza.price().toPlainString() + " THB");
    }
}

interface Pizza {
    String description();
    BigDecimal price();
}

final class MargheritaPizza implements Pizza {
    @Override
    public String description() {
        return "Margherita";
    }

    @Override
    public BigDecimal price() {
        return new BigDecimal("100.00");
    }
}

abstract class PizzaDecorator implements Pizza {
    protected final Pizza wrapped;

    protected PizzaDecorator(Pizza wrapped) {
        this.wrapped = Objects.requireNonNull(wrapped, "wrapped pizza");
    }

    @Override
    public String description() {
        return wrapped.description();
    }

    @Override
    public BigDecimal price() {
        return wrapped.price();
    }
}

final class Cheese extends PizzaDecorator {
    private static final BigDecimal EXTRA = new BigDecimal("25.00");

    Cheese(Pizza wrapped) {
        super(wrapped);
    }

    @Override
    public String description() {
        return super.description() + " + Cheese";
    }

    @Override
    public BigDecimal price() {
        return super.price().add(EXTRA);
    }
}

final class Mushroom extends PizzaDecorator {
    private static final BigDecimal EXTRA = new BigDecimal("15.00");

    Mushroom(Pizza wrapped) {
        super(wrapped);
    }

    @Override
    public String description() {
        return super.description() + " + Mushroom";
    }

    @Override
    public BigDecimal price() {
        return super.price().add(EXTRA);
    }
}

#ขั้นตอนที่ 2: คอมไพล์และรัน

ตรวจสอบว่าติดตั้ง JDK แล้ว:

java -version
javac -version

คอมไพล์และเรียกโปรแกรม:

javac -encoding UTF-8 DecoratorDemo.java
java DecoratorDemo

หรือรัน source file โดยตรงด้วย JDK 17 ขึ้นไป:

java DecoratorDemo.java

#ขั้นตอนที่ 3: ตรวจสอบผลลัพธ์

Base: Margherita = 100.00 THB
Cheese: Margherita + Cheese = 125.00 THB
Loaded: Margherita + Cheese + Mushroom = 140.00 THB
Double cheese: Margherita + Cheese + Cheese = 150.00 THB
Reversed: Margherita + Mushroom + Cheese = 140.00 THB

base ยังคงมีราคา 100.00 บาท เพราะตัวอย่างนี้สร้าง wrapper ใหม่โดยไม่แก้สถานะของ object เดิม การไม่แก้สถานะดังกล่าวเป็นคุณสมบัติของ implementation นี้ ไม่ใช่สิ่งที่ Decorator Pattern รับประกันให้โดยอัตโนมัติ


#5. อ่านโค้ดทีละส่วน

#Component: สัญญาที่ผู้เรียกใช้มองเห็น

interface Pizza {
    String description();
    BigDecimal price();
}

ทั้งพิซซ่าพื้นฐานและท็อปปิงมีสัญญานี้ร่วมกัน จึงสามารถใช้ Cheese แทน Pizza ในจุดที่ต้องการคำนวณราคาได้

#Concrete Component: งานพื้นฐาน

MargheritaPizza ให้ชื่อและราคาของพิซซ่าที่ไม่เพิ่มท็อปปิง คลาสนี้ไม่จำเป็นต้องรู้ว่ามี Cheese หรือ Mushroom อยู่ในระบบ

#Base Decorator: เก็บ object และส่งต่อ

protected final Pizza wrapped;

protected PizzaDecorator(Pizza wrapped) {
    this.wrapped = Objects.requireNonNull(wrapped, "wrapped pizza");
}

ใช้ชนิด Pizza แทนการผูกกับ MargheritaPizza ทำให้ห่อได้ทั้งพิซซ่าพื้นฐานและ Decorator อีกชั้น ส่วน requireNonNull ทำให้ตรวจพบการประกอบ object ที่ผิดตั้งแต่ constructor

final ป้องกันการเปลี่ยน reference หลังสร้าง wrapper แต่ไม่ได้ทำให้ object ที่ถูกอ้างถึงกลายเป็น immutable เสมอไป

#Concrete Decorator: ส่งต่อแล้วเพิ่มงาน

@Override
public BigDecimal price() {
    return super.price().add(EXTRA);
}

super.price() เรียก implementation ใน PizzaDecorator ซึ่งส่งต่อไปยัง wrapped.price() จากนั้นชั้นปัจจุบันจึงบวกราคาของตนเอง

ถ้าคืนค่าเพียง EXTRA โดยไม่ใช้ราคาจาก object ด้านใน ราคาของชั้นก่อนหน้าจะหายไป


#6. ประกอบความสามารถตามตัวเลือกขณะทำงาน

ในระบบจริง ตัวเลือกอาจมาจากหน้าเว็บหรือ request ของ API สามารถสร้างคำสั่งซื้อแบบมีเงื่อนไขได้

เพิ่มเมธอดต่อไปนี้ภายในคลาส DecoratorDemo:

private static Pizza createOrder(boolean addCheese, boolean addMushroom) {
    Pizza order = new MargheritaPizza();

    if (addCheese) {
        order = new Cheese(order);
    }
    if (addMushroom) {
        order = new Mushroom(order);
    }

    return order;
}

จากนั้นเรียกภายใน main:

Pizza order = createOrder(true, true);
print("Selected", order);

ผลลัพธ์:

Selected: Margherita + Cheese + Mushroom = 140.00 THB

ตัวแปร order ถูกเปลี่ยนให้อ้างถึง wrapper ชั้นใหม่ แต่ยังคงใช้ชนิด Pizza ตลอด เมธอดประกอบคำสั่งซื้อเป็นจุดที่รับผิดชอบการเลือกชั้น ส่วนแต่ละ Decorator รับผิดชอบพฤติกรรมของตน


#7. เพิ่ม Decorator ใหม่โดยไม่แก้คลาสเดิม

สมมติต้องการเพิ่มมะกอกราคา 20.00 บาท ให้เพิ่มคลาสนี้ไว้ต่อท้ายไฟล์ DecoratorDemo.java:

final class Olive extends PizzaDecorator {
    private static final BigDecimal EXTRA = new BigDecimal("20.00");

    Olive(Pizza wrapped) {
        super(wrapped);
    }

    @Override
    public String description() {
        return super.description() + " + Olive";
    }

    @Override
    public BigDecimal price() {
        return super.price().add(EXTRA);
    }
}

เพิ่มโค้ดภายใน main เพื่อทดลอง:

Pizza special = new Olive(
        new Mushroom(
                new Cheese(
                        new MargheritaPizza())));
print("Special", special);

ผลลัพธ์:

Special: Margherita + Cheese + Mushroom + Olive = 160.00 THB

ไม่ต้องแก้ MargheritaPizza, Cheese หรือ Mushroom แต่ต้องปรับจุดประกอบคำสั่งซื้อให้เลือกใช้ Olive ได้ นี่คือขอบเขตที่ชัดเจนของหลัก Open/Closed ในตัวอย่างนี้: ส่วนพฤติกรรมเดิมคงที่ ส่วนความสามารถใหม่เพิ่มผ่านคลาสใหม่


#8. ลำดับการห่อมีผลหรือไม่

พิจารณาสองคำสั่งนี้:

Pizza first = new Mushroom(new Cheese(new MargheritaPizza()));
Pizza second = new Cheese(new Mushroom(new MargheritaPizza()));

ทั้งสองราคา 140.00 บาท เพราะตัวอย่างนี้บวกค่าคงที่เหมือนกัน แต่ description() เรียงข้อความต่างกัน

อย่าสรุปว่า Decorator สลับลำดับได้เสมอ หากแต่ละชั้นทำงานต่างชนิดกัน ผลลัพธ์อาจเปลี่ยน เช่น ส่วนลดหรือการแปลงข้อมูล

ลำดับการคำนวณจากราคาตั้งต้น 100 บาท วิธีคิด ราคาสุดท้าย
ลด 10 บาท แล้วลด 10% (100 − 10) × 0.90 81.00 บาท
ลด 10% แล้วลด 10 บาท (100 × 0.90) − 10 80.00 บาท

หากเมธอดของแต่ละชั้นเรียกชั้นด้านในก่อน แล้วค่อยใช้ส่วนลดของตนเอง ชั้นที่อยู่ด้านในจะคำนวณส่วนลดก่อนชั้นนอก สำหรับระบบราคาเงินจริงควรกำหนดลำดับ กติกาค่าต่ำสุด และการปัดเศษให้ชัดเจน


#9. ตัวอย่างใกล้เคียงใน Java I/O

Java API ระบุว่า FilterInputStream ห่อ input stream อีกตัวหนึ่ง แล้วอาจแปลงข้อมูลหรือเพิ่มความสามารถ ขณะที่ BufferedInputStream เป็น subclass ที่เพิ่ม buffering [1][2]

เมื่อนำมาเทียบกับ Decorator Pattern จะเห็นรูปแบบเดียวกัน: ใช้ชนิด stream ร่วมกัน เก็บ stream ด้านใน และประกอบความสามารถด้วย wrapper การจัดบทบาทนี้เป็นการอธิบายโครงสร้างจาก API

สร้างไฟล์ IoDecoratorDemo.java:

import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.IOException;

public class IoDecoratorDemo {
    public static void main(String[] args) throws IOException {
        byte[] bytes = {0, 0, 0, 42};

        try (DataInputStream input = new DataInputStream(
                new BufferedInputStream(
                        new ByteArrayInputStream(bytes)))) {
            System.out.println(input.readInt());
        }
    }
}

รัน:

javac IoDecoratorDemo.java
java IoDecoratorDemo

ผลลัพธ์:

42
Object บทบาทในตัวอย่าง
ByteArrayInputStream เป็นแหล่งข้อมูลไบต์ในหน่วยความจำ
BufferedInputStream เพิ่มการอ่านผ่าน buffer
DataInputStream เพิ่มเมธอดอ่านข้อมูลพื้นฐาน เช่น readInt()

ตัวอย่างใช้ข้อมูลในหน่วยความจำเพื่อให้รันได้ทันที Buffering จะเห็นประโยชน์ชัดขึ้นเมื่อแหล่งข้อมูลมีต้นทุนการอ่าน เช่น ไฟล์ การเรียก readInt() ครั้งเดียวในตัวอย่างนี้มีไว้แสดงการประกอบ object ไม่ใช่การวัดประสิทธิภาพ

try-with-resources ปิด stream ชั้นนอกเมื่อจบ block และ stream wrapper ในชุดนี้ส่งต่อการปิดไปยังชั้นด้านใน ควรตรวจสัญญาของ wrapper ที่ใช้งานจริงเสมอ โดยเฉพาะเมื่อห่อ resource ที่ใช้ร่วมกัน [1]


#10. Decorator ต่างจาก Pattern ใกล้เคียงอย่างไร

Pattern เป้าหมายหลัก วิธีคิดเมื่อออกแบบ
Decorator เพิ่มพฤติกรรมให้ object ผ่านสัญญาเดิมและประกอบซ้อนกันได้ “เพิ่มความสามารถอีกชั้น”
Adapter ทำให้ interface ที่ต่างกันใช้งานร่วมกันได้ “แปลงสัญญาให้ตรงกับที่ผู้ใช้ต้องการ”
Proxy ควบคุมการเข้าถึง object เช่น lazy loading หรือ remote access “จัดการการเข้าถึงตัวจริง”
Composite จัดกลุ่ม object เป็นโครงสร้างต้นไม้และปฏิบัติต่อรายตัวกับกลุ่มผ่านสัญญาร่วม “จัดการส่วนย่อยและองค์รวม”
Strategy สลับอัลกอริทึมที่ object ใช้งาน “เลือกวิธีทำงาน”

Decorator กับ Proxy อาจมีโครงสร้างคล้ายกันมาก ควรพิจารณาเจตนาและหน้าที่จริง ส่วน logging, caching หรือ security ไม่ได้ชี้ว่าเป็น Pattern ใดโดยอัตโนมัติ ต้องดูการออกแบบและเหตุผลที่เพิ่ม wrapper นั้น

Decorator ของ GoF ใน Java ก็ไม่ใช่กลไก annotation @... หรือคำว่า decorator ในภาษาอื่นโดยตรง แม้อาจมีแนวคิดเพิ่มพฤติกรรมที่คล้ายกัน


#11. ประโยชน์และต้นทุน

ประเด็น สิ่งที่ได้ สิ่งที่ต้องดูแล
การขยายความสามารถ เพิ่ม Decorator ใหม่ได้ จุดประกอบ object ต้องเลือกใช้ชั้นใหม่
การใช้ซ้ำ ใช้ชั้นเดียวกันกับ Component หลายชนิดได้ ทุกชนิดต้องรักษาสัญญาร่วม
การประกอบ เลือกชุดความสามารถขณะทำงานได้ ลำดับและการซ้ำอาจมีความหมาย
การดูแลโค้ด แยกหน้าที่ของแต่ละชั้นได้ มีคลาสและเส้นทางเรียกเพิ่มขึ้น
การทดสอบ ตรวจพฤติกรรมแต่ละชั้นและการประกอบได้ ต้องเลือกกรณีผสมที่เสี่ยงผิดจริง

ถ้ามี k ชั้นและแต่ละชั้นส่งต่อเมธอดหนึ่งครั้งพร้อมทำงาน O(1) เวลาของการเรียกนั้นจะเป็น O(k) และใช้พื้นที่เก็บ wrapper O(k) ส่วนการต่อ string ยาวขึ้นทีละชั้นอาจมีต้นทุนคัดลอกสะสมมากกว่านั้น จึงไม่ควรเหมารวมว่าเมธอดทุกตัวมีต้นทุนเท่ากัน


#12. ข้อควรระวังในการนำไปใช้

#รักษาสัญญาของ Component

Decorator ควรยังใช้แทน Component ในขอบเขตสัญญาที่กำหนดไว้ได้ เช่น price() ควรคืนราคาของรายการที่ห่อรวมกับความสามารถของชั้นปัจจุบัน โดยไม่เปลี่ยนไปทำงานที่ไม่เกี่ยวข้อง

#แยก Reference ออกจากสถานะ

ตัวอย่างเก็บ wrapped เป็น final แต่ถ้า Component ด้านในเปลี่ยนสถานะได้ wrapper ที่แชร์ Component ตัวเดียวกันก็อาจเห็นการเปลี่ยนแปลงนั้นด้วย

#ระวังการเพิ่มซ้ำ

new Cheese(new Cheese(base)) เพิ่มชีสสองครั้งและคิดเงินสองครั้ง ตัวอย่างนี้อนุญาต แต่ในระบบแจ้งเตือน การห่อชั้นส่ง SMS ซ้ำอาจทำให้ส่งข้อความซ้ำ ต้องกำหนดกฎการประกอบให้ตรงกับงาน

#อย่าซ่อนความล้มเหลวโดยไม่มีนโยบาย

เมื่อชั้นด้านในโยน exception ชั้นนอกต้องตัดสินใจตามสัญญาว่าจะส่งต่อ แปลง หรือจัดการอย่างไร หากเพิ่ม retry รอบงานที่มีผลข้างเคียง อาจทำให้เกิดงานซ้ำ ควรออกแบบเรื่อง idempotency ตามระบบนั้น

#กำหนดอายุของ Resource

เมื่อ Decorator ห่อ stream, connection หรือ resource อื่น ต้องระบุว่าการปิดชั้นนอกปิดชั้นในด้วยหรือไม่ และใครเป็นเจ้าของ resource

#เข้าใจว่ากำลังใช้ Object คนละตัว

new Cheese(base) เป็น object ใหม่ มี identity ต่างจาก base และไม่ได้เป็น instance ของ MargheritaPizza โดยตรง จึงควรเรียกผ่าน Pizza หลีกเลี่ยงการ cast เพื่อค้นหาทุกชั้น

#ไม่ใช้ Pattern เกินความจำเป็น

หากมีความสามารถเพิ่มเติมเพียงอย่างเดียวและไม่ต้องประกอบซ้อน เมธอดธรรมดาหรือคลาส wrapper เดียวอาจพอ หากเพิ่มเพียงข้อมูลท็อปปิง รายการข้อมูลอาจดูแลง่ายกว่า Decorator หลายสิบคลาส


#13. แนวทางตรวจสอบความถูกต้อง

สำหรับตัวอย่างในบทความนี้ ให้ตรวจพฤติกรรมที่สังเกตได้ดังต่อไปนี้

กรณี ผลที่ควรได้
พิซซ่าพื้นฐาน 100.00 บาท
เพิ่มชีส 125.00 บาท
เพิ่มเห็ด 115.00 บาท
เพิ่มชีสและเห็ด 140.00 บาท
เพิ่มชีสสองครั้ง 150.00 บาท
เพิ่มชีส เห็ด และมะกอก 160.00 บาท
สลับลำดับชีสกับเห็ด ราคาเท่าเดิม แต่รายละเอียดเรียงต่างกัน
อ่านราคาจาก base หลังสร้าง wrapper ยังเป็น 100.00 บาทใน implementation นี้
สร้าง new Cheese(null) เกิด NullPointerException ตอนสร้าง

เมื่อตรวจค่าตัวเลขของ BigDecimal ให้เลือกตามเจตนา: compareTo เปรียบเทียบค่าตัวเลข ขณะที่ equals พิจารณา scale ด้วย เช่น 140.0 และ 140.00 มีค่าตัวเลขเท่ากัน แต่ equals ให้ผลเป็น false [3]

ตัวอย่างเงื่อนไขตรวจค่าตัวเลขภายใน main:

if (loaded.price().compareTo(new BigDecimal("140.00")) != 0) {
    throw new AssertionError("Loaded pizza should cost 140.00");
}

สำหรับระบบที่มีผลข้างเคียง ควรเพิ่มกรณีตรวจลำดับการเรียก จำนวนครั้งที่เรียก และพฤติกรรมเมื่อชั้นด้านในล้มเหลวด้วย


#14. แบบฝึกหัด

  1. เพิ่ม Olive แล้วตรวจราคาชีส + เห็ด + มะกอกว่าได้ 160.00 บาท
  2. เพิ่ม ExtraSauce ราคา 10.00 บาท โดยใช้ PizzaDecorator เดิม
  3. เปลี่ยน createOrder ให้รับจำนวนชีส แล้วใช้ loop ประกอบหลายชั้น
  4. ออกแบบส่วนลด 10% และส่วนลด 10 บาท ทดลองสลับลำดับ พร้อมกำหนดการปัดเศษให้ชัดเจน
  5. อธิบายว่าเหตุใด new Cheese(base) จึงไม่เปลี่ยน base.price() ในตัวอย่างนี้ และจะเกิดอะไรขึ้นหาก base มี setter เปลี่ยนราคา

เกณฑ์ตรวจงาน: ความสามารถใหม่ต้องรับ Pizza เป็น object ที่ห่อ ส่งต่อพฤติกรรมที่ยังต้องใช้ และยังเรียกผ่านชนิด Pizza ได้


#15. ทบทวนแนวคิด

Decorator ใช้การห่อ object ที่มีสัญญาร่วมกันเพื่อเพิ่มพฤติกรรมเป็นชั้น ๆ ช่วยให้ประกอบความสามารถของแต่ละ instance ขณะโปรแกรมทำงานได้ โดยไม่สร้าง subclass สำหรับทุกชุดตัวเลือก

สำหรับตัวอย่างนี้ Pizza คือสัญญา MargheritaPizza คือพฤติกรรมพื้นฐาน และ Cheese กับ Mushroom คือชั้นที่ส่งต่อพร้อมเพิ่มงาน เมื่อใช้งานจริงต้องใส่ใจกับลำดับ การเพิ่มซ้ำ สถานะที่แชร์กัน และสัญญาเรื่องความล้มเหลวหรือ resource


#เอกสารอ้างอิง

[1] Oracle. FilterInputStream — Java SE 21 API. อธิบายการห่อ stream การส่งต่อ และการปิด stream ด้านใน

[2] Oracle. BufferedInputStream — Java SE 21 API. อธิบายการเพิ่มความสามารถ buffering ให้ input stream

[3] Oracle. BigDecimal — Java SE 21 API. อธิบาย constructor, การคำนวณทศนิยม และความแตกต่างระหว่าง equals กับ compareTo

หมายเหตุ: โค้ดพิซซ่าเป็นตัวอย่างที่จัดทำขึ้นเพื่อการเรียนรู้ ส่วนตัวอย่าง Java I/O อ้างอิงโครงสร้างและพฤติกรรมจากเอกสาร Oracle ข้างต้น