All pages
Powered by GitBook
1 of 6

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Facade

Facade

Provide a unified interface to a set of interfaces in a subsystem. Façade defines a higher-level interface that makes the subsystem easier to use.

🎯 เป้าหมายของ pattern นี้

ทำให้ของที่ใช้งานยากๆซับซ้อนๆ สามารถใช้งานได้แบบง่ายๆ

✌ หลักการแบบสั้นๆ

  1. สร้าง class ที่ทำงานกับ subsystem ที่วุ่นวายๆ แล้วจัดการเรื่องที่ client ต้องเรียกใช้ทั้งหมด

  2. สร้างช่องทางให้ client เรียกใช้งานแบบง่ายๆ

😢 ปัญหา

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

แค่ได้ยินก็ปวดหัวละ แต่การที่จะให้เณรน้อยไปออกแบบระบบไฟฟ้านิวเคลียนี่ปวดตับกว่า แต่งานก็คืองาน ดังนั้น อิคิวกับเราจะออกแบบโค้ทของเรายังไงกันดี เพื่อให้มันสามารถทำงานกับ library นั้นได้ โดยที่โค้ดของเราจะต้องไม่ไปผูกติด (coupling) อยู่กับ class ต่างๆของ library ที่ว่ามานี้ และเราก็ไม่อยากให้โค้ดเราซับซ้อบแบบระบบพวกนั้นด้วยเช่นกัน

😄 วิธีแก้ไข

นั่งสมาธิรอจนกว่าจะได้ยินเสียงปิ๊ง! มาม่าก็จะสุกพอดี แล้วอิคิวซังก็จะบอกกับเราว่า ใช้ Facade ดิ!! (ผมเริ่มสงสัยเบื้องหลังของวัดนี้แล้วละ)

ซึ่งอิคิวซังก็ได้อธิบายต่อว่า Facade คือ class ธรรมดา 1 ตัวนี้แหละ แต่มันจะช่วยไปจัดการเรื่องยากๆทั้งหมดให้เรา แล้วเมื่อไหร่ที่เราจะเรียกใช้งานเรื่องยากๆพวกนั้น มันก็จะมีช่องทางง่ายๆให้เราเรียกใช้

หลังจากที่อิคิวเห็นเราทำหน้า งงๆ เขาก็พล่ามต่อโดยไม่สนใจเราว่า การที่มี Facade มันจะช่วยให้เราทำงานง่ายขึ้น เมื่อเราต้องไปทำงานกับ library ที่ค่อนข้างซับซ้อน มี feature เยอะๆ แต่เราใช้งานแค่ไม่กี่ feature ของมันเท่านั้น

📌 โครงสร้างของ pattern นี้

อธิบาย Facade - ตัวที่ช่วยลดความซับซ้อนให้ client โดยเปิดค่อยช่องทางให้ client เรียกใช้งานง่ายๆเอาไว้ ส่วนการทำงานที่มันต้องไปเรียกอะไรต่อจะถูกจัดการไว้ที่นี่ Additional Facade - Facade อื่นๆที่คอยทำงานให้กับ subsystem อื่นๆ มีไว้เพื่อให้ Facade อื่นเรียกใช้ เพราะบางทีการเขียนทุกอย่างไว้ที่ facade ตัวเดียวอาจจะก่อให้เกิดปัญหาได้ Complex Subsystem - (ก้อนที่มันเขียนว่า Subsystem หลายๆอัน) คือกลุ่มของระบบที่มีอะไรเยอะแยะวุ่นวายๆอยู่ด้วยกัน เพื่อทำงานบางอย่าง ซึ่งเจ้าก้อนพวกนี้แหละที่ Facade จะมาทำงานด้วย ซึ่งเจ้าพวก subsystem ทั้งหมดนั้นจะไม่รู้ถึงการมีอยู่ของ facade เลย

🛠 ตัวอย่างการนำไปใช้งาน

สมมุติว่าเราต้องการอัพโหลดไฟล์ VDO ขึ้นไปบน PornHub (วีดีโอไรหว่า) ถ้าเรามานั่งคิดดูถึงขั้นตอนการอัพโหลดไฟล์ vdo เราก็จะพบว่ามันมีขันตอนค่อนข้างจุกจิกอยู่นะเช่น ปรับขนาดไฟล์ให้เหมาะสม, เข้ารหัสให้ถูกฟอร์แมตที่เว็บนั้นๆจะใช้, จัดการคุณภาพเสียง บลาๆ แล้วก็อัพโหลดขึ้นเซิฟเวอร์ (อิคิวซังแอบกระซิบมาว่า ระบบสมัยใหม่ขั้นตอนวุ่นวายๆพวกนี้จะอยู่ที่เซิฟเวอร์หมดแล้วนะครับ)

จากที่ว่ามาทั้งหมดนั่น ถามว่าจริงๆ client ต้องไปยุ่งกับเรื่องพวกนั้นไหม? ไม่ใช่เขาแค่ต้องการจะอัพโหลดไฟล์เหรอ?

ดังนั้นการที่เราเอา Facade เข้ามาช่วย เมื่อ client ต้องการจะอัพโหลดไฟล์ เขาก็แค่เรียกใช้ method ที่ชื่อว่า Upload ของ Facade ก็พอ สวนที่เหลือมันต้องเอา vdo ของเราไปทำอะไรต่อ ก็จะเป็นหน้าที่ของ facade ที่ไปจัดการต่อให้นั้นเอง นี้แหละคือประโยชน์ของ Facadeทำเรื่องยากๆให้เป็นเรื่องง่าย ปะไปดูโค้ดตัวอย่างกัน

👍 ข้อดี

  • โค้ดของเราไม่ไปผูกติดกับ subsystem ที่วุ่นวาย

👎 ข้อเสีย

  • เจ้าตัว Facade จะกลายเป็น God object และตัวมันจะผูกติดกับ subsystem ที่มันทำงานอยู่

God object - คือของที่มันทำงานกับทุกสิ่งทุกอย่าง รู้ทุกเรื่อง เจือกได้ทุกอย่าง คิดง่ายๆคือคนที่เขียนโปรแกรมใหม่ๆยังแยก Module ไม่เป็น จะยัดทุกอย่างไว้ภายใน class เดียว และให้มันทำงานได้ครอบจักรวาล สิ่งนั้นแหละคือ God object

‍‍📝 Code ตัวอย่าง

Output

using System;
using System.IO;

// Subsystem classes
class VideoCompressionCodec
{
    public Stream CompressFile(Stream file) {
        // Compress file
        return file;
    }
}
class AudioMixer
{
    public bool CanEnhancement(Stream file) {
        // Check the video
        return true;
    }
    public Stream EnhanceAudio(Stream file) {
        // Compress file
        return file;
    }
}
class FileUploader
{
    public string UploadVideo(Stream file) {
        // Upload it to the server
        return "UC6fBbrBB4dF7WznwLmfbOnQ";
    }

    public string GetVideoUrlById(string id) {
        // Search the video from its id
        return $"https://www.youtube.com/channel/{id}";
    }
}

// Facade
class VideoFacade
{
    public string UploadVideoAndGetUrl(Stream file)
    {
        var compressor = new VideoCompressionCodec();
        var compressedSteam = compressor.CompressFile(file);
        var audio = new AudioMixer();
        var canEnhance = audio.CanEnhancement(compressedSteam);
        if(canEnhance)
        {
            compressedSteam = audio.EnhanceAudio(compressedSteam);
        }
        var uploader = new FileUploader();
        var videoId = uploader.UploadVideo(compressedSteam);
        var url = uploader.GetVideoUrlById(videoId);
        return url;
    }
}

// Client
class Program
{
    static void Main(string[] args)
    {
        // Get file from somewhere
        Stream src = null;
        var facade = new VideoFacade();
        var url = facade.UploadVideoAndGetUrl(src);
        Console.WriteLine(url);
    }
}
https://www.youtube.com/channel/UC6fBbrBB4dF7WznwLmfbOnQ

Proxy

Proxy

Provide a surrogate or placeholder for another object to control access to it.

🎯 เป้าหมายของ pattern นี้

สร้างตัวแทนของ object ที่เราจะเรียกใช้งานมัน ด้วย object อีกตัว เพื่อควบคุมการเข้าใช้งานของ object ตัวจริง

✌ หลักการแบบสั้นๆ

  1. สร้าง interface สำหรับ service

  2. สร้าง concrete service

  3. สร้าง proxy ที่ implement interface นั้น ส่วนการทำงานจะส่งต่อให้กับ concrete service ทำงานต่อ

  4. เมื่อ client ต้องการใช้ service ให้ส่ง proxy object ไปให้ใช้งานแทน

😢 ปัญหา

ตอนนี้เราเข้ามาอยู่ในวัดเพื่อปรึกษากับหล่วงพ่อ เพราะ library ของเรามีจุดที่ใช้งาน database อยู่เยอะมาก และแต่ละจุดที่เข้าไปต่อ database บางทีมันก็ไม่จำเป็นต้องไปต่อ database จริงๆ แค่เอาข้อมูลที่จำลองไว้มาใช้งานแทนก็ได้ และในหลายๆจุดก็ต้องการให้มันงานอะไรบางอย่าง ก่อน/หลัง query อีกด้วย

หลวงพ่อได้ฟังดังนั้นจึงทรงตรัสว่า โยมรู้หรือไม่ว่าการต่อ database โดยไม่จำเป็นมันเปลืองทรัพยากร ควรทำเป็น Lazy Initialization ไว้ด้วยนะจ๊ะ

Lazy Initialization คือการสร้าง object เมื่อมันจำเป็นที่จะต้องใช้ ไม่สร้างมั่วซั่วทิ้งไว้เรี่ยราด ส่วนใหญ่จะใช้กับ อะไรก็ตามที่มันเปลืองทรัพยากร (หลวงพ่อท่านฝากบอกมา)

และท่านยังชี้แนะปัญหาที่จะตามมาในอนาคตอีกด้วยว่า ถ้าโยมเขียนข้อมูลจำลองไว้ใช้เอง หรือ มีโค้ทให้มันทำงานอะไรบางอย่าง ก่อน/หลัง query เอาไว้ใช้เองนั้น มันอาจจะใช้งานได้กับโปรแกรมของโยมเท่านั้น

โยมลองคิดดูนะถ้ามี dev คนอื่นเอา library ของโยมไปใช้ละ โยมจะไปควบคุมเขายังไง เช่นถ้าจะเรียกใช้งานตาราง A จะต้องไปทำงานเรื่องนี้ก่อน แต่ถ้าใช้งานตาราง A ในกรณีนี้ไม่ต้องไปทำงานเรื่องนั้นก็ได้ หรือเมื่อไหร่จะส่งข้อมูลจำลองให้เขา เมื่อไหร่จะส่งข้อมูลตัวจริงให้เขากันละ

หลังจากหลวงพ่อตรัสจบ เณรน้อยอิคิวก็พูดขึ้นมาว่า แล้วโยมพี่จะทำยังไงดีฮะ?

😄 วิธีแก้ไข

หลวงพ่อมองหน้าเราอยู่ซักพัก เมื่อเห็นว่าเราทำหน้าเหมือน "ถ้ากรู๊วรู้ แล้วกรู๊วจะมานี่ทำมายฟระ" ท่านเลยชิ่งตรัสออกมาว่า โยมก็ใช้ Proxy pattern ดิ!! (ผมว่าวัดนี้ไม่ได้สอนแค่ปฏิบัติธรรมแล้วละ)

ก่อนที่เราจะได้เอ่ยปากถามต่อ ท่านก็โซโล่ต่อไปว่า จำตอนที่โยมเรียนมาได้ไหม ว่าจะจัดการยังไงถ้าเซิฟเวอร์รับโหลดเยอะๆ? ... Multitier architecture ไงโยม

(โพ่ง ... เสียง ชูเน็นซังทำบาตรหล่น ไม่ใช่เสียงอุทานแต่อย่างใด ดังขึ้น)

หลังจากหล่วงพ่อตั้งสติได้ ก็พล่ามต่อไปว่า

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

หลังจากนิ่งเงียบอยู่นาน ก็มีเสียงปิ๊ง!! ดังนั้น อิคิวซังเลยเดินไปเปิดไมโครเวฟเอามาม่ามากิน ก่อนจะพูดขึ้นทั้งที่มาม่าเต็มปากว่า

จากที่หลวงพ่อใบ้มา ผมคิดว่าโยมพี่น่าจะสร้าง class ที่มี interface เหมือนกับ class ที่ใช้ต่อ database ตัวจริงไว้นะ ส่วนถ้าใครจะต่อ database ก็ให้ทำผ่าน class ใหม่นี้ แทนที่จะทำงานกับตัวที่ต่อ database ตัวจริงยังไงละ เมื่อเป็นแบบนี้ถ้าโยมพี่อยากจะให้มันแก้ไข query ก่อน หรือแก้ไขข้อมูลก่อนส่งกลับไปให้ client ก็สามารถทำได้แล้วไง

พูดจบอิคิวก็วาดรูปลงใน iPad ให้ดู

จากรูปนะ class ใหม่นั่นก็คือ Proxy ซึ่งมันจะปลอมตัวให้คนอื่นเข้าใจว่า มันนั่นแหละที่เป็นตัวต่อ database ดังนั้น client ที่ต้องการต่อ database ก็จะทำงานกับ proxy ไป แล้วเจ้า proxy ก็จะไปทำงานกับ class ที่ต่อ database ตัวจริงอีกที

ปุจฉา: "แล้วข้อดีในการที่มีตัวมาคั่นกลางคืออะไร" เราชิ่งถามก่อนที่จะจบลงโดยที่เราไม่มีบทบาทอะไร

วิสัชนา: ข้อดีคือ เมื่อเจ้า proxy มันได้รับคำสั่งจาก client แล้ว มันจะไปทำอะไรอย่างอื่นก่อนแล้วค่อยไปทำงานกับ database ต่ออีกทีก็ได้ หรือได้รับข้อมูลจาก database แล้วอาจจะปรับแก้ข้อมูล หรือไปทำงานอย่างอื่นก่อนค่อยส่งข้อมูลคืนให้ client ก็ได้ หรือแม้แต่การทำ Lazy Initialization ก็ยังได้ด้วยยังไงละฮะ

และแล้วมาม่าก็อืด

📌 โครงสร้างของ pattern นี้

อธิบาย 1.Service Interface - Interface สำหรับให้ client เรียกใช้บริการของเรา และเพื่อให้ Proxy เข้ามาสวมรอยเจ้าเซอร์วิสที่แท้จริงของเราได้ 2.Service - เซอร์วิสที่แท้จริงของเรา (ต้อง implement Service Interface) 3.Proxy - ตัวที่จะมาสวมรอยเป็นเซอร์วิสนั้นๆ สามารถ execute ก่อน/หลัง ทำงานกับเซอร์วิสได้ (ต้อง implement Service Interface) 4.Client - คนที่มาใช้งานเซอร์วิส จะรู้แค่ว่ามันทำงานกับ Service Interface ซึ่งจริงๆถูก Proxy สวมรอยมาทำงานแทน

🛠 ตัวอย่างการนำไปใช้งาน

สมมุติว่าเราจะทำ Service ที่ใช้ในการดึงรูปจาก hardisk ซึ่ง การเข้าไปดึงข้อมูลจาก hardisk มันจะเปลืองทรัพยากร เราเลยจะใช้ Lazy Initialization เข้ามาช่วย ไปดูตัวอย่าง code กันเลย

👍 ข้อดี

  • สามารถจัดการกับ service object ได้ โดยที่ client ไม่รู้ตัว

  • จัดการกับ life cycle ของ service ตัวจริงได้ เมื่อ client ไม่ต้องการใช้แล้ว

  • Proxy พร้อมทำงานได้เลย แม้ว่าตัว service จะยังไม่พร้อมทำงาน

  • เป็น Open/Closed Principle

👎 ข้อเสีย

  • อาจจะช้าเพราะมันคุยกัน 2 ต่อ

  • เพิ่มความซับซ้อนให้กับโค้ด เพราะต้องไปสร้าง class และ interface มากมาย

‍‍📝 Code ตัวอย่าง

Output

using System;

// Service Interface
interface IImageService
{
    void ShowImage();
}
// Service
class ImageService : IImageService
{
    private string fileName;

    public ImageService(string filePath)
        => loadImageFromDisk(filePath);

    public void ShowImage()
        => Console.WriteLine(fileName);

    private void loadImageFromDisk(string filePath)
        => this.fileName = "Batman";
}
// Proxy
class ImageProxy : IImageService
{
    private ImageService svc;
    private string filePath;

    public ImageProxy(string filePath)
        => this.filePath = filePath;

    public void ShowImage()
    {
        // สั่งให้ทำงานบางอย่างก่อน (pre execution)
        log($"มีการอ่านไฟล์รูปเมื่อเวลา: {DateTime.Now}");

        // Lazy Initialization
        if(svc == null) 
        {
            svc = new ImageService(filePath);
        }
        svc.ShowImage();

        // สั่งให้ทำงานบางอย่างต่อ (post execution)
        Console.WriteLine("เพิ่มตัวเลขในฐานข้อมูล ว่ารูปนี้โดนเปิดดูเพิ่มขึ้นอีกครั้งแล้ว");
    }

    private void log(string msg)
        => Console.WriteLine(msg);
}

class Program
{
    static void Main(string[] args)
    {
        IImageService svc = new ImageProxy("c:/porn/japan.png");
        svc.ShowImage();
    }
}
มีการอ่านไฟล์รูปเมื่อเวลา: 2/1/19 7:32:10 PM
Batman
เพิ่มตัวเลขในฐานข้อมูล ว่ารูปนี้โดนเปิดดูเพิ่มขึ้นอีกครั้งแล้ว

Bridge

Bridge

Decouple an abstraction from its implementation so that the two can vary independently.

🎯 เป้าหมายของ pattern นี้

แยกรูปแบบการทำงาน (abstraction) ออกจากการทำงานจริงๆของมัน เพื่อให้สามารถจัดการแยกกันได้

Note abstraction ในที่นี้ไม่ใช่ abstract class ของโปรแกรมมิ่งเรานะ ในที่นี้มันจะเป็นคล้ายๆรูปแบบการทำงานมากกว่า เช่น หน้าตา (GUI) ของโปรแกรมเครื่องคิดเลข abstraction ของมันคือมีปุ่มกดที่เป็นตัวเลข กับปุ่มบวกลบไรพวกนี้ ส่วนเมื่อกดแล้วมันจะต้องไปทำงานอะไร เป็นเรื่องของการทำงานจริงๆของมัน

✌ หลักการแบบสั้นๆ

  1. เปลี่ยน inheritance เป็น composition

😢 ปัญหา

สมมุติว่าเรามี class รูปทรง ที่มี subclass อยู่ 2 แบบคือ วงลม กับ สี่เหลี่ยม แล้วเราอยากให้มันทำงานร่วมกับเรื่องสี ได้ ดังนั้นเราก็เลยสร้าง subclass เพื่อมาทำงานกับ สีแดง และ สีน้ำเงิน

จากในตอนแรกที่มีแค่ 2 subclass พอเราทำเรื่องสีเข้ามารวมด้วย มันเลยทำให้เราต้องแก้ subclass กลายเป็น 4 subclass (วงลมสีแดง, วงกลมสีน้ำเงิน, สี่เหลี่ยมสีแดง, สี่เหลี่ยมสีน้ำเงิน) ตามรูป

(เพื่อให้เข้าใจไอเดีย จะขอสมมุติว่าเรื่องสี มันไม่ได้ง่ายแค่ไปกำหนดเป็น field ใน class รูปทรง ได้ก็แล้วกัน)

จากภาพจะเห็นว่าถ้าเราเพิ่ม รูปทรงใหม่ๆเข้าไป 1 อัน เราจะต้องสร้าง subclass ใหม่ 2 ตัว สำหรับสีแดง และ สีน้ำเงิน

และในทางกลับกัน ถ้าเราเพิ่มสีใหม่เข้าไป 1 สี เราก็ต้องเพิ่ม subclass ใหม่ให้กับทุกรูปทรงเช่นกัน

แล้วเราจะแก้ปัญหาแบบนี้ยังไงดี ยิ่งเพิ่มยิ่งเยอะ ยิ่งเยอะยิ่งเลอะ ... ปวดตับเลย เพราะอ่านไม่ให้ลิ้นพันกันเนี่ยแหละ

😄 วิธีแก้ไข

ปัญหาที่มันเกิดขึ้นจริงๆมันเกิดจาก การที่เราเอาของ 2 อย่างที่ไม่เกี่ยวข้องอะไรกันเลยมาร่วมกัน ซึ่งเจ้านี่แหละคือปัญหาที่เกิดขึ้นบ่อยๆในการทำ inheritance

ซึ่ง Bridge pattern บอกว่า อย่าไปทำ inheritance ถ้าของ 2 อย่างไม่เกี่ยวข้องกัน ให้ใช้ composition แทน ซึ่งมันจะช่วยให้เราแยกเรื่องที่ไม่เกี่ยวข้องกันออกจากกันได้ ตามรูปเลย

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

ซึ่งเจ้า ลิงค์ของ object นี่แหละมันทำงานคล้ายกับ สะพาน ที่เชื่อมระหว่าง รูปทรง กับ สี และการทำแบบนี้มันทำให้เราสามารถแยกของ 2 อย่าง ออกจากกันได้

ยกตัวอย่างเช่น แยกหน้าตาโปรแกรม (GUI) ออกเป็นหลายๆตัว สำหรับผู้ดูแลระบบ, สำหรับผู้ใช้ทั่วไป ออกจาก โค้ดที่ใช้ทำงานจริงๆของมัน

จากรูป ถ้าเราไม่แยกเรื่องออกจากกัน นั่นหมายความว่าโค้ดที่ดูแลรับผิดชอบเรื่องนั้นๆย่อมใหญ่ตาม เวลาที่มี requirement เข้ามาก็จะแก้ยาก แต่ในทางกลับกันถ้าเราแยกเรื่องออกจากกัน โค้ดที่ดูแลรับผิดชอบก็จะถูกแบ่งเป็นเรื่องๆไป เวลามี requirement เข้ามาก็จะแก้ไปเรื่องๆไปเช่นกัน

ตัวอย่างที่ชัดเจนของเรื่องนี้คือ เราต้องเขียนแอพเพื่อไปให้มันใช้งานได้ทั้งบน Windows, Linux และ MacOS สิ่งที่เราควรทำคือ

แยกหน้าตา (abstraction) ออกมาเป็นเรื่องหนึ่ง และแยก การทำงานจริงๆออกเป็นอีกเรื่อง

ซึ่งเราจะให้หน้าตา (abstraction) คอยดูแลว่ามันต้องแสดงอะไรบนแอพเมื่อไปทำงานในแต่ละระบบ แล้ว เชื่อม เข้ากับการทำงานจริงๆของมัน

ดังนั้นปุ่มเซฟที่แสดงอยู่บนหน้าจอ Windows กับ Linux เมื่อผู้ใช้กดปุ่มนั้นลงไป มันก็จะไปเรียก ตัวที่ทำงานจริงๆของแต่ละระบบขึ้นมาทำงาน Windows ก็ไปเรียก windows command ส่วน Linux ก็ไปเรียก linux command

ส่วนการแสดงผลปุ่มเซฟบน Windows กับ Linux ก็จะเป็นเรื่องของหน้าตา (abstraction) ว่าจะโชว์ icon แบบไหนมาแสดง ต้องโค้งต้องเหลี่ยมยังไง ก็ขึ้นกับระบบปฏิบัติการของมัน

📌 โครงสร้างของ pattern นี้

อธิบาย Abstraction - คอยให้ client ใช้ในระบะ high-level ซึ่งภายในตัวมันจะเชื่อมไปยังตัวทำงานที่แท้จริง (low-level work) Implementation - จัดเตรียม interface ให้กับตัวทำงานที่แท้จริง โดยที่ Abstraction จะเรียกใช้ผ่าน interface พวกนี้เท่านั้น Concrete Implementations - โค้ทที่ทำงานจริงๆ Refined Abstractions - คอยจัดการเรื่องที่นอกเหนือจากที่ Abstraction ธรรมดามี ซึ่งคลาสนี้จะมีหรือไม่มีก็ได้

🛠 ตัวอย่างการนำไปใช้งาน

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

ดังนั้นแทนที่จะใช้ inheritance เราก็จะไปใช้ composition แทน โดยแบ่งออกเป็น 2 เรื่องคือ ตัวควบคุม(รีโมท) และ การทำงาน(อุปกรณ์) ตามรูป

ถ้าเราอยากมีรีโมทที่ทำได้มากกว่ารีโมทธรรมดาก็ได้ โดยสร้าง Refined Abstract class ขึ้นมา ในรูปคือ AdvancedRemote เพื่อเอาไว้ปิดเสียง

👍 ข้อดี

  • แยกงานของแต่ละ platform ออกจากกันได้

👎 ข้อเสีย

  • เพิ่มความซับซ้อนให้กับโค้ด เพราะต้องไปสร้าง class และ interface มากมาย

‍‍📝 Code ตัวอย่าง

Output

Adapter

Adapter

Convert the interface of a class into another interface clients expect. Adapter lets classes work together that couldn't otherwise because of incompatible interfaces.

🎯 เป้าหมายของ pattern นี้

ทำให้ของ 2 อย่างทำงานร่วมกันได้ แม้ว่ามันไม่ได้ถูกออกแบบให้ทำงานร่วมกันแต่แรก โดยไม่แตะต้องโค้ดเดิมที่ถูกเขียนไว้เลย

✌ หลักการแบบสั้นๆ

  1. สร้าง interface ที่เราอยากจะทำงานด้วยขึ้นมา แล้วทำงานกับ interface นั้นแทน

  2. นำ interface ที่เราสร้างขึ้นมาไปทำงานร่วมกัน class ที่ต้องการทำงานด้วย

😢 ปัญหา

กาละครั้งหนึ่ง เง็กเซียนฮ่องเต้ได้ทำแอพซื้อขายหุ้น ซึ่งตัวแอพสามารถดูราคาหุ้นในตลาดได้ตลอด 24 ชม ซึ่งการทำงานของแอพนี้ จริงๆแล้วมันก็แค่ไปดึงข้อมูลหุ้นที่เป็นไฟล์ XML จากหลายๆที่มากองๆไว้ด้วยกัน แล้วพล๊อตกราฟเท่ๆออกมาให้ดู และเง็กเซียนก็พอพระทัยในแอพนั้นเป็นอันมาก

จนมาถึงวันหนึ่ง เง็กเซียนได้ข่าวมาว่า มีบริษัทหนึ่งขาย library สำหรับวิเคราะห์หุ้นขั้นเทพ ที่ให้ผลแม่นดุจตาเห็น!! ดังนั้นเง็กเซียนเลยตัดสินใจซื้อ library ที่ว่านั้นมา

แต่เง็กเซียนแทบหงายเงิบ เพราะ library ที่ซื้อมามันดันทำงานกับ JSON format ได้เท่านั้น!!

จากรูปเลยข้อมูลหุ้นวิ่งเข้าแอพของเง็กเซียนทำงานได้ตามปรกติ แต่มันเอาเข้าไปทำงานกับ library ที่ซื้อมาไม่ได้

เง็กเซียนจะทำเช่นไรกับปัญหาที่เจอมานี้ดี เงินก็เสียไปแล้ว โปรแกรมเขาก็ไม่ได้พังอะไร จะไปแก้ให้มันทำงานกับไฟล์ XML ก็ได้ แต่นั่นหมายความว่าเง็กเซียนต้องไปแก้โค้ดที่มันมีอยู่แล้ว

และหลังจากไตร่ตรองอยู่นั่นเอง เง็กเซียนก็ตระหนักได้ว่า โค้ดนั่นมันเป็นของบริษัทอื่นนิหว่า มันแก้เองไม่ได้โว้ยยยย!! (นึกถึงหน้าจตุรเทพในเรื่องคุโรมาตี้ขึ้นมาเลย)

แล้วเง็กเซียนจะแก้ปัญหานี้อย่างไรดี . . .

😄 วิธีแก้ไข

เง็กเซียนก็โทรไปเพื่อซื้อบริษัทนั้นมา แล้วสั่งให้มันเขียนรองรับ XML จบข่าวววว (ฮา)

วิธีการแก้ไขปัญหาก็แสนจะง่าย ให้จินตนาการง่ายๆว่า สาย power โน๊ตบุ๊คเรามันมี 3 ขาใช่ปะ แต่เต้าเสียบไฟแบบทั่วๆไปมันจะมีรูให้เสียบแค่ 2 รู แล้วเราจะทำยังไงให้มันเสียบได้?? ... เราก็แค่ไปซื้อตัวแปลงจาก 3 ขาเป็น 2 ขามาใช้ไงละ!! (เคยมีครั้งนึงผมต้องหักสายกราวทิ้ง เพื่อให้มันมี 2 ขา จริงจังนะนิ -_-'')

ก่อนที่จะเมากาวไปมากกว่านี้ สิ่งที่เง็กเซียนต้องทำก็คือ สร้างสิ่งที่เรียกว่า Adapter ขึ้นมา

ซึ่งสิ่งที่เรียกว่า Adapter มันจะมีหน้าที่ห่อหุ้ม object ที่เราไม่สามารถทำงานร่วมกับมันเอาไว้ แล้วเปลี่ยนมันให้กลายเป็นของที่เราสามารถทำงานร่วมกับมันได้ รวมถึงจัดการความซับซ้อนของ object นั้นทั้งหมดอีกด้วย (ในตัวอย่าง adapter ก็คือตัวแปลงจาก 3 ขาเป็น 2 ขา)

และในบางครั้ง เราอาจจะต้องทำ Adapter ที่เป็น 2 ทาง (two-way adapter) คือ แปลงให้ฝั่ง A ใช้ฝั่ง B ได้ และยังต้อง แปลงให้ฝั่ง B ก็ใช้ฝั่ง A ได้ ด้วยช่วยกัน

จากภาพ เง็กเซียนก็จะสร้าง Adapter ขึ้นมาตัวนึงนั่นคือ XMLToJSONAdapter เอาไว้แปลงข้อมูล XML ไปเป็น JSON เพื่อส่งข้อมูลไปให้กับ library ที่ซื้อมานั่นเอง

📌 โครงสร้างของ pattern นี้

อธิบาย

  1. Client - คือคลาสที่ทำงานร่วมกับ interface ที่เป็นต้นแบบของ Adapter

  2. Client Interface - เป็นมาตรฐานในการสร้าง Adapter เพื่อให้ Client สามารถทำงานร่วมกับ class ที่ได้ตามที่ client คาดหวังไว้

  3. Service - class ที่แท้จริงที่ client ต้องทำงานร่วมด้วย ซึ่งปรกติ client จะไม่สามารถทำงานร่วมกับมันตรงๆได้

🛠 ตัวอย่างการนำไปใช้งาน

เราต้องการเขียนโปรแกรมไปดึงข้อมูลจาก Youtube ซึ่งโปรแกรมเราทำงานกับ class A แต่ เจ้าตัว Youtube API นั้นทำงานกับ class B เลยทำให้เราไม่สามารถคุยกับ Youtube API ได้ตรงๆ หรือถ้าเราไปใช้ class B ตาม Youtube แล้วเกิดวันหนึ่ง Youtube API เปลี่ยนไปใช้ class C ละ? โค้ดเราก็จะต้องคอยตามแก้ทุกครั้งที่ API เขาเปลี่ยนด้วยเหรอ?

ดังนั้นเราก็จะสร้าง Adapter ขึ้น เพื่อใช้เป็นตัวแทนคุยกับ Youtube API อีกที (ดูใน Code ตัวอย่างเลย)

👍 ข้อดี

  • สามารถเปลี่ยน Adapter เมื่อไหร่ก็ได้ นั่นหมายความว่าเราสามารถเปลี่ยน library ของ 3rd party เป็นตัวอื่นได้เรื่อยๆ โดยไม่ต้องกลับไปแก้ไขโค้ดเก่าของเราเลย

  • ถูกหลัก Single Responsibility Principle

  • ถูกหลัก Open/Closed Principle

👎 ข้อเสีย

  • เพิ่มความซับซ้อนให้กับโค้ด เพราะต้องไปสร้าง class และ interface มากมาย

‍‍📝 Code ตัวอย่าง

Output

using System;

// Abstracts
class Remote
{
    private IDevice device;

    public Remote(IDevice device)
    {
        this.device = device;
    }

    public void TogglePower()
    {
        if(device.IsEnabled)
        {
            device.Disable();
        }
        else
        {
            device.Enable();
        }
    }
}
class AdvancedRemote : Remote
{
    public AdvancedRemote(IDevice device) 
        : base(device) {}

    public void Mute()
        => Console.WriteLine("Mute!!");
}

// Implementations
interface IDevice
{
    bool IsEnabled { get; }
    void Enable();
    void Disable();
}
class Radio : IDevice
{
    public bool IsEnabled { get; private set; }
    public void Enable()
    {
        IsEnabled = true;
        Console.WriteLine("Radio is turned on");
    }
    public void Disable()
    {
        IsEnabled = false;
        Console.WriteLine("Radio is turned off");
    }
}
class Tv : IDevice
{
    public bool IsEnabled { get; private set; }
    public void Enable()
    {
        IsEnabled = true;
        Console.WriteLine("Tv is turned on");
    }
    public void Disable()
    {
        IsEnabled = false;
        Console.WriteLine("Tv is turned off");
    }
}

// Client
class Program
{
    static void Main(string[] args)
    {
        var radioRemote = new Remote(new Radio());
        radioRemote.TogglePower();
        radioRemote.TogglePower();

        var tvRemote = new AdvancedRemote(new Tv());
        tvRemote.TogglePower();
        tvRemote.TogglePower();
        tvRemote.Mute();
    }
}
Radio is turned on
Radio is turned off
Tv is turned on
Tv is turned off
Mute!!
Adapter - ตัวที่ทำหน้าที่จัดการให้ client ทำงานร่วมกับ class ที่แท้จริงที่ต้องทำงานร่วมกัน
  • Client ทำงานร่วมกับ interface เท่านั้น ไม่ได้ทำงานร่วมกับ class ที่แท้จริง ทำให้เราสามารถเขียน Adapter แบบอื่นๆให้ client ทำงานรวมได้โดยที่ไม่ต้องเปลี่ยนโค้ดใดๆเราเลย

  • // Youtube API & Models
    public class YoutubeProfile
    {
        public string Username { get; set; }
        public string DisplayName { get; set; }
        public DateTime BirthDate { get; set; }
    }
    public class YoutubeAPI
    {
        public YoutubeProfile GetProfile()
        {
            return new YoutubeProfile
            {
                Username = "saladpuk",
                DisplayName = "Mr.SaladPuk",
                BirthDate = new DateTime(2000, 1, 1),
            };
        }
    }
    
    // Adapter & Models
    public class AccountInfo
    {
        public string Id { get; set; }
        public string Name { get; set; }
        public int Age { get; set; }
    }
    public interface IAccountAdapter
    {
        AccountInfo GetAccountInfo();
    }
    public class YoutubeAdapter : IAccountAdapter
    {
        private YoutubeAPI adaptee = new YoutubeAPI();
    
        public AccountInfo GetAccountInfo()
        {
            var profile = adaptee.GetProfile();
            var age = new DateTime() + (DateTime.Now - profile.BirthDate);
            return new AccountInfo
            {
                Id = profile.Username,
                Name = profile.DisplayName,
                Age = age.Year,
            };
        }
    }
    
    // Client
    class Program
    {
        static void Main(string[] args)
        {
            var adapter = new YoutubeAdapter();
            var acc = adapter.GetAccountInfo();
            Console.WriteLine($"Id: {acc.Id}, Name: {acc.Name}, Age: {acc.Age}");
        }
    }
    Id: saladpuk, Name: Mr.SaladPuk, Age: 20

    Decorator

    Decorator

    Attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.

    🎯 เป้าหมายของ pattern นี้

    เพิ่ม/ลด ความสามารถให้กับ object ขณะ runtime

    ✌ หลักการแบบสั้นๆ

    1. สร้าง wrapper class ที่สามารถเพิ่มความสามารถเข้าไปได้เรื่อยๆ

    2. นำ wrapper class มา wrap ตัว wrapper class ที่เราต้องการความสามารถนั้นๆ

    😢 ปัญหา

    สมมุติว่าเรากำลังพัฒนา library ที่ใช้ส่งข้อความออกไปตัวหนึ่ง และมันก็เป็นที่นิยมมาก ทำให้มีบริษัทมากมายมาใช้ library ของเรา เพื่อส่งข้อความเหตุการณ์สำคัญต่างๆให้กับลูกค้าของพวกเขา

    ในเวอร์ชั่นแรก library ของเรามี class แค่ Notifier เท่านั้น ซึ่งมันมี 1 method นั่นคือ Send เอาไว้ใช้ส่งข้อความออกไป และมันทำงานได้แค่ส่งข้อความทาง อีเมล์ เท่านั้น ตามภาพ

    จากภาพ เมื่อบริษัทไหนต้องการส่งข้อความออกไป เขาก็จะเรียกใช้ Send method เพื่อส่งอีเมล์ออกไปให้กับลูกค้าของเขา

    หลังจากพัฒนาไปซักพัก เราก็ค้นพบว่าบริษัทที่มาใช้ library ของเราบางแห่งเริ่มอยากส่งข้อความออกไปทางอื่นที่ไม่ใช่อีเมล์บ้าง เช่น SMS, Facebook หรือ Slack

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

    หลังจากที่อัพเดท library ใหม่ตามรูปไปไม่นาน ก็มีบริษัทบางรายบ่นอุบอิบมาว่า ทำไมมันใช้งานยากจัง จะส่ง SMS ก็ต้องไป new object ใหม่ จะส่ง noti ให้ facebook ก็ต้องไป new ใหม่ จะส่ง slack ก็ต้องไป new ใหม่ อะไรก็ new new new ทำไมไม่ทำให้มันใช้งานง่ายๆละ

    หลังจากที่ทนฟังบริษัทพวกนั้นบ่นจนเราทนไม่ได้ เราก็เลยมาเพิ่ม subclass ใหม่เพื่อให้มันส่งข้อความได้หลายๆอย่าง พวกเอ็งอยากส่งข้อความแบบไหนก็จงไปเลือกเอาเองไป๊!!

    จากภาพไม่บอกก็คงรู้ว่า class บานเบอะ ไม่เพียงแค่เยอะใน library ของเราเท่านั้น ฝั่งที่เอา lib เราไปใช้ก็ปวดตับเช่นกัน

    ในตอนนี้เราก็ได้แต่สวดมนต์หวังว่า พวกมันจะไม่อยากได้ช่องทางส่งข้อความแบบอื่นอีก ไม่งั้น library เราอาจจะได้ลงกินเนสบุ๊คก็เป็นได้

    😄 วิธีแก้ไข

    สาเหตุที่ lib เรามันบวมเรื่อยๆก็เพราะเราแก้ไขปัญหาผิด (ก็แหงดิ!!) ดังนั้นก่อนที่จะไปดูวิธีแก้ไข เรามาดูวิเคราะห์กันก่อนว่าเกิดอะไรขึ้นกับวิธีการนั้น

    สิ่งที่เราอยากได้จริงๆคือ เราอยากให้ class ของเรามันมีความสามารถแบบใหม่ๆเพิ่มเข้าไปเรื่อยๆชิมิ? แต่การแก้ไขปัญหาโดยการใช้ inheritance มันก่อนให้เกิดปัญหาเพราะว่า

    1. Inheritance มันไม่สามารถแก้ไขความสามารถของมันได้ ในขณะ runtime นั่นหมายความว่า เราเขียนให้มันทำงานยังไง มันก็จะทำงานอย่างนั้นตลอดไป วิธีเดียวที่จะให้มันทำงานต่างจากที่มันทำ คือการสร้าง object ใหม่จาก subclass อื่นมาแทนที่มัน

    2. Subclass มี Baseclass ได้เพียงแค่ 1 ตัวเท่านั้น (ภาษาส่วนใหญ่จะเป็นแบบนั้น)

    ดังนั้นจากที่ว่ามา ถ้าเราเขียน class ให้มันส่งอีเมล์ได้เท่านั้น ทำให้ตายยังไงมันก็ไม่มีทางส่งอย่างอื่นได้นอกจากอีเมล์ (หรือคุณเคยเขียนโค้ดส่งอีเมล์แล้วพอไปทำงานมันไปส่งพิซซ่าให่คุณปะ?)

    ซึ่งวิธีการที่จะให้การทำงานของมันเปลี่ยนได้ตลอดเวลา นั่นคือการให้มัน อ้างอิง ไปหาตัวทำงานแทนต่างหาก หรือพูดง่ายๆคือให้ใช้ Composition นั่นเอง

    จากที่ว่ามาแทนที่เราจะมี hardcode ว่ามันต้องทำงานยังไง เราก็แค่เปลี่ยนมาเก็บ reference object ที่เอาไว้ใช้ทำงานแทนก็พอ ถ้าเราอยากให้มันส่งอีเมล์ได้เราก็แค่เก็บ object ที่ใช้ส่งอีเมล์ ถ้าอยากให้มันส่ง SMS ก็เปลี่ยนไปเก็บ object ที่ส่ง SMS เพียงเท่านี้เราก็สามารถเปลี่ยนพฤติกรรมของมันขณะ runtime ได้แบ๊วววว~*

    Composition เป็นประเด็นสำคัญ ที่ใช้ใน design pattern อีกหลายๆตัวเลยนะ

    ก่อนที่จะไปต่อ ขออธิบาย Wrapper นิสสสนึง (ขอเรียกมันว่า เสื้อคลุม ละกันนะ)

    หลักการทำงานของเสื้อคลุมก็คือ ตัวมันจะมีหน้าที่เข้าไปเอา object ที่เป็นเป้าหมายของมันเข้ามาอยู่ภายในตัวมัน (มันถึงถูกเรียกว่าเสื้อคลุมไง) ซึ่งเจ้าเสื้อคลุมนี้มันจะมีของทุกอย่างเหมือนกับ object เป้าหมายของมันทุกประการเลย

    คราวนี้เมื่อมี object ไหนมาสั่งให้เสื้อคลุมไปทำงาน เจ้าเสื้อคลุมมันก็จะสั่งให้ object ตัวที่มันคลุมอยู่ไปทำงาน หรือมันจะทำงานก่อนแล้วค่อยไปเรียก object ที่มันคลุมทำงานตามก็ได้

    ดังนั้น object ที่มาเรียกใช้เสื้อคลุมก็จะไม่เห็นความแตกต่างระหว่าง เจ้าเสื้อคลุม กับ object ที่อยู่ในเสื้อคลุมเลย

    ซึ่งเสื้อคลุมแต่ละตัวก็จะมีความสามารถของมันอยู่แล้ว แล้วพอมันไปคลุม object อื่น มันก็จะได้ความสามารถของ object นั้นๆเข้ามาด้วย

    และนอกจากนี้เจ้าเสื้อคลุม มันยังสามารถไปคลุม object เสื้อคลุมอันอื่นๆได้ด้วย (เหมือนคนใส่เสื้อหลายๆชั้น)

    ดังนั้น พอมันไปคลุม object 1 ตัว มันก็จะมีความสามารถของตัวมันเอง + ความสามารถของ object นั้น แล้วยิ่งถ้ามันคลุมทับกันไปทับกันมา มันก็จะยิ่งมีความสามารถเพิ่มขึ้นเรื่อยๆ (โขมยเน็นได้ว่างั้น)

    แล้วเจ้าเสื้อคลุมมันเกี่ยวอะไรกับ Decorator พระเอกของเราฟระ

    ลองคิดง่ายๆว่าเอาแนวคิดของเสื้อคลุมมาแก้ปัญหานี้สิ

    อย่างแรกคือให้มี class Notifier ไว้สำหรับส่งอีเมล์เหมือนเดิม

    ถัดมาก็สร้าง class เสื้อคลุมที่ชื่อว่า BaseDecorator ไว้ โดยออกแบบให้มันไปคลุม Notifier ของเรา

    ดังนั้น BaseDecorator เลยมี method ต่างๆเหมือนกับ Notifier ทุกประการ (นั่นคือ Send นั่นเอง) และแน่นอนมันต้องมี Composition กลับไปหาเจ้า object ตัวที่โดนคลุมด้วย

    ถัดมา ความสามารถในการส่งข้อความทาง SMS, Facebook, Slack เราก็จะไปสร้างเป็น subclass ของเสื้อคลุม BaseDecorator อีกที ออกมาเป็นตามรูป

    จากภาพ เมื่อ client อยากจะส่งอีเมล์ธรรมดาก็แค่เรียกใช้ Send จาก Notifier

    แต่ถ้าเราอยากให้มันส่งอีเมล์ได้และส่ง Facebook ได้ด้วย เราก็จะเอาเสื้อคลุมที่ชื่อว่า FacebookDecorator ไปหุ้ม object ของ Notifier อีกทีหนึ่ง ทำให้มันมีความสามารถเพิ่มขึ้นเป็น ส่งได้ทั้งทาง Facebook และ อีเมล์ นั่นเอง

    และถ้าอยากให้มันส่งไปยัง Slack ได้ด้วย เราก็จะเอา SlackDecorator ไปหุ้มต่ออีกชั้นนึง มันก็จะส่งทาง Facebook อีเมล์ และ Slack ได้ ทำให้มันมีความสามารถต่อยอดกันได้เรื่อยๆ ตามรูปเลย

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

    จากเหตุนี้เอง เลยทำให้ Decorator pattern มีชื่อเรียกอีกอันนึงคือ Wrapper นั่นเอง

    📌 โครงสร้างของ pattern นี้

    อธิบาย 1.Component - เป็น interface ที่ใช้เป็นแบบอย่างของ object ที่จะถูกคลุม กับตัว Wrapper 2.Concrete Component - คือ class ที่จะถูกคลุม ซึ่งตัวมันจะทำงานพื้นฐานที่สุดไว้ เช่น ส่งอีเมล์ 3.Base Decorator - เป็น baseclass ที่ใช้ในการคลุมของต่างๆจาก Component 4.Concrete Decorators - เป็น subclass ของ Base Decorator ที่มีสามารถอื่นๆเข้ามา มีไว้ใช้เพิ่มความสามารถให้กับ decorator เวลามาคลุม ซึ่ง subclass จะทำการ override method จาก Base Decorator โดยสั่งให้ไปเรียก object ที่มันคลุมอยู่ไปทำงานก่อน หรือทำงานในส่วนของมันแล้วค่อยไปเรียก object ที่มันคลุมอยู่ไปทำงานต่อ

    🛠 ตัวอย่างการนำไปใช้งาน

    สมมุติว่าเราต้องเขียนโปรแกรมคิดเงินให้กับร้านขายน้ำปั่นซักร้าน ซึ่งร้านนี้มีความพิเศษคือ สามารให้ลูกค้าเพิ่มทอปปิ้งได้ไม่จำกัด (แต่คิดเงินเพิ่มนะ) และ ทอปปิ้งแต่ละตัวราคาก็ไม่เท่ากันด้วย ซึ่งพอลูกค้าสั่งเสร็จก็ต้องออกใบเสร็จให้ลูกค้าด้วยว่าเขาสั่งอะไรไป รวมเป็นเท่าไหร่

    เช่น สั่งกาแฟ(35บ) เพิ่มนมข้นหวาน(10บ) เพิ่มวิปครีม(13บ) เพิ่มวิปครีม(13บ) เพิ่มคาราเมล(15บ) แล้วก็สั่ง ชานม(30บ) เพิ่มวิปครีม(13บ) เพิ่มไข่มุก(7บ)

    จากโจทย์เราก็จะใช้ Decorator pattern มาช่วย ซึ่งเราก็จะแบ่งของออกเป็น 2 อย่างคือ

    1. ประเภทน้ำปั่น มองว่าเป็น Component

    2. ทอปปิ้งต่างๆ มองว่าเป็น Decorator

    พอแยกของออกเป็น 2 อย่างแบบนี้ก็จะเห็นว่า ผมสามารถเพิ่มเมนูน้ำปั่นเข้าไปได้เรื่อยๆ โดยไม่มีผลกระทบกับ ทอปปิ้ง และเช่นกันผมก็เพิ่มทอปปิ้งได้เรื่อยๆไม่มีผลกับเมนูน้ำปั่น

    แล้วถ้าอยากเพิ่มทอปปิ้งลงไปในน้ำอะไร เราก็แค่สร้าง decorator object ไปหุ้ม object ก่อนหน้าซะก็สิ้นเรื่อง

    ปะไปดูตัวอย่างโค้ดดีกว่า อ่านอย่างเดียวปวดตับเปล่าๆ

    👍 ข้อดี

    • เพิ่มความสามารถให้กับ object โดยไม่ต้องสร้าง class ใหม่เรื่อยๆ

    • เพิ่ม/ลด ความสามารถของ object ขณะ runtime ได้

    👎 ข้อเสีย

    • เมื่อต้องการลบความสามารถบางอย่างออกจาก wrapper จะลบค่อนข้างยาก (เพราะมันซ้อนๆกันอยู่)

    • การทำงานต้องเป็นไปตามลำดับเท่านั้น ถ้าจะไม่ให้เรียงตามลำดับจะทำค่อนข้างยาก

    • โค้ดตอนสร้างและเอาความสามารถมาเพิ่มๆให้กัน ค่อนข้างน่าเกลียด

    ‍‍📝 Code ตัวอย่าง

    Output

    using System;
    
    // Component
    interface IBeverage
    {
        string Description { get; }
        int GetCost();
    }
    // Concrete components
    class Coffee : IBeverage
    {
        public string Description => "กาแฟ";
        public int GetCost() => 35;
    }
    class MilkTea : IBeverage
    {
        public string Description => "ชานม";
        public int GetCost() => 30;
    }
    
    // Decorator
    abstract class ToppingDecorator : IBeverage
    {
        private IBeverage wrappee;
        public ToppingDecorator(IBeverage beverage)
        {
            wrappee = beverage;
        }
    
        public virtual string Description => wrappee.Description;
        public virtual int GetCost() => wrappee.GetCost();
    }
    // Concrete decorators
    class CondensedMilk : ToppingDecorator
    {
        public override string Description => $"{base.Description} + ขมข้นหวาน";
        public override int GetCost() => base.GetCost() + 10;
        public CondensedMilk(IBeverage beverage) : base(beverage) { }
    }
    class WhipCream : ToppingDecorator
    {
        public override string Description => $"{base.Description} + วิปครีม";
        public override int GetCost() => base.GetCost() + 13;
        public WhipCream(IBeverage beverage) : base(beverage) { }
    }
    class Caramel : ToppingDecorator
    {
        public override string Description => $"{base.Description} + คาราเมล";
        public override int GetCost() => base.GetCost() + 15;
        public Caramel(IBeverage beverage) : base(beverage) { }
    }
    class Pearl : ToppingDecorator
    {
        public override string Description => $"{base.Description} + ไข่มุก";
        public override int GetCost() => base.GetCost() + 7;
        public Pearl(IBeverage beverage) : base(beverage) { }
    }
    
    class Program
    {
        static void Main(string[] args)
        {
            var coffee = new Coffee();
            Console.WriteLine("กาแฟธรรมดา");
            Console.WriteLine($"{coffee.Description}, ราคา: {coffee.GetCost()} {Environment.NewLine}");
    
            IBeverage extraCoffee = new CondensedMilk(coffee);
            extraCoffee = new WhipCream(extraCoffee);
            extraCoffee = new WhipCream(extraCoffee);
            extraCoffee = new Caramel(extraCoffee);
            Console.WriteLine("กาแฟพิเศษเพิ่มทอปปิ้ง");
            Console.WriteLine($"{extraCoffee.Description}, ราคา: {extraCoffee.GetCost()} {Environment.NewLine}");
    
            var milkTea = new MilkTea();
            IBeverage extraMilkTea = new WhipCream(milkTea);
            extraMilkTea = new Pearl(extraMilkTea);
            Console.WriteLine("ชานมพิเศษเพิ่มทอปปิ้ง");
            Console.WriteLine($"{extraMilkTea.Description}, ราคา: {extraMilkTea.GetCost()} {Environment.NewLine}");
        }
    }
    กาแฟธรรมดา
    กาแฟ, ราคา: 35 
    
    กาแฟพิเศษเพิ่มทอปปิ้ง
    กาแฟ + ขมข้นหวาน + วิปครีม + วิปครีม + คาราเมล, ราคา: 86 
    
    ชานมพิเศษเพิ่มทอปปิ้ง
    ชานม + วิปครีม + ไข่มุก, ราคา: 50 
    img
    img
    img
    img
    img
    img
    img
    img
    img
    img
    img
    img
    img
    img
    img
    img
    img
    img
    img
    img
    img
    img
    img
    img
    img

    Structural patterns

    ช่วยในการออกแบบโครงสร้างของ class ต่างๆ ซึ่งในกลุ่มนี้จะประกอบไปด้วย patterns ต่างๆด้านล่างนี้

    AdapterBridge
    • Composite pattern

    DecoratorFacade
    • Flyweight pattern

    Proxy