แฉหมดเปลือก! Dependency Injection ไม่ได้ยาก แค่ Dagger แม่งงง
DI คืออะไร แล้วจะฉีดไปทำไม
เรื่อง DI นี่ผมเล็งจะเขียนนานละ แต่มันเป็นเรื่องที่คลุมเครือมากๆ คือตอนแรกที่ผมหัดใช้ Dagger ก็แค่เปิด doc ทำตาม ลองผิดลองถูกจนสำเร็จใช้งานได้ แต่ไม่ได้เข้าใจ มันงงอะ จะต้มกาแฟทีนี่ต้องประกอบเครื่องทำกาแฟ มี pump มี heater มี thermosiphon อะไรก็ไม่รู้ บาริสต้าก็ไม่ใช่ ไม่รู้เรื่องเว่ย
เมื่อเวลาผ่านไป ผมได้เขียน Angular โดนบังคับให้ใช้ DI และได้ลอง Spring Boot ซึ่งมีพื้นฐานอยู่บน Spring Framework ที่เป็น dependency injection framework โดยกำเนิด ทีนี้พอผมได้เห็นการใช้งาน DI 3 แบบ ก็เริ่มมองเห็น pattern บางอย่าง เริ่มจับใจความได้จนออกมาเป็นโพสนี้ในที่สุด
Dependency คืออะไร
อุปสรรคแรกที่ทำให้ผมไม่เข้าใจ DI คือศัพท์คำนี้ เมื่อก่อนผมไม่เข้าใจว่ามันคืออะไร หลังจากไปเปิด google translate พบว่า
depend = พึ่งพา, อาศัย, ขึ้นอยู่กับ
dependency คือ “โค้ด” ที่โค้ดของเราต้องใช้ หรือ “lib” ที่แอพเราต้องใช้ ยกตัวอย่างเช่น
อย่างนี้คือ Car
depends on Engine
หรือ Engine
เป็น dependency ของ Car
นั่นเอง ไม่งั้นก็ start()
ไม่ได้
อย่างนี้คือแอพของเรา depends on gson
หรือ gson
เป็น dependency ของแอพเรานั่นเอง ไม่งั้นก็ parse json ไม่ได้
หรือกรณีนี้ ผม depends on Music หรือ Music เป็น dependency ของผมนั่นเอง ไม่งั้นก็มีชีวิตอยู่ไม่ได้
Dependency ในบริบทของ DI จะเป็นลักษณะ object หนึ่ง พึ่งพาอีก object หนึ่งแบบ Car
พึ่งพา Engine
ครับ
Dependency Injection คืออะไร
จากตัวอย่าง Car
+ Engine
ข้างต้น จะเห็นว่า Car
เป็นคน new object engine
ขึ้นมาเองไม่จัดเป็นการ inject
การ inject คือการที่เราขอ dependency มาใช้ โดยไม่ได้สร้างเอง
ใช่แล้วครับ แค่การส่งค่าผ่าน constructor ก็ถือเป็น DI แล้ว Car
ไม่ได้สร้าง Engine เอง แต่ขอมาใช้ผ่าน constructor เค้าเรียกว่า constructor injection นอกจากนั้นยังมี method inject และ field injection อีก ไปศึกษาต่อได้
ทำไมต้อง Dependency Injection
ตอนแรกที่ผมหัดใช้ Dagger ผมหัดเสร็จก็เลิก ไม่ได้ใช้จริง รู้สึกมันเป็น boilerplate code ที่ไม่จำเป็น แต่วันนี้กลายเป็นว่าขาด DI จะรู้สึกแปลกๆ จุดเปลี่ยนก็คือ
การเขียน Test
ผมขอยกคุณสมบัติของโค้ดที่เทสง่ายมา 2 ประการคือ
- จัดฉากได้ สมมติผมเขียน MVP ผมจะเทส presenter ว่าถ้ายิง api fail จะแสดง dialog error ถูกมั้ย ผมต้องบังคับ(จัดฉาก)ให้การยิง api มัน fail ได้
- วัดผลได้ ทีนี้พอผมบังคับให้ยิง api fail ได้แล้ว ก็ต้องมีวิธีวัดว่า dialog แสดงจริงมั้ย
จากโค้ดด้านบน ถ้า presenter
new Api
ขึ้นมาเอง จะไม่สามารถเอาของปลอมเข้าไปแทนเพื่อจัดฉากได้ และถ้า view ไม่ได้ถูก inject เข้ามา ก็ไม่สามารถ verify(view)
ได้เช่นกัน
Loose Coupling
เค้าว่ากันว่าให้ออกแบบโค้ดให้ low couple high cohesion ใช่มั้ยครับ วิธีนึงที่ลด coupling ได้คือการใช้ interface แทน class
อย่างนี้ไม่ต่ำเท่าไร เพราะ MyActivity
รู้จัก class MyPresenterImpl
โดยตรง
อย่างนี้ต่ำลงเพราะ MyActivity
รู้จักแค่ interface MyPresenter
จะเปลี่ยนจะแก้อะไร ขอให้ implement ตาม interface ก็พอ
แต่ก็ยังตกม้าตายตอนจบอยู่ดีเพราะความจริง MyActivity
รู้จัก MyPresenterImpl
ตอน new ขึ้นมาใหม่นั่นแหละ
ถ้าเราผลักภาระการสร้างออบเจ็คใหม่ไปที่ DI framework MyActivity
ของเราก็จะรู้จักแค่ interface MyPresenter
จริงๆ
และยังมีเหตุผลอื่นๆที่เราควรใช้ DI อีก ตามไปอ่านตรงนี้ได้เลย
ส่วนประกอบของ Dependency Injection Framework
เท่าที่ผมสังเกต การใช้งาน DI framework โดยทั่วไปประกอบด้วย 3 ขั้นตอน รายละเอียดจะแตกต่างกันไปตามแต่ละ framework
- ขอ dependency
- ให้ dependency
- สร้าง dependency graph
1. ขอ dependency
ขั้นตอนนี้ง่ายสุด แค่ประกาศว่าคลาสเราจะใช้ออบเจ็คอะไรบ้าง
เช่น Android + Dagger แค่เรา @Inject
ไปแปะหน้า constructor มันก็จะรู้แล้วว่าคลาส MyPresenter
ต้องการใช้ Api
การขอยังมีอีกหลายวิธี แต่ไม่ขอลงรายละเอียดนะครับ
2. ให้ dependency
โอเค ทีนี้พอมีคนขอแล้ว เราก็ต้องบอก DI framework ว่า dependency เหล่านั้นสร้างยังไง
แค่เอา @Inject
ไปแปะหน้า constructor ก็เป็นการบอก Dagger ให้รู้แล้วว่า ถ้าใครมาขอ Api
ให้ new Api
ด้วย constructor ตัวนี้นะ
ถ้าเพื่อนๆสังเกตดีๆ จะพบว่าทั้งการขอและการให้ ใช้ @Inject
เหมือนกันเลย เดี๋ยวอ่านข้อ 3 แล้วจะอ๋อทันที
การให้ dependency ยังมีอีกหลายวิธี ไม่ขอลงรายละเอียดเช่นกันครับ
3. สร้าง dependency graph เชื่อมระหว่างผู้ขอผู้ให้ทุกคน
ขั้นตอนนี้จะเป็นการรวมผู้ขอและผู้ให้ทุกคน มาวิเคราห์ และผลิตสิ่งที่เรียกว่า dependency graph ออกมา เจ้ากราฟนี้จะเก็บขั้นตอนวิธีการสร้างออบเจ็คทุกชนิดเอาไว้ ไม่ว่าใครขอใช้อะไร DI framework ก็สามารถผลิตให้ได้
ตรงนี้เราไม่ต้องทำอะไร เป็นหน้าที่ของ framework ยกเว้นว่าเราจะ config อะไรเป็นพิเศษตามแต่ละ framework จะให้ทำ
สมมติ MyActivity
ขอ MyPresenter
จะเกิดเหตุการณ์ดังนี้
- Dagger พยายามสร้าง
MyPresenter
จาก@Inject constructor(val api: Api)
- Dagger พบว่าการสร้าง
MyPresenter
ต้องมีApi
ก่อน - Dagger พยายามสร้าง
Api
จาก@Inject constructor()
Api
ไม่มี dependency สร้างได้เลย- นำ
Api
ที่เพิ่งสร้างไปสร้างMyPresenter
- นำ
MyPresenter
ไปให้MyActivity
แต่ละ dependency ก็มีวิธีการสร้างมัน ซึ่งก็อาจจะมี dependency ของมันเองอีก ซึ่งก็อาจจะมี dependency ของมันเองอีก depenception จริงๆ
4. Integrate DI framework/library กับโปรเจ็คของเรา
อ้าว ไหนบอกว่ามี 3 ขั้นไง แล้วอันนี้มาจากไหน คืออันนี้แล้วแต่ว่าเรากำลังเขียนอะไร ถ้าผมทำ Spring Boot หรือ Angular ก็ไม่ต้องทำอะไรเลย DI ถูกฝังอยู่ในตัวแล้ว แต่ถ้าผมทำ Android เนี่ย ก็ต้องมานั่งสร้าง component สร้าง module ไล่ generate subcomponent ของแต่ละ Activity
ของแต่ละ Fragment
เติม ActivityInjector
ใน Application
แล้วต้องใส่ AndroidInjection.inject()
ใส่ base class ต่างๆอีก
ตรงนี้ผมก็ไม่ได้จำได้หรอกนะ จะทำทีก็เปิดอ่านที ตัวใครตัวมันละกัน ตาม link พวกนี้ไปอ่านกันเอง 55555