Dependency injection ฉบับบ้านๆ

ฉลองครบรอบแปดปีที่เขียนบล็อกที่นี่ ขอยกกรณีศึกษา “ไม่อยาก Load test การส่งอีเมล์” เพื่อให้เห็นภาพเนื่องจากเป็นสิ่งใกล้ตัว ตัวอย่างเช่นหากผู้ใช้งานมีการลงทะเบียนในเว็บเรา ให้ทำการส่งอีเมล์ยืนยันการสมัครกลับไปหาลูกค้า นั่นหมายความว่าระบบลงทะเบียนมีความสัมพันธ์กับระบบส่งอีเมล์อย่างเลี่ยงไม่ได้ ดังนั้นหากเราต้องการทำ load test ระบบลงทะเบียนแล้วเราไม่ต้องการให้ส่งอีเมล์ไปหาลูกค้าจริงๆ จะทำอย่างไร? ถ้าเราปิดระบบส่งอีเมล์แล้วหากมีบางเหตุการณ์ที่ต้องการให้ส่งอย่างเช่นเชื่อมต่อฐานข้อมูลไม่ได้จะทำอย่างไร?

Dependency

ผมสร้างคลาสตัวอย่างเพื่อจำลองการส่งอีเมล์ชื่อ EmailService

public class EmailService{
    public void sendMail(String to, String body){
        // connect to email gateway and send
    }
}

ดังนั้นหากระบบลงทะเบียนต้องการส่งอีเมล์จะต้องทำการ new class EmailService ดังนี้

public class RegisterService{
    public void sendWelcomeEmail(String email){
        EmailService emailService = new EmailService();
        emailService.sendMail(email, "Welcome....");
    }
}

ทำให้ทั้งสองคลาสนี้เกิด Dependency ต่อกัน

Config เมื่อไม่ต้องการให้ส่งอีเมล์

เมื่อเราไม่ต้องการให้เกิดการส่งอีเมล์จริงตอนทำ load test สิ่งที่ผมพบเจอบ่อยคือเราจะสร้าง config ขึ้นมาหนึ่งตัว (ไม่ว่าจะเก็บเป็น properties file หรือว่าลง database) จากนั้นก็สร้างคลาสขึ้นมาเพื่ออ่านค่านี้

public class Config{
    private boolean enableSendMail;

    public Config(){
         // Read config value
    }

    public boolean isEnableSendMail(){
        return enableSendMail;
    }
}

แล้วแก้ไข EmailService เพื่อทำให้รู้ว่าต้องส่งเมล์ตอนใหน

public class EmailService{
    public void sendMail(String to, String body){
        Config config = new Config();

        if(config.isEnableSendMail()){
            // connect to email gateway and send
        }
    }
}

ยังไม่ตอบโจทย์

ตามหลักปฏิบัติในการออกแบบและเขียนโปรแกรมเชิงวัตถุ เราต้องทำให้หนึ่งคลาสทำงานเพียงแค่หนึ่งอย่างตามหลัก Single responsibility principle ซึ่งเป็นหนึ่งในห้าข้อปฏิบัติที่ชื่อ S.O.L.I.D. (S ตัวแรกคือคำย่อของ Single responsibility principle หรือ SRP ขอไม่อธิบายในที่นี้) อย่างแรกที่เราเห็นคือคลาส EmailService จะต้องมี Dependency เพิ่มหนึ่งคลาสคือคลาส Config และคลาสนี้ยังต้องไปพึ่งค่า config จากภายนอก (properties file หรือ database)
ในเมื่อเราไม่ต้องการให้มีการส่งเมล์ เราก็สร้างคลาส EmailService เพื่อที่จะไม่ต้องส่งเมล์ขึ้นมาอีกหนึ่งอันก็ได้ ดังนี้

public class MockEmailService{
    public void sendMail(String to, String body){
       return;
    }
}

ดังนั้น หากเราไม่ต้องการให้ RegisterService ส่งเมล์เราก็ new MockEmailService ดังนี้

public class RegisterService{
    public void sendWelcomeEmail(String email){
        MockEmailService emailService = new MockEmailService();
        emailService.sendMail(email, "Welcome....");
    }
}

หากมี Service อื่นที่ต้องการส่งเมล์จริง เราก็ new EmailService ตามปกติมาใช้ได้ เช่น DatabaseMonitorService

public class DatabaseMonitorService{
    public void sendWelcomeEmail(String email){
        EmailService emailService = new EmailService();
        emailService.sendMail(email, "Welcome....");
    }
}

แล้วมันจะไม่งงเหรอครับ

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

Dependency Injection

กรณีของ RegisterService ความต้องการที่บอกว่าจะให้ส่งหรือไม่ส่งเมล์นั้น ไม่ใช่ความต้องการของของคลาสนี้เลย เป็นความต้องการของคนอื่นทั้งนั้น เราจึงต้องหาทางเพื่อบอกให้คนที่มาเรียกใช้ RegisterService ว่า

พี่น้องค้าบบ ถ้าจะให้ส่งหรือไม่ส่งเมล์หน่ะ ช่วยเลือกมาให้เลยได้มั้ย มันไม่ใช่ความรับผิดชอบของโพ้มมมม

เพราะฉะนั้น RegisterService จึงต้องประกาศข้อตกลงนี้ผ่านทาง interface ไว้ว่า ถ้าจะให้ส่งเมล์หรือไม่ส่ง รบกวนไป implement interface นี้ไว้ให้กับคลาสพวกที่เกี่ยวกับอีเมล์ไว้ด้วย

การโยนความรับผิดชอบในการ new class ให้กับคนอื่นที่มาเรียกใช้เรา เรียกว่า Inversion of Control (IoC) อยู่มาพักใหญ่ก็มีการเรียกชื่อใหม่ว่า Dependency Injection ซึ่งก็คืออันเดียวกัน

เราสร้าง interface เพื่อบังคับให้คนอื่นทำตามดังนี้

public interface SendEmailable{
    void sendMail(String to, String body);
}

จากนั้นก็ไปทำการ implement ให้กับคลาสที่ใช้ส่งอีเมล์ทั้งสองดังนี้

public class EmailService implements SendEmailable{
    public void sendMail(String to, String body){
        // connect to email gateway and send
    }
}

และ

public class MockEmailService implements SendEmailable{
    public void sendMail(String to, String body){
       return;
    }
}

และท้ายสุด ก็แก้ไขคลาส RegisterService เพื่อบังคับให้คนอื่นต้องส่งคลาสอีเมล์มาด้วย ผ่าน constructor ดังนี้

public class RegisterService{

    private SendEmailable emailService;

    public RegisterService(SendEmailable emailService){
        this.emailService = emailService;
    }

    public void sendWelcomeEmail(String email){
        emailService.sendMail(email, "Welcome....");
    }
}

เพียงเท่านี้คลาส RegisterService ก็ไม่ต้องห่วงเรื่องที่จะต้องแก้คลาสที่ใช้ส่งอีเมล์แล้ว

ยังไม่จบ

ปัญหาคือในการเอาไปใช้งานจริง ใครจะเป็นคนกำหนดว่า จะ new คลาส MockEmailService เพื่อใช้ตอนทดสอบระบบหรือ EmailService เพื่อใช้งานจริง นั่นคือเหตุผลที่ทำให้เกิด Spring framework ขึ้นมาในยุคนนั้น เพื่อเป็นการ Manage Dependency จากตัวอย่างที่ผมยกมาเราต้องนำเอา MockEmailService และ EmailService ไปลงทะเบียนไว้กับ Spring (ยุคนั้นเป็น xml) พอเวลาจะปรับเปลี่ยน เราก็แก้ไขที่ xml นั้นไฟล์เดียว

จนมาถึงยุค Spring 3.1 ที่มีความสามารถในการทำ profiles ให้กับ bean ได้ว่า ถ้ากำหนด active profile = test ให้ทำการ new MockEmailService ถ้าเป็น production ให้ new EmailService โดยที่เราไม่ต้องไปแก้ไขเองอีกต่อไป

แหล่งอ้างอิง

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s