Decorator
จุดประสงค์
หัวข้อที่มีชื่อว่า “จุดประสงค์”Decorator เพิ่มความรับผิดชอบให้ object แบบ dynamic ด้วยการห่อไว้ใน object อีกตัวที่ใช้ interface เดียวกัน เป็นทางเลือกที่ยืดหยุ่นกว่าการ subclassing เวลาต้องต่อยอดพฤติกรรม
คุณมี data source ที่อ่านและเขียน byte ได้ ตอนนี้อยากได้ feature เสริม เช่น บีบอัดข้อมูล เข้ารหัส หรือทำทั้งสองอย่างตามลำดับที่เลือกเอง ถ้าใช้ subclass ครอบทุกการผสม คุณจะได้ CompressedSource, EncryptedSource, CompressedEncryptedSource, EncryptedCompressedSource ซึ่งบานปลายเร็วมาก แถมยังตรึงตัวเลือกไว้ตั้งแต่ตอน compile จะเปิด feature ให้ object ตัวหนึ่งแล้วปิดให้อีกตัวตอน runtime ก็ทำไม่ได้
Decorator แก้ปัญหานี้ด้วย composition แต่ละ feature กลายเป็น wrapper ที่ implement interface เดียวกับของที่ห่อไว้ ถือ reference ไปยัง object ข้างใน ทำงานส่วนของตัวเองก่อน แล้วค่อยส่งต่อเข้าไปข้างใน
เพราะ decorator เป็น interface นั้นอยู่แล้ว คุณจึงซ้อนกันได้เรื่อย ๆ เช่น เข้ารหัสครอบการบีบอัดครอบ source ดิบอีกที แต่ละชั้นทำหน้าที่ของตัวเองแล้วส่งงานที่เหลือต่อ ผลคือประกอบพฤติกรรมได้ตรงตามต้องการตอน runtime แยกกันเป็นราย object โดยไม่ต้องมี subclass ใหม่เลย
เปรียบเทียบกับ inheritance
หัวข้อที่มีชื่อว่า “เปรียบเทียบกับ inheritance”inheritance กำหนดพฤติกรรมที่เพิ่มเข้ามาตายตัวตอน compile และใช้กับทุก instance ของ subclass คุณไม่สามารถผสมและจับคู่แยกตาม object หรือเปลี่ยนสแต็กในภายหลังได้ Decorator ย้ายตัวเลือกนั้นไปยัง runtime คือ object พื้นฐานตัวเดียวกันสามารถถูกห่อด้วยการผสมของ decorator ที่ต่างกัน ในลำดับที่ต่างกัน และคุณเพิ่ม decorator ใหม่ได้โดยไม่ต้องแตะ base class หรือ wrapper ที่มีอยู่ใด ๆ ต้นทุนคือ object เล็ก ๆ ที่มากขึ้นและ indirection จากการส่งต่อการเรียกผ่านแต่ละชั้น
โครงสร้าง
หัวข้อที่มีชื่อว่า “โครงสร้าง”classDiagram
class DataSource {
<<interface>>
+write(data) string
+read() string
}
class FileSource {
+write(data) string
+read() string
}
class SourceDecorator {
<<abstract>>
#wrappee: DataSource
+write(data) string
+read() string
}
class CompressionDecorator
class EncryptionDecorator
DataSource <|.. FileSource
DataSource <|.. SourceDecorator
SourceDecorator <|-- CompressionDecorator
SourceDecorator <|-- EncryptionDecorator
SourceDecorator o--> DataSource : wraps - Component (
DataSource) — interface ที่ object ดิบและ decorator ใช้ร่วมกัน - Concrete Component (
FileSource) — object พื้นฐานที่รอให้พฤติกรรมถูกต่อยอด - Decorator (
SourceDecorator) — wrapper แบบ abstract ที่ถือ component ไว้หนึ่งตัว และส่งต่อการเรียกเข้าไปโดยปริยาย - Concrete Decorator (
CompressionDecorator,EncryptionDecorator) — เพิ่มพฤติกรรมก่อนหรือหลังการ delegate ไปยัง component ที่ถูกห่อ
ตัวอย่าง
หัวข้อที่มีชื่อว่า “ตัวอย่าง”data source ถูกห่อด้วยการบีบอัดแล้วตามด้วยการเข้ารหัส ตอน write แต่ละชั้นจะแปลงข้อมูลขาเข้า ตอน read แต่ละชั้นจะย้อนกลับขาออก
interface DataSource { write(data: string): string; read(stored: string): string;}
class FileSource implements DataSource { write = (data: string) => data; read = (stored: string) => stored;}
class CompressionDecorator implements DataSource { constructor(private readonly inner: DataSource) {} write = (data: string) => this.inner.write(`zip(${data})`); read = (stored: string) => this.inner.read(stored).replace(/^zip\((.*)\)$/, '$1');}
class EncryptionDecorator implements DataSource { constructor(private readonly inner: DataSource) {} write = (data: string) => this.inner.write(`enc(${data})`); read = (stored: string) => this.inner.read(stored).replace(/^enc\((.*)\)$/, '$1');}
const source = new EncryptionDecorator(new CompressionDecorator(new FileSource()));const stored = source.write('hello');console.log(stored); // zip(enc(hello))console.log(source.read(stored)); // hellofrom typing import Protocol
class DataSource(Protocol): def write(self, data: str) -> str: ... def read(self, stored: str) -> str: ...
class FileSource: def write(self, data: str) -> str: return data
def read(self, stored: str) -> str: return stored
class CompressionDecorator: def __init__(self, inner: DataSource) -> None: self._inner = inner
def write(self, data: str) -> str: return self._inner.write(f"zip({data})")
def read(self, stored: str) -> str: out = self._inner.read(stored) return out[4:-1] if out.startswith("zip(") else out
class EncryptionDecorator: def __init__(self, inner: DataSource) -> None: self._inner = inner
def write(self, data: str) -> str: return self._inner.write(f"enc({data})")
def read(self, stored: str) -> str: out = self._inner.read(stored) return out[4:-1] if out.startswith("enc(") else out
source = EncryptionDecorator(CompressionDecorator(FileSource()))stored = source.write("hello")print(stored) # zip(enc(hello))print(source.read(stored)) # hellopackage main
import ( "fmt" "strings")
type DataSource interface { Write(data string) string Read(stored string) string}
type FileSource struct{}
func (FileSource) Write(data string) string { return data }func (FileSource) Read(stored string) string { return stored }
type CompressionDecorator struct{ inner DataSource }
func (c CompressionDecorator) Write(data string) string { return c.inner.Write("zip(" + data + ")")}func (c CompressionDecorator) Read(stored string) string { out := c.inner.Read(stored) return strings.TrimSuffix(strings.TrimPrefix(out, "zip("), ")")}
type EncryptionDecorator struct{ inner DataSource }
func (e EncryptionDecorator) Write(data string) string { return e.inner.Write("enc(" + data + ")")}func (e EncryptionDecorator) Read(stored string) string { out := e.inner.Read(stored) return strings.TrimSuffix(strings.TrimPrefix(out, "enc("), ")")}
func main() { var source DataSource = EncryptionDecorator{CompressionDecorator{FileSource{}}} stored := source.Write("hello") fmt.Println(stored) // zip(enc(hello)) fmt.Println(source.Read(stored)) // hello}trait DataSource { fn write(&self, data: &str) -> String; fn read(&self, stored: &str) -> String;}
struct FileSource;impl DataSource for FileSource { fn write(&self, data: &str) -> String { data.to_string() } fn read(&self, stored: &str) -> String { stored.to_string() }}
struct CompressionDecorator { inner: Box<dyn DataSource>,}impl DataSource for CompressionDecorator { fn write(&self, data: &str) -> String { self.inner.write(&format!("zip({data})")) } fn read(&self, stored: &str) -> String { let out = self.inner.read(stored); out.strip_prefix("zip(").and_then(|s| s.strip_suffix(")")) .map(str::to_string).unwrap_or(out) }}
struct EncryptionDecorator { inner: Box<dyn DataSource>,}impl DataSource for EncryptionDecorator { fn write(&self, data: &str) -> String { self.inner.write(&format!("enc({data})")) } fn read(&self, stored: &str) -> String { let out = self.inner.read(stored); out.strip_prefix("enc(").and_then(|s| s.strip_suffix(")")) .map(str::to_string).unwrap_or(out) }}
fn main() { let source = EncryptionDecorator { inner: Box::new(CompressionDecorator { inner: Box::new(FileSource) }), }; let stored = source.write("hello"); println!("{stored}"); // zip(enc(hello)) println!("{}", source.read(&stored)); // hello}ใช้เมื่อไหร่ / ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ใช้เมื่อไหร่ / ข้อแลกเปลี่ยน”- ข้อดี: เพิ่มหรือลบความรับผิดชอบได้ตอน runtime แยกตาม object โดยไม่ต้องแตะ base class
- ข้อดี: หลีกเลี่ยงการระเบิดของ subclass สำหรับทุกการผสมฟีเจอร์
- ข้อดี: แต่ละ decorator เป็นหน่วยเล็ก ๆ ที่มีจุดประสงค์เดียวและทดสอบได้อย่างอิสระ
- ข้อเสีย: สแต็กลึกของ wrapper จิ๋ว ๆ ดีบักและไล่ทีละขั้นได้ยาก
- ข้อเสีย: ลำดับมีความสำคัญ การเข้ารหัสแล้วบีบอัดต่างจากการบีบอัดแล้วเข้ารหัส และการต่อสายผิดได้ง่าย
pattern ที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “pattern ที่เกี่ยวข้อง”- Adapter ห่อเพื่อเปลี่ยน interface ส่วน Decorator ห่อโดยคง interface เดิมไว้แล้วเพิ่มพฤติกรรมเข้าไป
- Composite ใช้แนวคิด recursive wrapping เหมือนกัน แต่รวมผลจากลูกหลายตัว ไม่ได้เสริมตัวเดียว
- Proxy ก็ห่อ object ด้วย interface เดียวกันเช่นกัน แต่เพื่อ ควบคุมการเข้าถึง แทนที่จะเพิ่มฟีเจอร์
| Decorator | Proxy | Subclass | |
|---|---|---|---|
| เพิ่ม behavior | ในขณะ runtime | ควบคุม access | ในขณะ compile |
| จำนวนที่ซ้อนได้ | ได้หลายชั้น | ปกติชั้นเดียว | ต้องสร้าง class ใหม่ทุกชุด |
| object ต้นแบบ | ยังคงอยู่ | ถูกซ่อน | ถูกขยาย |
| ตัวอย่าง | stream.pipe(), Express middleware | auth proxy, cache proxy | Array extends Object |