ข้ามไปยังเนื้อหา

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 กำหนดพฤติกรรมที่เพิ่มเข้ามาตายตัวตอน 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
decorator ใช้ component interface ร่วมกันและห่อ component ภายใน
  • 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)); // hello
  • ข้อดี: เพิ่มหรือลบความรับผิดชอบได้ตอน runtime แยกตาม object โดยไม่ต้องแตะ base class
  • ข้อดี: หลีกเลี่ยงการระเบิดของ subclass สำหรับทุกการผสมฟีเจอร์
  • ข้อดี: แต่ละ decorator เป็นหน่วยเล็ก ๆ ที่มีจุดประสงค์เดียวและทดสอบได้อย่างอิสระ
  • ข้อเสีย: สแต็กลึกของ wrapper จิ๋ว ๆ ดีบักและไล่ทีละขั้นได้ยาก
  • ข้อเสีย: ลำดับมีความสำคัญ การเข้ารหัสแล้วบีบอัดต่างจากการบีบอัดแล้วเข้ารหัส และการต่อสายผิดได้ง่าย
  • Adapter ห่อเพื่อเปลี่ยน interface ส่วน Decorator ห่อโดยคง interface เดิมไว้แล้วเพิ่มพฤติกรรมเข้าไป
  • Composite ใช้แนวคิด recursive wrapping เหมือนกัน แต่รวมผลจากลูกหลายตัว ไม่ได้เสริมตัวเดียว
  • Proxy ก็ห่อ object ด้วย interface เดียวกันเช่นกัน แต่เพื่อ ควบคุมการเข้าถึง แทนที่จะเพิ่มฟีเจอร์
DecoratorProxySubclass
เพิ่ม behaviorในขณะ runtimeควบคุม accessในขณะ compile
จำนวนที่ซ้อนได้ได้หลายชั้นปกติชั้นเดียวต้องสร้าง class ใหม่ทุกชุด
object ต้นแบบยังคงอยู่ถูกซ่อนถูกขยาย
ตัวอย่างstream.pipe(), Express middlewareauth proxy, cache proxyArray extends Object
Decorator เพิ่มพฤติกรรมให้กับ object อย่างไร?
Decorator มีข้อได้เปรียบอะไรเหนือ inheritance ในที่นี้?
decorator ถือ reference ไปยังอะไร?
อะไรแยกแยะ Decorator ออกจาก Proxy ทั้งที่มีรูปแบบคล้ายกัน?