DIP: พื้นฐาน การกลับด้านการพึ่งพาในการพัฒนา

ผู้แต่ง: IT Sectr เผยแพร่เมื่อ: 2026-05-12 เวลาอ่าน: 9 นาที

DIP (Dependency Inversion Principle) — หลักการ SOLID ข้อที่ห้าที่กำหนดกฎสำหรับการสร้างการพึ่งพาระหว่างโมดูล: โมดูลระดับสูงไม่ควรพึ่งพาโมดูลระดับต่ำ ทั้งคู่ควรพึ่งพาสิ่งที่เป็นนามธรรม สิ่งที่เป็นนามธรรมไม่ควรพึ่งพารายละเอียด — รายละเอียดควรพึ่งพาสิ่งที่เป็นนามธรรม หลักการนี้ ซึ่งอธิบายโดย Robert Martin ใน Clean Architecture (2017) เป็นพื้นฐานของสถาปัตยกรรมแบบเชื่อมโยงแบบหลวม ตามหนังสือเล่มนี้ หลักการกลับด้านการพึ่งพา ขจัดการเชื่อมโยงแบบแข็งระหว่างชั้นของแอปพลิเคชัน

ประเด็นสำคัญ

  • DIP — หลักการกลับด้านการพึ่งพา ข้อที่ห้าใน SOLID เกี่ยวกับขอบเขตทางสถาปัตยกรรม
  • โมดูลระดับสูง ไม่ควรนำเข้าโมดูลระดับต่ำ — เฉพาะสิ่งที่เป็นนามธรรมเท่านั้น
  • DIP ≠ DI: Dependency Inversion คือหลักการทางสถาปัตยกรรม Dependency Injection คือวิธีการนำไปปฏิบัติ
  • สิ่งที่เป็นนามธรรม เป็นของโมดูลระดับสูง ส่วนการนำไปใช้เป็นของโมดูลระดับต่ำ
  • DIP กลับลำดับชั้นการพึ่งพาแบบดั้งเดิมในสถาปัตยกรรมหลายชั้น

DIP (หลักการกลับด้านการพึ่งพา) คืออะไร?

DIP (Dependency Inversion Principle) คือหลักการกลับด้านการพึ่งพาที่กลับมุมมองดั้งเดิมเกี่ยวกับทิศทางการพึ่งพาระหว่างโมดูล โมดูลระดับสูง (ตรรกะทางธุรกิจ) ไม่ควรพึ่งพาโมดูลระดับต่ำ (ฐานข้อมูล เครือข่าย UI) โดยตรง แต่ทั้งสองระดับควรพึ่งพาสิ่งที่เป็นนามธรรมที่กำหนดในโมดูลระดับสูง

การกำหนดอย่างเป็นทางการของ DIP ประกอบด้วยกฎสองข้อ: A — โมดูลระดับสูงไม่ควรพึ่งพาโมดูลระดับต่ำ ทั้งคู่ควรพึ่งพาสิ่งที่เป็นนามธรรม B — สิ่งที่เป็นนามธรรมไม่ควรพึ่งพารายละเอียด รายละเอียดควรพึ่งพาสิ่งที่เป็นนามธรรม กฎข้อที่สองเป็นผลมาจากข้อแรก: หากสิ่งที่เป็นนามธรรมพึ่งพารายละเอียด มันก็ไม่สามารถเป็นพื้นฐานที่มั่นคงสำหรับโมดูลระดับสูงได้

หากไม่มี DIP สถาปัตยกรรมทั่วไปจะมีลักษณะดังนี้: BusinessLogic → DatabaseRepository — ตรรกะทางธุรกิจพึ่งพา repository ที่เป็นรูปธรรมโดยตรง เมื่อมี DIP: BusinessLogic → DatabaseServiceInterface ← DatabaseRepository BusinessLogic ไม่รู้เกี่ยวกับการมีอยู่ของ DatabaseRepository มันรู้เพียงอินเทอร์เฟส DatabaseService ซึ่งถูกนำไปใช้นอกตรรกะทางธุรกิจ

ทิศทางการพึ่งพาใน DIP

การกลับด้าน หมายถึงการไหลของการควบคุมและการไหลของการพึ่งพาไปในทิศทางตรงกันข้าม การไหลของการควบคุมจากบนลงล่าง: UI → ViewModel → UseCase → Repository การไหลของการพึ่งพาจากล่างขึ้นบน: Repository นำอินเทอร์เฟสที่กำหนดใน UseCase ไปใช้ Repository (ระดับต่ำ) พึ่งพา UseCase (ระดับสูง)

การกลับด้านนี้คือความแตกต่างที่สำคัญระหว่าง DIP และการแยกชั้นแบบธรรมดา ในสถาปัตยกรรมแบบชั้นดั้งเดิม แต่ละชั้นพึ่งพาชั้นด้านล่าง ในสถาปัตยกรรมที่มี DIP ทุกชั้นพึ่งพาสิ่งที่เป็นนามธรรม ในขณะที่การนำสิ่งที่เป็นนามธรรมเหล่านี้ไปใช้อยู่ในชั้นโครงสร้างพื้นฐาน ซึ่ง “เชื่อมต่อ” เข้ากับชั้นบนผ่านกลไก DI

หลักการกลับด้านการพึ่งพาทำงานอย่างไร

กลไก DIP ถูกนำไปใช้โดยการกำหนดสิ่งที่เป็นนามธรรมในโมดูลระดับสูงและนำไปใช้ในโมดูลระดับต่ำ โมดูลระดับสูงประกาศอินเทอร์เฟสสำหรับฟังก์ชันที่ต้องการ โมดูลระดับต่ำนำอินเทอร์เฟสนี้ไปใช้ การเชื่อมต่อ (wiring) เกิดขึ้นที่คอมโพซิชันรูทของแอปพลิเคชัน

กระบวนการนำ DIP เข้าสู่โค้ดที่มีอยู่: แยกอินเทอร์เฟสสำหรับโมดูลระดับต่ำ ย้ายอินเทอร์เฟสนี้ไปยังโมดูลระดับสูง (หรือชั้นนามธรรมที่แยกต่างหาก) เขียนการพึ่งพาของโมดูลระดับสูงใหม่เพื่อใช้อินเทอร์เฟส ทำให้โมดูลระดับต่ำนำอินเทอร์เฟสนี้ไปใช้ หลังจากขั้นตอนเหล่านี้ ทิศทางการพึ่งพาได้ถูกกลับด้านแล้ว

DIP ต้องมีกลไก คอมโพซิชันรูท — จุดในแอปพลิเคชันที่สร้างการพึ่งพาทั้งหมดและเชื่อมต่อซึ่งกันและกัน ใน Android คือ Application.get() หรือคอมโพเนนต์ Hilt ใน iOS — AppDelegate หรือ SceneDelegate คอมโพซิชันรูทเป็นที่เดียวที่โค้ดรู้เกี่ยวกับการนำไปใช้ที่เป็นรูปธรรม

การแยกชั้นผ่าน DIP

DIP สร้างขอบเขตทางสถาปัตยกรรม ระหว่างชั้นของแอปพลิเคชัน เมื่อ ViewModel พึ่งพาอินเทอร์เฟส UserRepository จะเกิดขอบเขตระหว่างชั้นนำเสนอและชั้นโดเมน: ViewModel (ชั้นนำเสนอ) ไม่รู้ว่าข้อมูลมาจากไหน ขอบเขตนี้ช่วยให้เปลี่ยนการนำ UserRepository ไปใช้ (Room → REST → Mock) ได้โดยไม่กระทบต่อ ViewModel ยิ่งมีขอบเขตดังกล่าวมากเท่าใด แอปพลิเคชันก็ยิ่งทนทานต่อการเปลี่ยนแปลงของเฟรมเวิร์กและไลบรารีมากขึ้นเท่านั้น

ในสถาปัตยกรรม Android ที่ Google แนะนำ DIP ถูกนำไปใช้ผ่าน UseCase ที่อยู่ในชั้นโดเมนและพึ่งพาอินเทอร์เฟส Repository RepositoryImpl อยู่ในชั้นข้อมูลและนำอินเทอร์เฟสเหล่านี้ไปใช้ ชั้นนำเสนอ (ViewModel) พึ่งพา UseCase ทิศทางการพึ่งพาไปจากชั้นนำเสนอสู่โดเมน จากโดเมนสู่ข้อมูล — แต่ไม่มีชั้นใดรู้เกี่ยวกับการนำไปใช้ที่เป็นรูปธรรมของอีกชั้นหนึ่ง

ความแตกต่างระหว่าง DIP และ DI (Dependency Injection)

DIP และ DI มักถูกสับสน แต่เป็นแนวคิดที่แตกต่างกัน DIP เป็นหลักการทางสถาปัตยกรรม (จะทำอะไร: พึ่งพาสิ่งที่เป็นนามธรรม) DI เป็นรูปแบบการนำไปใช้ (จะทำอย่างไร: ส่งการพึ่งพาผ่านคอนสตรัคเตอร์) DIP ตอบคำถาม “โมดูลควรยึดถืออะไร?” DI ตอบคำถาม “ออบเจ็กต์ได้รับการพึ่งพาของมันอย่างไร?”

Dependency Injection เป็นวิธีการฉีดการพึ่งพาเข้าไปในออบเจ็กต์ผ่านคอนสตรัคเตอร์ เมธอด หรือพรอพเพอร์ตี้ เมื่อคลาส Kotlin ได้รับอินเทอร์เฟส Repository ผ่านคอนสตรัคเตอร์ — นั่นคือ DI และความจริงที่ว่าคลาส ViewModel พึ่งพาอินเทอร์เฟส Repository แทนที่จะพึ่งพาการนำ RoomRepository ไปใช้ที่เป็นรูปธรรม — นั่นคือ DIP DI คือเครื่องมือ DIP คือเป้าหมาย

คุณสามารถปฏิบัติตาม DIP โดยไม่มีเฟรมเวิร์ก DI: การเชื่อมต่อการพึ่งพาด้วยตนเองในคอมโพซิชันรูทก็คือ DI (DI แบบแมนนวล) คุณสามารถใช้เฟรมเวิร์ก DI (Dagger, Hilt, Koin) ละเมิด DIP: หาก ViewModel สร้างออบเจ็กต์ Repository โดยตรงผ่าน new() — DIP ถูกละเมิด แม้ว่าเฟรมเวิร์กจะถูกติดตั้งแล้วก็ตาม DIP คือการตัดสินใจทางสถาปัตยกรรม DI คือรายละเอียดทางเทคนิค

ตัวอย่าง DIP ในการพัฒนาแอปมือถือ

ลองดู ตัวอย่าง Android ของการนำ DIP ไปใช้กับชั้นข้อมูล หากไม่มี DIP ViewModel จะสร้าง RoomDatabase และ DAO โดยตรง หากมี DIP — ViewModel พึ่งพาอินเทอร์เฟส UserRepository และการนำ RoomUserRepository ไปใช้ที่เป็นรูปธรรมจะถูกจัดเตรียมจากภายนอก

kotlin
// การทำให้เป็นนามธรรมเป็นของชั้นโดเมน (ระดับสูง)
interface UserRepository {
    fun getUser(id: Int): User
}

// ชั้นโดเมนพึ่งพาเฉพาะการทำให้เป็นนามธรรม
class GetUserUseCase(
    private val repo: UserRepository
) {
    fun execute(id: Int): User = repo.getUser(id)
}

// การนำไปใช้ในชั้นข้อมูลพึ่งพาการทำให้เป็นนามธรรมของชั้นโดเมน
class RoomUserRepository(
    private val dao: UserDao
) : UserRepository {
    override fun getUser(id: Int): User {
        return dao.getById(id)
    }
}

// คอมโพซิชันรูท
class AppModule {
    fun provideUserRepository(dao: UserDao): UserRepository {
        return RoomUserRepository(dao)
    }
}

ตัวอย่าง iOS กับ Application Coordinator และโพรโทคอลสำหรับการนำทาง:

swift
// การทำให้เป็นนามธรรมการนำทางในชั้นโดเมน
protocol AuthNavigation {
    func navigateToHome()
    func navigateToLogin()
}

// ViewModel พึ่งพาการทำให้เป็นนามธรรม ไม่พึ่งพา UIKit
final class AuthViewModel {
    private let navigation: AuthNavigation

    init(navigation: AuthNavigation) {
        self.navigation = navigation
    }

    func onLoginSuccess() {
        navigation.navigateToHome()
    }
}

// Coordinator (ชั้น UIKit) นำโพรโทคอลของชั้นโดเมนไปใช้
final class AppCoordinator: AuthNavigation {
    func navigateToHome() {
        // โค้ดการนำทาง UIKit
    }
    func navigateToLogin() {
        // โค้ดการนำทาง UIKit
    }
}

ประเด็นสำคัญ: AuthViewModel (โดเมน) ไม่รู้เกี่ยวกับการมีอยู่ของ AppCoordinator (UIKit) มันรู้เฉพาะโพรโทคอล AuthNavigation หากพรุ่งนี้ UIKit ถูกแทนที่ด้วย SwiftUI — AuthViewModel ไม่จำเป็นต้องเปลี่ยนแปลง DIP ทำให้ชั้นโดเมนเป็นอิสระจากเฟรมเวิร์กและไลบรารี UI

เครื่องมือสำหรับ DIP: Dagger, Hilt, Koin

Hilt เป็นเครื่องมือ DI มาตรฐานสำหรับ Android ที่ Google แนะนำ มันถูกรวมอยู่ใน Jetpack รองรับ ViewModel, Fragment, Service และคอมโพเนนต์ Android อื่น ๆ Hilt ทำให้การสร้างคอมโพซิชันรูทเป็นอัตโนมัติผ่านคำอธิบายประกอบ @Module, @Provides, @Inject การใช้ Hilt ไม่ได้รับประกันการปฏิบัติตาม DIP — อินเทอร์เฟส UserRepository ต้องถูกกำหนดในชั้นโดเมน ไม่ใช่ในชั้นข้อมูล

Koin เป็นเฟรมเวิร์ก DI แบบน้ำหนักเบาสำหรับ Kotlin โดยไม่ต้องสร้างโค้ดหรือประมวลผลคำอธิบายประกอบ DSL ของ Koin (module, single, factory) เรียนรู้ได้ง่ายกว่า แต่การตรวจสอบการพึ่งพาเกิดขึ้นที่รันไทม์แทนที่จะเป็นเวลาแปลโปรแกรม Koin เป็นที่นิยมในโครงการหลายแพลตฟอร์ม (KMP) เนื่องจากการรองรับ iOS

Dagger 2 เป็นรุ่นก่อนของ Hilt ยังคงใช้ในโครงการขนาดใหญ่ Dagger สร้างโค้ด DI ในเวลาแปลโปรแกรม ซึ่งให้ประสิทธิภาพสูงสุดและการวินิจฉัยข้อผิดพลาดในเวลาสร้าง Hilt สร้างขึ้นบน Dagger และมี API ที่เรียบง่ายขึ้น สำหรับโครงการใหม่ Google แนะนำ Hilt เป็นเฟรมเวิร์ก DI หลัก

การจัดระเบียบโมดูล DI ตามชั้น

โมดูล DI ควรสอดคล้องกับชั้นทางสถาปัตยกรรมและแบ่งออกเป็น DomainModule, DataModule, PresentationModule DomainModule ให้เฉพาะสิ่งที่เป็นนามธรรมและ UseCase DataModule ให้การนำไปใช้สำหรับสิ่งที่เป็นนามธรรม PresentationModule เชื่อมต่อ ViewModel กับ UseCase การจัดระเบียบนี้รับประกันว่า ชั้นโดเมนยังคงเป็นอิสระจากไลบรารีโครงสร้างพื้นฐาน

เมื่อย้ายระหว่างเฟรมเวิร์ก DI (เช่น จาก Koin ไป Hilt) โครงสร้างของ DomainModule จะไม่เปลี่ยนแปลง — เฉพาะวิธีการเชื่อมต่อใน DataModule และ PresentationModule เท่านั้นที่เปลี่ยนแปลง DIP รับประกันการแยกตรรกะโดเมน ในขณะที่เฟรมเวิร์ก DI เป็นกลไกการเชื่อมต่อทางเทคนิค

คำถามที่พบบ่อย

ควรใช้ DIP ทุกครั้งหรือไม่?

DIP จำเป็นที่ขอบเขตทางสถาปัตยกรรม — ระหว่างชั้นของแอปพลิเคชัน (โดเมน → ข้อมูล, ชั้นนำเสนอ → โดเมน) ภายในชั้นเดียว DIP อาจมากเกินไป ตัวอย่างเช่น คลาสยูทิลิตี้ StringFormatter ภายในชั้นโดเมนไม่ต้องการอินเทอร์เฟส — หากไม่มีเหตุผลที่จะแทนที่

DIP เหมือนกับการฉีดการพึ่งพาหรือไม่?

ไม่ DIP คือหลักการ: โมดูลควรพึ่งพาสิ่งที่เป็นนามธรรม DI คือรูปแบบ: ออบเจ็กต์ได้รับการพึ่งพาจากภายนอกแทนที่จะสร้างขึ้นเอง DI เป็นวิธีหนึ่งในการนำ DIP ไปใช้ แต่ DIP สามารถปฏิบัติตามได้โดยไม่ต้องมี DI (ผ่าน factory หรือ service locator) DI โดยไม่มี DIP เป็นไปได้แต่ไม่มีคุณค่าทางสถาปัตยกรรม

ควรกำหนดอินเทอร์เฟสสำหรับ DIP ที่ไหน?

อินเทอร์เฟสเป็นของโมดูลที่ใช้มัน ไม่ใช่ของโมดูลที่นำมันไปใช้ UserRepository ถูกประกาศในชั้นโดเมนและนำไปใช้ในชั้นข้อมูล นี่คือกฎสำคัญของ DIP: เจ้าของสิ่งที่เป็นนามธรรมคือผู้บริโภค ไม่ใช่ผู้ให้บริการการนำไปใช้

DIP ส่งผลต่อการทดสอบอย่างไร?

DIP ทำให้การทดสอบเป็นไปได้ บนชั้นที่แยกส่วน ViewModel ที่พึ่งพา UserRepository (อินเทอร์เฟส) สามารถทดสอบด้วยการนำไปใช้แบบจำลอง (mock) โดยไม่ต้องใช้ฐานข้อมูล หากไม่มี DIP ViewModel จะพึ่งพา RoomUserRepository และต้องตั้งค่าฐานข้อมูลสำหรับการทดสอบแต่ละครั้ง DIP + DI ให้การแยกโมดูลอย่างสมบูรณ์ระหว่างการทดสอบ

ควรเลือกเฟรมเวิร์ก DI ใดสำหรับ Android?

Hilt คือตัวเลือกมาตรฐานสำหรับโครงการ Android ที่ Google แนะนำ Koin เป็นทางเลือกสำหรับโครงการ Kotlin Multiplatform Dagger 2 สำหรับโครงการที่มีอยู่ซึ่งการย้ายไป Hilt ไม่สมเหตุสมผล การเลือกเฟรมเวิร์กไม่ได้ลบล้างความจำเป็นในการปฏิบัติตาม DIP ในระดับสถาปัตยกรรม

สรุป

  • DIP (Dependency Inversion Principle) — หลักการ SOLID ข้อที่ห้าเกี่ยวกับขอบเขตทางสถาปัตยกรรมผ่านสิ่งที่เป็นนามธรรม
  • โมดูลระดับสูง ไม่พึ่งพาโมดูลระดับต่ำ — ทั้งคู่พึ่งพาสิ่งที่เป็นนามธรรม
  • DIP ≠ DI: หลักการเทียบกับรูปแบบการนำไปใช้ DI คือเครื่องมือ DIP คือเป้าหมาย
  • สิ่งที่เป็นนามธรรมเป็นของผู้บริโภค (ชั้นโดเมน) ไม่ใช่ของผู้ให้บริการ (ชั้นข้อมูล)
  • คอมโพซิชันรูท — ที่เดียวในแอปพลิเคชันที่รวบรวมการพึ่งพาที่เป็นรูปธรรม
  • Hilt, Koin, Dagger — เครื่องมือ DI ที่ทำให้การเชื่อมต่อเป็นอัตโนมัติแต่ไม่ได้แทนที่การตัดสินใจทางสถาปัตยกรรมของ DIP
  • ชั้นโดเมน ที่สร้างตาม DIP ยังคงเป็นอิสระจากเฟรมเวิร์ก UI และไลบรารีโครงสร้างพื้นฐาน

เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร

IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ

ปรึกษาโครงการ

อ่านเพิ่มเติม