รู้จักกับ Design Pattern - Structural (Part 2/3)
/ 15 min read
สามารถดู video ของหัวข้อนี้ก่อนได้ ดู video
Design Pattern Structural คืออะไร ?
Design Pattern Structural เป็นหนึ่งในหมวดหมู่ของ Design Patterns ที่มุ่งเน้นไปที่วิธีการจัดการและประกอบความสัมพันธ์ระหว่าง Object และ Class โดยทำให้โปรแกรมมีความยืดหยุ่นและสามารถรองรับการเปลี่ยนแปลงได้ง่ายขึ้น ซึ่งประกอบด้วย 7 รูปแบบหลัก ได้แก่ Adapter, Bridge, Composite, Decorator, Facade, Flyweight, และ Proxy แต่ละแบบมีคุณสมบัติดังนี้
- Adapter - ปรับให้ interface ของ Class ที่ไม่เข้ากันสามารถทำงานร่วมกันได้ โดยไม่ต้องเปลี่ยนแปลง code ที่มีอยู่ของทั้งสองฝ่าย (Class ผู้สร้างและ instance ที่ใช้) เหมือนกับการใช้ adapter ในชีวิตจริงเพื่อให้ปลั๊กไฟจากประเทศหนึ่งเข้ากับปลั๊กของประเทศอื่นได้
- Bridge - แยก abstraction (interface) จาก implementation ของมันเอง เพื่อให้ทั้งสอง (ที่เคยอยู่ใน class เดียวกัน) สามารถเปลี่ยนแปลงได้อย่างอิสระต่อกันได้ (แทนที่จะต้องสร้าง class กับทุกๆประเภทออกมาแทน)
- Composite - การประกอบ Object ในรูปแบบ tree เพื่อทำให้สามารถสร้าง Object ทั้งแบบตัวเอง และ แบบภายในตัวของมันเอง (แบบกลุ่ม) ออกมาได้
- Decorator - เพิ่ม method ใหม่ให้กับ Object ตัวนั้นๆ โดยไม่เปลี่ยนแปลง Class และ bahavior ของ Object อื่นๆที่เรียกใช้ class นั้นอยู่ (เป็นการเพิ่ม function ให้กับ object ขณะเรียกใช้งาน)
- Facade - คือการสร้าง interface ที่คุยกับ complex system ของ class, library หรือ framework (subsystem ทั้งหลาย) เพื่อเป็นการรวมการพูดคุยมาไว้ภายใน interface ตัวเดียวออกมาแทน
- Flyweight - Pattern ที่ใช้สำหรับลดการใช้ memory ด้วยการ share data ระหว่าง object ที่ใช้งานเหมือนๆกันออกมา มักใช้กับเหตุการณ์ที่ต้องมีการสร้าง object จำนวนมากขึ้นมา (จุดประสงค์เพื่อเป็นการลดการ instance object จำนวนมากให้เกิดขึ้นใน memory ขึ้นมา)
- Proxy - Pattern ที่จะทำการสร้าง object เป็นตัวแทนของการพูดคุยกับ object อื่นแทนการ access ตรงๆ เพื่อให้สามารถควบคุมการจัดการกับ object ที่เป็นเป้าหมายการเรียกใช้ออกมาได้ (เช่น ควบคุมการ access, ควบคุมการสร้าง, การทำ cache หรือ lazy initialization เป็นต้น)
เราจะมาทำความรู้จักทีละ Pattern กันว่ามีลักษณะเป็นอย่างไรบ้างผ่าน Typescript เช่นเดิม (เช่นเดียวกับหัวข้อก่อนหน้านี้)
1. รูปแบบ Adapter
Adapter pattern (บางที่ก็จะใช้คำว่า Wrapper pattern) คือ design pattern ที่อนุญาตให้ object ที่ไม่สามารถใช้งานร่วมกันได้ สามารถใช้งานร่วมกันได้ ซึ่งเบื้องหลังของการใช้ Adapter pattern คือการสร้างตัวที่เป็นเหมือนตัวแปลงระหว่าง 2 ตัวที่ไม่เข้ากัน convert เข้ามาหากัน เพื่อให้สามารถใช้งานร่วมกันได้ตามที่ client ต้องการ
องค์ประกอบของ Adapter จะมีดังนี้
- Target interface ที่ client ต้องการจะใช้งาน
- Adaptee class ที่มี interface ของตัวที่เป็นอยู่ปัจจุบัน (เป็นตัวที่ต้องการจะแปลงไป) แต่ไม่สามารถใช้งานได้ใน client code version ปัจจุบันแล้ว
- Adapter class ที่จะทำการ implement ตาม Target interface ที่จะทำการห่อ Adaptee เพื่อทำการแปลง Adaptee ให้ใช้งานกับ Target ได้
เรามาลองดูตัวอย่างผ่านโจทย์นี้กัน
- สมมุติเรามีระบบ log อยู่ ซึ่งแต่เดิม log เรามีเพียงแค่ function เดียวคือ log ที่สามารถ log ได้ทุกอย่างในโลกใบนี้
- ต่อมา เราได้มี log service ตัวใหม่ ที่ทำการแยก log ระหว่าง info, error, debug ที่ระบบที่ implement หลังจากนี้จะทำการใช้งานตาม log service ตัวใหม่
- แต่ทีนี้เราอยากให้ระบบ log ใหม่สามารถใช้งานร่วมกับระบบ log เก่าได้ด้วยโดยการให้ระบบ log ใหม่นั้นยังคงใช้งานผ่านระบบ log เก่าได้แต่ให้ใช้ผ่านคำสั่งของ info log ออกมาแทน (เพื่อให้ระบบ log ยังคงสามารถทำงานได้เหมือนเดิม)
และนี่คือ code ตัวอย่างของเคสนี้
เมื่อเขียน Class Diagram ออกมาก็จะมีหน้าตาประมาณนี้
อธิบายจาก code และ diagram
- OldLogger Interface เป็น log interface ตัวเก่า
- NewLogger Interface เป็น log interface ตัวใหม่ที่เพิ่ม feature ใหม่เข้ามา
- LoggerAdapter เป็น Adapter ที่จะทำการนำ Log ตัวเก่า (
OldLogger
) มา implement (ตาม interface ของOldLogger
) เพื่อใช้งานกับ Log ตัวใหม่ (NewLogger
) โดยทำการ map log ตัวเก่าให้กลายเป็น info log ของ Log ตัวใหม่ออกมาแทน = เพียงเท่านี้OldLogger
ก็จะสามารถใช้งานได้เหมือนกับ info log ของNewLogger
ออกมาได้ - ตอนใช้งาน application ก็จะทำการสร้าง instance จาก
LoggerAdapter
โดยทำการใส่ log ตัวใหม่ เข้าไป และเมื่อผ่าน Adapter ออกมา instance ตัวใหม่ก็จะทำการ implement คำสั่งของ Log ตัวใหม่เข้าไป ทำให้สามารถใช้งาน log ตัวเก่าที่ทำการ run บน Log ตัวใหม่ออกมาได้ในที่สุด
Use case อื่นๆที่สามารถใช้งานได้
- ใช้ร่วมกับ External API Adapter สามารถใช้แปลง interface ภายนอกให้เป็น interface ที่เข้ากันได้กับ code base ที่มีอยู่ของ application
- การแปลงรูปแบบข้อมูล เมื่อจัดการกับรูปแบบข้อมูลที่แตกต่างกัน (เช่น XML, JSON, CSV) Adapter สามารถใช้แปลงข้อมูลที่ได้รับในรูปแบบหนึ่งเป็นอีกรูปแบบหนึ่งที่ application สามารถใช้งานได้
2. รูปแบบ Bridge
Bridge design pattern คือ pattern ที่ทำการแยกส่วน abstraction ออกมาจากส่วนของ implementation อีกทีเมื่อพบว่า ของลักษณะ 2 อย่างสามารถแยกส่วนให้ independent (ไม่ขึ้นต่อกัน) ได้ โดยปกติ pattern นี้จะเกี่ยวกับการแยกส่วนของพวก class ใหญ่ๆ หรือ set ของ class ที่ใกล้ๆกัน (แต่ใช้แยกกันได้) ทำการแยกส่วนกันออกมา เพื่อให้สามารถ implement ทั้ง 2 ส่วนแยกออกจากกันได้
องค์ประกอบของ Bridge จะประกอบด้วย
- Abstraction layer นี้จะเป็นส่วนที่เป็น core (ส่วนใหญ่จะเป็น interface หรือ abstract class) ของ component ที่ต้องการประกาศให้เป็น high level control logic เพื่อใช้สำหรับการแยกส่วน implementation ออกจากกัน
- Refined Abstraction เป็นส่วนขยายจาก Abstraction อีกทีโดยเพิ่ม behavior ลงไปใน Abstraction (โดยไม่ต้องแก้ไข interface เพิ่มเติมใน Implementor)
- Implementor เป็นส่วนของ interface ที่ทำการกำหนดว่า จะต้อง implement operation ออกมายังไง (โดยอ้างอิงถึง abstraction ที่เป็นตัวหลัก)
- Concrete Implementor เป็น class ที่จะ implement ต่อจาก implementor interface อีกที โดยจะเป็นการใส่ method ของการทำงานเข้าไป
เรามาลองดูผ่านโจทย์ตัวอย่างนี้กัน
อธิบายจาก code.
- MessageDisplay (Abstraction) คือส่วน abstract class ที่นำเสนอ high level ของส่วนที่แยกออกมา ซึ่งจะประกอบด้วย
MessageSource
ที่อนุญาตให้ใช้ message จากหลายๆ source เข้ามาได้ - MessageSource (Implementor) คือ interface ที่กำหนดว่าต้องมี
getMessage
สำหรับ message ตัวไหนก็ตามที่จะใช้งานMessageSource
นี้ - AlertMessageDisplay & ModalMessageDisplay (RefinedAbstractions) class ที่ขยายเพิ่มมาจาก
MessageDisplay
ที่ทำการ implement ไว้ว่า จากMessageSource
ของแค่ละ Class ทำหน้าที่แสดงผลแบบไหนออกมาบ้าง (แยกระหว่าง Alert กับ Modal) - StaticMessageSource & ApiMessageSource (Concrete Implementors) class ที่ทำการ implement ตาม
MessageSource
เพื่อทำการระบุ message ที่มาจาก Source ที่แตกต่างกันตามชื่อ Class ที่ระบุมาได้
อย่างที่เห็นใน Class diagram จะเห็นว่า จริงๆ Source กับ Display นั้นท้ายที่สุดก็จะถูกนำมาใช้งานร่วมกัน แต่ด้วย idea ของ Bridge จะทำให้เราแยกส่วนของการดึง Source และการแสดงผลออกจากกันได้ ทำให้ Source code ทั้ง 2 ส่วน independent กันแต่ก็มา depentdent กันตอนใช้งานได้นั่นเอง
Use case อื่นๆที่สามารถใช้งานได้
- แยกส่วน UI และ Business Logic โดยการใช้รูปแบบ Bridge เราสามารถแยก UI ออกจาก business logic ที่ทำงานอยู่เบื้องหลังได้ วิธีนี้จะช่วยให้สามารถเปลี่ยนแปลง UI หรือ business logic ได้โดยไม่กระทบกับส่วนอื่นๆ ได้
- รองรับระบบจัดการ Database หลายประเภท หาก web application จำเป็นต้องใช้ database หลากหลายประเภท (เช่น SQL, NoSQL, graph databases) สามารถใช้รูปแบบ Bridge เพื่อสรุปการทำงานของ database ให้แยกออกจากการทำงานเฉพาะของ database แต่ละประเภทได้
- ระบบการแจ้งเตือน (Notification) ในกรณีที่ web application มีส่งการแจ้งเตือนผ่านช่องทางต่างๆ (เช่น อีเมล, SMS, Push notifications) รูปแบบ Bridge จะสามารถแยก logic การส่งการแจ้งเตือนออกจากการทำงานของแต่ละช่องทางออกจากกันได้
3. รูปแบบ Composite
Compisite pattern คือ pattern ที่ทำการรวม object ไว้เป็นโครงสร้างแบบ tree structure เพื่อนำเสนอ object ในรูปแบบ heirarchies ออกมา โดย Pattern นี้จะอนุญาตให้ client สามารถเรียก object ทีละตัวแยกออกจากกัน และสามารถเรียกใข้ object นั้นแบบรวมหลายตัวพร้อมกัน ด้วย class ตัวเดียวกันออกมาได้นั่นเอง
องค์ประกอบของ Composite
- Components intetface ที่กำหนด opration ทั่วไปสำหรับ composite (object ที่ใช้รวม object ตัวอื่นๆ) และ leaf node (object ที่ใช้แยก) ใน tree structure ซึ่งโดยปกติ ก็จะประกอบไปด้วย เพิ่ม, ลบ และดึง child component ออกมา
- Leaf นำเสนอ leaf object ที่อยู่ใน composite โดย leaf object นั้นจะไม่มี children อยู่ แต่จะมีการกำหนด behavior ของ object เอาไว้ในนี้แทน (ซึ่งจะเป็น behavior เดียวกับที่ composite ทำงาน)
- Composite class ที่ทำการเก็บ child components เอาไว้ ซึ่งใน composite จะประกอบด้วย leaf และ composite อยู่ด้วยกัน โดย Composite จะ implement ตาม interface และกำหนด operation ให้กับ children เพื่อให้ children แต่ละตัวต่างไปทำ behavior ของตัวเองต่อได้
มาลองดูโจทย์ตัวอย่างกัน เราจะสร้างระบบสำหรับดึงข้อมูล file ออกมาซึ่งโดยปกติ file จะเก็บเป็น 2 แบบคือ ข้อมูล file และ directory (ที่เก็บ file ไว้) ซึ่งการที่ directory จะไปสู่ file นั้นได้ ก็จะต้องค่อยๆดำลึกลงไปเรื่อยๆจนถึงตัว file ออกมา (เปรียบได้กับการทำ composite ค่อยๆดำดึงลงไปใน Leaf)
และนี่คือ code ตัวอย่างของเคสนี้
ผลลัพธ์ออกมาประมาณนี้
อธิบายจาก code
- FileSystemCmoponent interface เป็นการกำหนด operation ชื่อ
showStructure
ตรงกลางเอาไว้เพื่อใช้สำหรับ file และ directories - File (Leaf) นำเสนอ file ที่อยู่ใน directory โดย implement จาก
FileSystemComponent
interface - Directory (Composite): นำเสนอ directory ที่ทำการประกอบด้วย files และ directories ตัวอื่นเอาไว้ด้วยกัน โดยทำการ implement method สำหรับการเพิ่ม component เข้าไปคือ
addComponent
(สำหรับเพิ่ม file หรือ directory) และ คำสั่งสำหรับ display structure ออกมาให้กับ children ทุกตัวผ่านคำสั่งshowStructure
ออกมา
Use case อื่นๆที่สามารถใช้งานได้
- CMS ใน CMS เนื้อหาสามารถจัดโครงสร้างตามลำดับชั้นโดยมีส่วนประกอบต่างๆ เช่น Page ส่วนต่างๆ (Sections) และบทความ (Blog) รูปแบบ Composite สามารถจัดการส่วนประกอบเหล่านี้ให้ใช้งานเป็นหนึ่งเดียวกันได้ ช่วยให้จัดการโครงสร้างเนื้อหาให้จัดการได้ง่ายขึ้น เช่น การเพิ่ม การลบ หรือการอัพเดตเนื้อหาใน category level ต่างๆได้
- Product catalog สำหรับ E Commerce Product catalog สามารถจัดโครงสร้างโดยใช้รูปแบบ Composite โดย Category และ Sub category จะมี product อยู่ภายในนั้น รูปแบบนี้จะช่วยในการใช้งาน use case ต่างๆภายใน E Commerce ได้ เช่น การแสดงผล การค้นหา และการ filter product ทั้ง catalog โดยเป็นใช้งานที่เหมือนกันกับ category และ product แบบเดียวกันได้ (เนื่องจากเป็น class ประเภท composite เดียวกัน)
- Feed Social Media Feed สามารถประกอบด้วยเนื้อหาประเภทต่างๆ (เช่น โพสต์, รูปภาพ, วิดีโอ) ที่อาจมีเนื้อหาอื่นๆที่สามารถนำมาวางซ้อนๆต่อกันได้ (เช่น Comment, Reply Comment) รูปแบบ Composite ช่วยให้สามารถจัดการต่อเนื้อหาทุกประเภทเหล่านี้ได้ง่ายสำหรับการขึ้น เช่น การแสดงผล การแก้ไข หรือการลบ ก็สามารถทำได้ภายในรูปแบบ Composite ตัวเดียวได้
4. รูปแบบ Decorator
รูปแบบการออกแบบ Decorator เป็นรูปแบบเชิงโครงสร้าง (structural pattern) ที่เปิดให้เราสามารถเพิ่มพฤติกรรม (behavior) เข้าไปยังวัตถุ (object) เดี่ยวๆ ได้ โดยไม่กระทบต่อพฤติกรรมของ object อื่นๆ ใน class เดียวกัน รูปแบบนี้เป็นประโยชน์อย่างมากในการยึดหลักการ Open/Closed ซึ่งเป็นหนึ่งในหลักการ SOLID ที่ระบุว่าคลาสหนึ่งๆ ควรเปิดสำหรับการเพิ่มเติมส่วนขยาย แต่ปิดสำหรับการแก้ไขดัดแปลงโดยตรง
องค์ประกอบของ Decorator
- Component กำหนด interface สำหรับ Object ที่เราจะสามารถเพิ่มความสามารถเข้าไปได้ภายหลัง (ตอนที่โปรแกรมรันอยู่)
- Concrete Component นำ interface ของ Component มา implement ตัวนี้เปรียบเสมือน Base Object ที่เราจะเพิ่มความสามารถให้ในภายหลังได้
- Decorator ส่วนนี้จะ reference ไปยัง Object ที่เป็น Component และมี interface ที่สอดคล้องกับ interface ของ Component ด้วย ตัว Decorator จะเป็นเหมือน Base ในการสร้างตัวตกแต่งที่เฉพาะเจาะจงต่อไป
- Concrete Decorators เป็นการใช้งาน Decorator จริงๆ ตรงนี้เราจะเขียน code ใส่ behavior ต่างๆ ที่อยากเพิ่มให้กับ Object xของเราเข้าไป ซึ่งอาจมี Concrete Decorator ได้หลายตัวเพื่อตกแต่งในรูปแบบที่แตกต่างกันออกไป
และนี่คือตัวอย่างของ Decorator สมมุติเรามี FetchService ที่สามารถดึงข้อมูลจาก API ออกมาได้ ทีนี้เราอาจจะมีบาง service ที่เมื่อใช้ FetchService เข้าไป เราอยากเพิ่ม Log เข้าไปด้วย (ไม่ได้เพิ่มเป็น base ให้กับ FetchService) ในเคสนี้เราจะใช้ Decorator มา on top เพื่อเพิ่ม Log service เข้าไป
ลักษณะ code ก็จะเป็นแบบนี้
ผลลัพธ์
อธิบายจาก code
- FetchService Interface กำหนด interface หลัก ที่ทั้งตัว service ที่ดึงข้อมูลจริงๆ และตัว Decorator (ตัวตกแต่ง) จะต้องนำไปใช้งานเพื่อให้โครงสร้างโดยรวมทำงานร่วมกันได้
- ConcreteFetchService ตัวนี้คือส่วนที่ทำหน้าที่หลักของการดึงข้อมูลจริงๆ จาก URL เปรียบเหมือน Object ตั้งต้นก่อนจะโดนตกแต่งความสามารถเพิ่มโดย Decorator
- FetchServiceDecorator เป็น base class สำหรับตัวตกแต่ง (Decorator) ทั้งหมด ตรงนี้จะมีการเก็บ reference ถึงตัว service และคอยมอบหมายงานต่อไปยังตัว service หลัก
- LoggingFetchServiceDecorator ตัวอย่างของ Decorator โดยจะทำการเพิ่มความสามารถในการบันทึกข้อมูลของระบบ (log) เข้าไปก่อนและหลังการดึงข้อมูล
- clientCode ตัวอย่างการใช้งานจริงว่าเราจะนำ service ที่ถูกตกแต่งแล้วนี้ไปใช้ยังไง
use case อื่นๆที่สามารถใช้ได้
- Data Validation Decorator สามารถนำมาใช้ห่อหุ้ม function การประมวลผลด้วยการเพิ่ม logic ตรวจสอบเพิ่มเติมได้ เพื่อให้แน่ใจว่าข้อมูลตรงตามเกณฑ์ที่กำหนดแล้วเรียบร้อย
- Caching สามารถนำมาใช้เพื่อเพิ่มกลไกการ cache (การจำข้อมูลชั่วคราวเพื่อให้เรียกใช้งานได้ครั้งต่อไปได้รวดเร็วขึ้น) สำหรับการดำเนินการที่มี cost สูง (ใช้กำลังการประมวลผลค่อนข้างมาก) หรือการดึงข้อมูลได้
5. รูปแบบ Facade
รูปแบบการออกแบบ Facade เป็นรูปแบบที่สร้าง interface แบบง่าย เพื่อให้สามารถใช้งานระบบย่อย (subsystems) อันซับซ้อนที่อาจประกอบด้วย class, library หรือ framework ต่างๆออกมาได้ รูปแบบนี้จะใช้ facade object เพื่อทำหน้าที่เป็นจุดเข้าถึงเพียงจุดเดียว ซึ่งจะคอยอำพรางความซับซ้อนของระบบย่อยเอาไว้ไม่ให้ฝั่งผู้ใช้งาน (client) เห็น ทำให้ผู้ใช้งานสามารถโต้ตอบกับระบบได้ผ่าน interface ที่ตรงไปตรงมา โดยไม่ต้องกังวลกับความซับซ้อนภายในได้
องค์ประกอบของ Facade ประกอบด้วย
- Facade class หลักที่ทำหน้าที่เป็น interface แบบง่ายสำหรับการเข้าถึงระบบย่อย Facade จะรับผิดชอบจัดการกับระบบย่อยทั้งหมดและเปิดเผย API ที่ใช้งานง่ายแก่ผู้ใช้งาน
- Subsystem ระบบย่อยหรือ Class ต่างๆ ที่มีความซับซ้อน Facade จะทำหน้าที่ซ่อนความซับซ้อนเหล่านี้จากผู้ใช้งานเอาไว้
- Client ผู้ใช้งานหรือ Class อื่นๆ ที่ต้องการใช้ระบบย่อย Client จะโต้ตอบกับระบบผ่าน Facade โดยไม่ต้องรู้รายละเอียดของระบบย่อย
และนี่คือตัวอย่างของการใช้ Facade สมมุติเราสร้าง API Service ขึ้นมาตัวหนึ่ง ซึ่งใน API Service นี้ประกอบด้วยของ 3 อย่างที่ต้องทำคือ การส่ง HTTP Request, การแปลง Response ข้อมูลส่งกลับมา และการทำ Error Handle ในกรณีที่ Request มีปัญหา เพื่อให้ฝั่งของ Client ไม่ต้องคอยมาเรียกใช้งานทีละส่วน Facade จะทำการรวมมาไว้ใน API Service และเป็นคนจัดการลำดับการเรียกให้แทน
code ก็จะมีลักษณะออกมาเป็นแบบนี้
จาก code ด้านบน
- ApiFacadeSimulator คือตัว Facade ใน แcode นี้ ทำหน้าที่สร้าง interface แบบง่ายในการทำ API call
- Subsystems ระบบย่อยที่ถูกซ่อนไว้ประกอบด้วย
HttpRequestSimulator
ทำหน้าที่จำลองการส่ง HTTP requestResponseParserSimulator
จำลองการแปลผล JSON responseErrorHandlerSimulator
จำลองการจัดการข้อผิดพลาด (error)
- clientCodeSimulator คือผู้ใช้งานหรือ Client ซึ่งจะโต้ตอบผ่าน Facade อย่างที่เห็นว่า ฝั่ง Client ไม่มีอะไรมากนอกจากการ instance
ApiFacadeSimulator
ขึ้นมาและเรียกใช้ ก็จะได้ความสามารถของทุก subsystem ออกมาได้จากการเรียกใช้งานผ่าน Facade ออกมาได้
use case อื่นๆที่สามารถใช้ได้
- Simplifying Third-party Service Integration web application ****มักจะต้องทำงานร่วมกับ service หรือ API จากภายนอกที่อาจมีความซับซ้อน (เช่น Payment Gateway, Social Media API) Facade สามารถทำหน้าที่เป็นเหมือนจุดเชื่อมต่อให้กับบริการเหล่านี้ และช่วยให้ส่วนอื่นๆ ของ application สามารถสื่อสารมายัง Facade เพียงตัวเดียวเพื่อใช้งานแทนได้
- Database Access Abstraction โดย Facade จะมีวิธีการที่รวมคำสั่งสำหรับการค้นหาและ update ข้อมูลภายใน database ให้คำสั่งนั้นเรียบง่ายยิ่งขึ้น โดยการช่วยซ่อนความซับซ้อนของคำสั่ง SQL หรือการจัดการ database อื่นๆเอาไว้ เพื่อให้ใช้งานได้ง่ายมากขึ้น
6. รูปแบบ Flyweight
รูปแบบการออกแบบ Flyweight เป็นรูปแบบที่ใช้เพื่อลดการใช้หน่วยความจำหรือลดต้นทุนการคำนวณโดยการแบ่งปันข้อมูลให้มากที่สุดเท่าที่จะทำได้กับ Object ที่เกี่ยวข้อง รูปแบบนี้มีประโยชน์อย่างมากเมื่อมี Object จำนวนมากที่ต้องใช้ state ร่วมกัน โดยหลักการแล้วรูปแบบ Flyweight มีเป้าหมายเพื่อนำ instance ของวัตถุกลับมาใช้ใหม่เพื่อลดความต้องการทรัพยากรหน่วยความจำด้วยการ share ร่วมกัน โดยแยกแยะระหว่างสถานะภายในของวัตถุ (intrinsic state) ซึ่งจะเหมือนกันในทุก instance ออกจากสถานะภายนอก (extrinsic state) ซึ่งจะแตกต่างกันได้ระหว่าง object
อธิบายเพิ่มเติม
- Intrinsic state คือ ข้อมูลหรือคุณสมบัติที่เป็นคุณสมบัติหลักของ Object (unique) ไม่เปลี่ยนแปลงตามบริบทการใช้งาน ตัวอย่างเช่น รหัสอักขระของตัวอักษร (a, ก), ขนาดของรูปภาพ, เพศของบุคคล
- Extrinsic State คือ ข้อมูลหรือคุณสมบัติที่เปลี่ยนแปลงตามบริบทการใช้งาน ตัวอย่างเช่น ตำแหน่งของตัวอักษรในข้อความ, สีพื้นหลังของรูปภาพ, อารมณ์ของบุคคล
องค์ประกอบของ Flyweight ประกอบด้วย
- Flyweight interface กลางที่ Flyweight ตัวต่างๆ จะนำไปใช้งานเพื่อรองรับการรับและจัดการกับ extrinsic state (สถานะภายนอก)
- Concrete Flyweight คือการเขียน code เพื่อใช้งานตาม interface ของ Flyweight ตรงนี้จะมีการเก็บ intrinsic state (สถานะภายใน) ไว้ โดย intrinsic state จะเป็นข้อมูลที่ไม่ขึ้นอยู่กับบริบทของการใช้งาน และสามารถแชร์กันได้ (ตัวอย่างเช่น รหัสอักขระของตัวอักษรแต่ละตัว)
- Flyweight Factory ส่วนนี้ทำหน้าที่จัดการ object ประเภท Flyweight และมั่นใจว่าจะเกิดการแชร์ใช้ object หรือ instance ร่วมกันอย่างเหมาะสม เมื่อ Client request การใช้งาน Flyweight Factory นี้จะดูว่ามีอยู่แล้วหรือไม่ ถ้ายังไม่มีก็จะสร้างใหม่ออกไปและจัดเก็บไว้ใน Factory อย่างถูกต้องได้
- Client Code ส่วนที่เรียกใช้ Flyweight Factory เพื่อเข้าถึง Object ประเภท Flyweight โดยมีหน้าที่รับผิดชอบเรื่องการจัดการ extrinsic state (สถานะภายนอก) ที่เกี่ยวข้องกับ Flyweight แต่ละตัว (ซึ่งมีโอกาสแตกต่างกันได้ และแชร์กันไม่ได้)
มาลองดูตัวอย่างของ Flyweight กัน หนึ่งในตัวอย่าง classic ของการใช้ Flyweight คือ Text Editor ลองนึกถึงการใช้งาน Text Editor ที่เราต้องพิมพ์ ตัวอักษร แต่ละตัวเข้าไปใน Editor โดยตัวอักษรแต่ละตัวก็จะมีขนาด (fontSize), ความหนาตัวอักษร (fontWeight), สี (color) ของตัวเอง และ Editor 1 ตัวเองก็ต้องเก็บตัวอักษรจำนวนไม่น้อยเช่นกัน (คิดง่ายๆว่าเราอาจจะมีช่องที่สามารถกรอกตัวอักษรได้ราวๆ 3000 ตัวอักษร) การใช้ Flyweight ก็จะช่วยทำให้เราไม่ต้อง instance object ที่มี style ซ้ำๆออกมาได้ แต่เป็นการสร้างตัวสำหรับ (Factory) จัดการ ตัวอักษรและ style ตัวอักษรให้ใช้งานร่วมกันได้ออกมาแทน
code ก็จะมีลักษณะประมาณนี้ออกมา
อธิบาย code ด้านบน
- TextStyleFlyweight Interface & ConcreteTextStyle ส่วนนี้จะกำหนดว่ารูปแบบตัวอักษร (text style) จะถูกนำไปใช้งานอย่างไร โดย
ConcreteTextStyle
จะเก็บค่าต่างๆ ที่เป็น intrinsic state (เช่น ขนาดตัวอักษร, ความหนา, สี) ที่สามารถแชร์ร่วมกันได้ - TextStyleFactory ส่วนนี้จะทำให้มั่นใจได้ว่า รูปแบบตัวอักษรที่มีค่าเหมือนกันทุกอย่าง จะมีเพียงแค่หนึ่ง instance ของ
ConcreteTextStyle
ที่ถูกสร้างขึ้นและใช้แชร์ร่วมกัน - การใช้งานด้านล่างนั้น เป็นการแสดงตัวอย่างการใช้งานจริง เช่น การสร้างองค์ประกอบต่างๆ (elements) ในหน้าจอ แล้วนำรูปแบบตัวอักษร (ซึ่งอ้างอิงไปยัง
TextStyleFlyweight objects
ที่อาจถูกแชร์กัน) ไปใช้งานร่วมกัน ทำให้ประหยัดหน่วยความจำ เนื่องจากใช้รูปแบบตัวอักษรซ้ำได้โดยไม่ต้องสร้างหลายๆ instance สำหรับรูปแบบที่ซ้ำกัน
use case อื่นๆที่สามารถใช้ได้
- Shared User Data web application ที่มีผู้ใช้งานจำนวนมาก แต่ละคนก็มีข้อมูลบางส่วนที่เหมือนกันอยู่ (เช่น roles, permission หรือ perferences) Flyweight pattern จะช่วยเก็บข้อมูลที่ใช้ร่วมกันของ user ไว้ต่างหาก แล้วให้ข้อมูลผู้ใช้แต่ละคนชี้มาที่ข้อมูลตรงนั้นแทน ก็จะช่วยลดจำนวนการใช้ memory ที่ใช้เกินความจำเป็นไปได้
- Caching Reusable Images or Icons โดยทั่วไป web application มักจะมีรูปหรือ icon เหมือนๆกันใช้อยู่หลายๆที่ Flyweight pattern จะช่วยจัดการ Resourece โดยการโหลดแต่ละภาพหรือ icon ที่ไม่ซ้ำกันแค่ครั้งเดียว แล้วนำกลับมาใช้ใหม่ทุกครั้งที่เจอ ซึ่งจะช่วยลดการใช้หน่วยความจำและทำให้เว็บโหลดเร็วขึ้นได้
7. รูปแบบ Proxy
รูปแบบการออกแบบเชิงโครงสร้าง Proxy จะสร้าง Object ตัวแทนขึ้นมาทำหน้าที่ควบคุมการเข้าถึง Object จริง โดย Proxy สามารถดักจับการเข้าถึง Object จริงไว้ทั้งหมดแทน เพื่อเป็นตัวดำเนินการเพิ่มเติมอื่นๆแทน ไม่ว่าจะเป็นก่อนหรือหลังจากที่ส่งต่อการทำงานไปยัง Object จริง ตัวอย่างการใช้งาน ได้แก่ การควบคุมการเข้าถึง การเก็บข้อมูลไว้ใช้ซ้ำ (caching) การเริ่มใช้งาน Object จริงเมื่อจำเป็นเท่านั้น (lazy initialization) การบันทึกข้อมูล (logging) และการตรวจสอบ (monitoring) เป็นต้น
องค์ประกอบของ Proxy ประกอบด้วย
- Subject interface ที่กำหนดชุดคำสั่งการทำงานพื้นฐานที่ทั้ง
RealSubject
และProxy
ต้องมีใช้งานร่วมกัน เพื่อให้ฝั่ง Client (ผู้ใช้งาน) สามารถใช้ทั้ง Object จริงและ Object Proxy ออกมาได้ - RealSubject นี่คือ Object จริง ที่มีชุดคำสั่งหลักของระบบที่เราต้องการให้มีการควบคุมการเข้าถึง
- Proxy จะมีการเก็บ reference ไปยัง
RealSubject
ส่วนProxy
นี้มีหน้าที่ควบคุมการเข้าถึงตัวRealSubject
ได้หลายรูปแบบ ไม่ว่าจะเป็นส่งต่อคำสั่งให้RealSubject
ทำงาน ไปจนถึงหน่วงเวลาการสร้างRealSubject
(ในกรณี lazy initialization) หรือห้ามเข้าถึงเลยก็ทำได้ นอกจากนี้Proxy
สามารถทำงานอื่นๆ เพิ่มเติมได้ทั้งก่อน และหลัง การติดต่อRealSubject
ด้วยเช่นกัน
ลองมาดูผ่านตัวอย่างนี้กัน สมมุติเราสร้าง feature สำหรับ premium access ขึ้นมา โดยคนที่จะสามารถเข้าใช้งานต้องเป็นที่สมัครสมาชิก (subscription) ไว้เท่านั้น จึงจะสามารถใช้งานได้ ในเคสนี้เราจะใช้ Proxy มากั้นการตรวจสอบไว้ก่อนว่า ข้อมูล user คนนั้น subscription แล้วหรือไม่ ก่อนจะเข้าถึงการใช้งานที่ object จริงออกมาได้
หน้าตา code ก็จะออกมาประมาณนี้
จาก code ด้านบน
- IFeatureAccess Interface สร้าง common interface ที่ทั้ง service ตัวจริงและตัว Proxy จะนำไปใช้งานเพื่อทำให้สามารถใช้แทนกันได้
- FeatureAccessService นี่คือส่วน core หลักที่ทำงานของการให้เข้าถึง feature ต่างๆ (มันคือ service หลักที่ทำงานจริงๆนั่นแหละ)
- SubscriptionProxy เป็น Proxy ที่ทำหน้าที่ควบคุมการเข้าถึงตัว
FeatureAccessService
โดยจะต้องดูด้วยว่าผู้ใช้มีการสมัครสมาชิก (subscription) ไว้หรือไม่ เพื่ออนุญาตหรือไม่อนุญาตให้เข้าถึง feature นี้ - clientCode ตัวอย่างการใช้
SubscriptionProxy
เพื่อควบคุมการเข้าถึง feature นี้ขึ้นอยู่กับว่าผู้ใช้ได้ทำการสมัครใช้งานหรือไม่
use case อื่นๆที่สามารถใช้ได้
- Lazy Initialization (สร้างเมื่อจำเป็น) Proxy สามารถจัดการกับการสร้าง object ที่มีขนาดใหญ่ ที่ใช้เวลาหรือ Resource ในการสร้าง โดย Proxy จะชะลอการสร้างออกไปจนกว่าจะมีการใช้งานจริง ช่วยให้ application เปิดได้เร็วขึ้น และประสิทธิภาพโดยรวมดีขึ้นด้วย
- Security Proxy สามารถเพิ่มระดับการป้องกันให้กับ object ที่มีข้อมูลสำคัญ โดยจะทำการเพิ่มการตรวจสอบเพิ่มเติมเข้ามาที่ Proxy ได้ เช่น การตรวจสอบความถูกต้องของข้อมูลป้อนเข้า การป้องกัน SQL injection หรือการ clean ข้อมูลก่อนที่จะส่งไปยัง object จริงหรือส่งเข้า database
สรุป
อย่างที่เห็น Design Pattern แบบ Structural นั้นจะเน้นไปที่โครงสร้างของการใช้งานแทน โดยเป็นการเพิ่มความสามารถของ code ให้สามารถออกแบบ code ที่ยืดหยุ่นและปรับการใช้งานได้ง่ายขึ้น จากการแยกส่วนประกอบต่างๆของ code ออกจากกัน (ในรูปแบบต่างๆอย่างที่เราเรียนรู้กันไป) เพื่อทำให้สามารถเปลี่ยนแปลงหรือเพิ่มเติม feature ใหม่เข้าไปได้ง่ายขึ้น โดยไม่จำเป็นต้องแก้ไข code ทั้งหมดได้
รวมถึงบาง Design pattern ใน Structural เช่น Flyweight ก็มีความสามารถในการช่วยเพิ่มประสิทธิภาพในการใช้หน่วยความจำ ทำให้สามารถใช้งาน instance ตามความจำเป็นของการใช้งานออกมาได้ หรือ อย่าง Proxy pattern ที่สามารถเพิ่มความปลอดภัยในการใช้งานของ service หลักออกมาได้
และนี่ก็คือ Design Pattern แบบ Structural หวังว่าจากตัวอย่างที่เล่ามาจะช่วยทำให้เห็นภาพการใช้งาน Design pattern ประเภทนี้มากขึ้นนะครับ 😁
Reference
https://refactoring.guru/design-patterns/structural-patterns
- ลองเล่น Supabase กับ Next.js กันมี Video มี Github
รู้จักกับ Supabase เทคโนโลยีฐานข้อมูลที่เรียกตัวเองว่าเป็น Firebase alternative กันว่าใช้ทำอะไรได้บ้าง
- รู้จักกับ Storybook และการทำ Component Specsมี Video
มาลองทำ Component Specs และ Interactive Test ผ่าน Storybook กัน
- มาลองเล่น Gemini Pro กันมี Video มี Github
มาทำความรู้จักกับ Gemini Pro และ Prompt design กันว่าเราสามารถเอา Gemini ไปทำอะไรได้บ้าง
- รู้จักกับ Drizzle ORM ผ่าน Next.jsมี Video
มาทำความรู้จัก Drizzle ORM กัน ว่ามันคืออะไร และทำไมถึงเป็นที่นิยมในวงการนักพัฒนา และลองเล่นกับ Next.js ด้วยกัน