16 July 2018

Dagger 2 in Android [Part 1] - Dependency Injection แบบหล่อๆด้วย Dagger 2

Updated on


        ในที่สุดก็ถึงเวลาเขียนบทความของ 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 แล้วนะจ๊ะ

สรุป 

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