/ OOD

[365 วันแห่งโปรแกรม #day48] SOLID (ตอนที่ 5 Dependency inversion)

วันที่สี่สิบแปดของ ‪#‎365วันแห่งโปรแกรม ตอนสุดท้ายของ SOLID แล้วครับ มาใชชื่อเรื่อง Dependency inversion


Dependency inversion principle

Dependency inversion principle หรือ DIP กล่าวถึงวิธีการลด coupling ระหว่างโมดูล ซึ่งบอกเอาไว้ว่าส่วนการทำงานที่เป็น High Level Module นั้นจะต้องไม่ผูกติดกับส่วน Low Level Module โดยตรง แต่ทั้งสองจะคุยกันผ่าน abstraction โดย abstraction นั้นก็จะไม่ได้เจาะจงเรื่องรายละเอียดเลย แต่เราจะต้อง implement ส่วนการทำงานขึ้นมาให้สอดคล้องกับ interface

โค้ดเลยดีกว่า

อ่านไปก็งงเปล่าๆ ครับ เราว่าดูตัวอย่างกันก่อนแล้วค่อยดูว่าประโยชน์ที่ได้คืออะไรแน่

public class Email
{  
    public void SendEmail()
    {
    // code
    }
}

public class Notification
{
    private Email _email;
    public Notification()
    {
        _email = new Email();
    }

    public void PromotionalNotification()
    {
       _email.SendEmail();
    }
}

หลายๆ คนคงเคยเขียนโค้ดประมาณนี้ครับ คือมีคลาสหนึ่งเป็น model และก็อีกคลาสนึงเอาไปใช้ และนี้คือตัวอย่างที่ผิดกฎของ DIP ครับ คำถามคือผิดยังไง? เมื่อเราพิจารณาแล้วจะพบว่า High Level Module ในที่นี้ก็คือคลาส Notification ซึ่งมีการสื่อการกับ Low Level Module ซึ่งก็คือ Email โดยตรงเลย ทำให้เกิดการ Coupling กัน หมายความว่าถ้าเราแก้คลาส Email แล้วก็มีโอกาสที่จะส่งผลกระทบต่อ Notification เพราะไม่มีหลักประกัน (interface) อะไรเลยที่บอกไว้ว่า Email จะมีหน้าตาแบบนี้เสมอ ถ้าวันนึงเราอยากให้ส่ง SMS แทนล่ะ ก็เหมือนกันครับต้องมานั่งแก้ที่ High Level Module ใหม่

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

งั้นเรามาดูกันดีกว่าครับว่าจะแก้ได้อย่างไร

public interface IMessageService
{
    void SendMessage();
}
public class Email : IMessageService
{
    public void SendMessage()
    {
        // code
    }
}
public class Notification
{
    private IMessageService _iMessageService;

    public Notification()
    {
        _iMessageService = new Email();
    }
    public void PromotionalNotification()
    {
        _iMessageService.SendMessage();
    }
}

คราวนี้เราสร้าง interface IMessageService ขึ้นมาเป็นต้นแบบให้โมดูลสำหรับส่งข้อความ เสร็จแล้วก็ให้คลาส Notification เอา interface นี้ไปใช้ ทุกอย่างก็ดูดีขึ้นครับ ถ้าวันนึงเรา imeplement การส่ง SMS ขึ้นมาจาก interface เดิม ก็แค่มาแก้ใน Notification ว่าเปลี่ยนเป็น SMS แทนนะ แต่เมื่อกี้พูดว่าอะไรนะครับ ให้แก้เหรอ? ถ้าบอกว่าต้องแก้มันก็คือผิดหลัก DIP แล้วสิ เพราะ High Level Module ไม่ควรรู้ด้วยซ้ำว่ามันกำลังคุยอยู่กับใคร

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

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

ลองจินตนาการถึงธนาคารสีเขยวดูครับ เราเป็นลูกค้า internet banking ของเค้ามาตลอด แล้วอยู่วันนึง พอเถอะครับ!! น่าจะเห็นภาพกันตั้งแต่เรื่องแรกแล้วแหละ ><

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

Dependency Injection

วิธีการที่จะแก้ไขปัญหาข้างต้นนั้นก็คือเปลี่ยนไปใช้วิธีใหม่แทนที่จะกำหนดตัวคนที่คุยด้วยจากภายในโมดูล ก็ให้คนข้างนอกส่งคนเข้าไปแทน เราเรียกวิธีนี้ว่า Dependency Injection

  • Dependency ก็แปลตรงตัวเลยครับ คือสิ่งที่โมดูลที่เราสนใน depend อยู่ จากตัวอย่างก็คือ IMessageService

  • Injection ก็คือการฉีดเข้าไป

สรุปก็คือการส่ง implementation ของ Dependency เข้าไปในโมดูลครับ

เราสามารถแบ่ง Dependency Injection หรือ DI ออกได้เป็น 3 แบบตามวิธีการ inject ได้แก่

  • Constructor Injection คือการ inject ตั้งแต่เราสร้างโมดูลที่เป็น High Level

  • Property injection คือการ inject หลังจากสร้าง High Level Module แล้ว

  • Method injection คือการ ​inject ในเวลาที่จะใช้

Constructor Injection

Constructor Injection เป็นวิธีการที่ใช้บ่อยที่สุดแล้วโดยเราจะทำการ inject เข้าไปตั้งแต่ตอนสร้าง module ผ่าน constructor ของโมดูลนั้นๆ แล้วเก็บ dependency นั้นไว้

public class Notification
{
    private IMessageService _iMessageService;

    public Notification(IMessageService _messageService)
    {
        this._iMessageService = _messageService;
    }
    public void PromotionalNotification()
    {
        _iMessageService.SendMessage();
    }
}

จากโค้ดข้างต้น ในตอนสร้าง object ของ Notification เราจะโยน object ของ IMessageService เข้สไปด้วย แล้ว Notification ก็จะเก็บ IMessageService เอาไว้ใช้ต่อไป ซึ่งวิธีนี้เราได้ใมช้กันไปแล้วเมื่อวานนี้ตอนที่ implement AllInOneMachine

Property injection

Property injection คือการ inject dependency หลังจากเราได้สร้าง object ของ Hight Level Module แล้ว ซึ่งเราไม่ค่อยนิยมใช้วิธีนี้กันเท่าไหร่ เนื่องจากต้องเปิดให้ property สำหรับ dependency นั้นๆ ถูกเขียนได้จากภายนอก

public class Notification
{
    public IMessageService MessageService
    {
        get;
        set;
    }
    public void PromotionalNotification()
    {

        if (MessageService == null)
        {
            // some error message
        }
        else
        {
            MessageService.SendMessage();

        }
    }
}

จากโค้ดข้างต้นเราสร้าง property สำหรับเก็บ IMessageService เอาไว้ และทุกครั้งก่อนเราจะใช้งาน IMessageService นี้ เราต้องทำการเช็คดูก่อนว่าเราได้ inject instance ของ IMessageService เข้ามาแล้วหรือยัง ซึ่งก็ทำให้ยุ่งยากกว่าวิธีแลก แต่ก็ได้มาซึ่งความสามารถในการเปลี่ยน implementation ของ dependency ตอนรันไทม์

Method injection

Method injection เป็นการ inject dependency เข้ามาใช้งานตอนเรียกเมธอดที่ต้องการโดยไม่เก็บ instance เอาไว้เลย

public class Notification
{
    public void PromotionalNotification(IMessageService _messageService)
    {
        _messageService.SendMessage();
    }
}

จากโค้ดจะเห็นว่าเราแก้ไข PromotionalNotification() ให้รับ IMessageService เข้ามาด้วย หลังจากนั้นเราก็ใช้ IMessageService ตัวนี้ในการทำงานต่อไปแล้วก็จบเมธอดไปเฉยๆ วิธีนี้ค่อนข้างเป็นที่นิยมเนื่องจากเราสามารถเปลี่ยน implementation ของ dependency ได้ในตอนรันไทม์เลย และยังไม่ต้องทำ accessor ให้เข้าถึง property จากภายนอกด้วย

ประโยชน์ที่ได้รับจาก DIP และ DI

หัวใจหลักของ DIP คือการลดการ depend บน implementation แต่ยอมให้ depend บน interface ได้ ส่วน DI ก็บอกถึงวิธีที่จะส่ง implementation ของ dependency เข้าไปใช้ ซึ่งเมื่อเราทำตามเจ้าสองตัวนี้แล้ว เราก็จะได้โค้ดที่สามารถปรับเปลี่ยนได้ง่าย และเทสได้ (โดย inject mockup data เข้าไปทดสอบ)

สรุป

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

References

Dependency Inversion Principle and the Dependency Injection Pattern

An Absolute Beginner's Tutorial on Dependency Inversion Principle, Inversion of Control and Dependency Injection

Dependency injection

SOLID Part 5 :: แนวคิด Dependency Inversion Principle

Dependency inversion principle

#‎day48 #365วันแห่งโปรแกรม ‪#‎โครงการ365วันแห่ง‬...