ในที่สุดก็ถึงเวลาเขียนบทความของ Dagger 2 เสียที เพราะนี่คือหนึ่งใน Library ยอดนิยมที่ใช้กันในโปรเจคใหญ่ๆที่มีความซับซ้อนที่มีอะไรข้างในมากกว่าโค้ดแบบ MVC ธรรมดาๆ
บทความที่เกี่ยวข้อง
• [ตอนที่ 1] - Dependency Injection แบบหล่อๆด้วย Dagger 2• [ตอนที่ 2] - มาเตรียมโปรเจคสำหรับ Dagger กัน
• [ตอนที่ 3] - ทำ Dependency Injection ให้กับ Activity และ Fragment
• [ตอนที่ 4] - ทำ Dependency Injection ให้กับ Android Framework Component ต่างๆ
Dependency Injection?
Dependency Injection หรือที่เรียกกันแบบย่อๆว่า DI เป็นเทคนิคการเขียนโค้ดแบบหนึ่งที่จะช่วยให้โค้ดมีความยืดหยุ่นมากขึ้น ไม่ผูกกับคลาสด้วยกันจนเกินไป ซึ่งเป็นหัวใจสำคัญสำหรับการเขียนโค้ดที่มีโครงสร้างขนาดใหญ่ที่จะต้องมีการดูแลตลอดเวลา และที่ขาดไปไม่ได้ก็คือการเขียนเทสให้กับโค้ดเหล่านี้นั่นเองเพื่อให้เห็นภาพได้ง่ายขึ้นว่าทำไม DI ถึงทำให้การเขียนโค้ดนั้นดีขึ้น ให้ลองดูโค้ดตัวอย่างนี้ก่อน
// MainRepository.kt
class MainRepository {
private val userPreferenceManager = UserPreferenceManager()
fun saveUser(user: User) {
userPreferenceManager.saveUserId(user.id)
userPreferenceManager.saveUserName(user.name)
}
}
จะเห็นว่าคลาส UserPreferenceManager ถูกสร้างขึ้นใน MainRepository เพื่อทำงานบางอย่าง นั่นหมายความว่า UserPreferenceManager ผูกการทำงานทั้งหมดไว้ใน MainRepository
โค้ดดังกล่าวดูเหมือนจะไม่มีปัญหาอะไร จนกระทั่ง "อยากเขียนเทสให้กับ MainRepository" เพื่อเช็คให้มั่นใจว่าเวลาที่คำสั่ง saveUser(...) ทำงาน จะไปเรียกคำสั่งของ UserPreferenceManager อย่างถูกต้อง โดยที่ไม่ให้ข้อมูลที่เขียนเทสนั้นบันทึกลงในเครื่องจริงๆ (ก็จะเขียน Unit Test อ่ะ)
นั่นล่ะ ปัญหาจะเกิดขึ้นทันที มันทำแบบนั้นไม่ได้ไงล่ะ เพราะว่าดันเอา UserPreferenceManager ไปยัดไว้ใน MainRepository ตรงๆแบบนี้ (และจะเลวร้ายไปใหญ่ถ้าทำเป็น Static Class หรือ Static Method)
ดังนั้นตาม Concept ของ Dependency Inject คือแทนที่จะสร้าง UserPreferenceManager ข้างในนี้โดยตรง ก็ทำให้มันสามารถกำหนดจากข้างนอกดีกว่า
เจ้าของบล็อกก็เลยเปลี่ยนใหม่ให้ UserPreferenceManager ถูกส่งเข้ามาผ่านทาง Constructor แทน
// MainRepository.kt
class MainRepository(var userPreferenceManager: UserPreferenceManager) {
fun saveUser(user: User) {
userPreferenceManager.saveUserId(user.id)
userPreferenceManager.saveUserName(user.name)
}
}
เพียงแค่นี้เจ้าของบล็อกก็สามารถสร้าง UserPreferenceManager แบบไหนก็ได้แล้ว จากนั้นค่อยโยนเข้ามาให้ MainRepository เอาไปใช้งาน ทำให้เวลาเขียนเทสเจ้าของบล็อกสามารถ Mock หรือ Spy ให้กับ UserPreferenceManager ได้อย่างง่ายดาย โดยไม่ต้องแก้ไขโค้ดใน MainRepository เพื่อให้เทสได้เลยซักนิด
นั่นล่ะครับ Concept อันเรียบง่ายแต่ดูสวยงามของ Dependency Injection
แต่โลกของ Dependency Injection ก็ไม่ได้สวยหรูขนาดนั้น
ด้วย Concept ที่ต้องพยายามโยนทุกอย่างจากภายนอกเข้ามาแทนที่จะสร้างขึ้นจากข้างในโดยตรง ก็เลย...// MainRepository.kt
class MainRepository(var userPreferenceManager: UserPreferenceManager,
var networkManager: NetworkManager,
var addressManager: AddressManager,
var userUtil: UserUtil) {
...
}
// UserPreferenceManager.kt
class UserPreferenceManager(var context: Context) {
...
}
// NetworkManager.kt
class NetworkManager(var context: Context,
var serviceUtil: ServiceUtil) {
...
}
// ServiceUtil.kt
class ServiceUtil(var context: Context) {
...
}
// AddressManager.kt
class AddressManager(var locationManager: LocationManager) {
...
}
// UserUtil.kt
class UserUtil() {
...
}
ยิ่งมีคลาสเยอะเท่าไรก็ยิ่งต้องทำให้มันโยนเข้ามาได้มากเท่านั้น
ลองเดาดูสิว่าภาระทั้งหมดจะอยู่ที่ไหน?
อยู่ที่ต้นทางยังไงล่ะ เพราะต้นทางต้องสร้างทุกอย่างที่จำเป็นขึ้นมาเพื่อโยนเข้าไปในแต่ละคลาสตาม Concept ของ Dependency Injection
// MainActivity.kt
class MainActivity : AppCompatActivity() {
private var repository: MainRepository
init {
val locationManager = getSystemService(Context.LOCATION_SERVICE) as LocationManager
val userUtil = UserUtil()
val serviceUtil = ServiceUtil(this)
val networkManager = NetworkManager(this, serviceUtil)
val addressManager = AddressManager(locationManager)
val userPreferenceManager = UserPreferenceManager(this)
repository = MainRepository(userPreferenceManager,
networkManager,
addressManager,
userUtil)
}
...
}
ถ้าโปรเจคที่มีไม่ใหญ่มาก มีความซับซ้อนน้อยๆก็คงไม่เป็นอะไร แต่ถ้ามันเยอะมากจนเกินไป ก็ทำให้ท้อได้เหมือนกันนะ จนสุดท้ายก็แอบคิดว่าจะเขียนทำ Dependency Injection ไปทำไมเนี่ย เสียเวลาไม่ใช่น้อยๆ
และนั่นก็ทำให้ Dagger 2 ถือกำเนิดขึ้นมา
เปลี่ยนชีวิตให้ดีขึ้นด้วย Dagger 2
Dagger 2 นั้นเป็น Library ที่จะมาเปลี่ยนโลกของ Dependency Injection ให้หล่อขึ้นอย่างทันตาเห็น จากเดิมที่ผู้คนต้องทรมานและร้องอิดโอยเพราะต้องนั่งหลังขดหลังแข็งเขียนโค้ดเยอะแยะไปหมดโดย Dagger 2 จะช่วยจัดการโค้ดสำหรับ Dependency Injection เพื่อให้นักพัฒนาเขียนโค้ดที่ไม่จำเป็นน้อยลง เหลือแค่คำสั่งสั้นๆของ Dagger 2 ที่เอาไว้เรียกใช้งาน
ก่อนจะอธิบายว่า Dagger 2 ใช้งานยังไง ให้ดูผลลัพธ์จากตัวอย่างก่อนหน้านี้เมื่อใช้ Dagger 2 ก่อนเลยดีกว่า
// MainActivity.kt
class MainActivity : AppCompatActivity() {
@Inject
lateinit var repository: MainRepository
...
}
เฮ้ย!! เหลือแค่เนี้ย!?
ใช่ครับ นั่นล่ะ เวทมนต์ของ Dagger 2
Dagger 2 + Android
เดิมทีนั้น Dagger 2 เป็น Java Library ที่สร้างขึ้นมาเพื่อให้สามารถทำ Dependency Injection ใน Java ได้ง่ายขึ้น ซึ่ง Android ก็ได้ผลประโยชน์ไปด้วย แต่ทว่าด้วยความเป็น Android จึงทำให้มีโค้ดบางส่วนที่จำเป็นต้องเขียนไว้อย่างน่าเกลียดเพื่อให้สามารถใช้งาน Dagger 2 ได้แต่สำหรับเวอร์ชัน 2.10 ขึ้นไป ทีมพัฒนาของ Dagger 2 ก็ได้เพิ่มความสามารถเพื่อให้รองรับกับคลาสหลักๆของ Android แล้ว ดังนั้นโค้ดจะสวยมากขึ้นและมีคำสั่งบางส่วนที่เปลี่ยนแปลงด้วยเช่นกัน ดังนั้นในบทความนี้จะนำเสนอ Dagger 2 ในรูปแบบใหม่ที่เพิ่มโค้ดเพื่อให้รองรับกับ Android แล้วนะจ๊ะ