Dependency Injection: คืออะไร, การฉีดพึ่งพาใน iOS และ Android

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

การฉีดพึ่งพา (DI) เป็นเทคนิคที่ออบเจ็กต์ได้รับการพึ่งพาจากภายนอกแทนที่จะสร้างขึ้นเอง DI เป็นการนำหลักการ IoC (Inversion of Control) มาใช้และเป็นพื้นฐานของ Dagger, Hilt และ Swinject การฉีดพึ่งพาช่วยลดการเชื่อมโยงของโค้ด ทำให้การทดสอบง่ายขึ้น และทำให้สถาปัตยกรรมมีความยืดหยุ่น บน Android, DI เป็นมาตรฐานผ่าน Dagger Hilt ของ Google บน iOS ผ่าน Swinject หรือการฉีดแบบด้วยตนเอง เรียนรู้เพิ่มเติมใน คู่มือ DI สำหรับ Android

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

  • การฉีดพึ่งพา — การพึ่งพาจะถูกส่งไปยังออบเจ็กต์จากภายนอก ไม่ได้สร้างขึ้นภายใน
  • Inversion of Control — DI นำหลักการ IoC มาใช้ การควบคุมจะถูกโอนไปยังคอนเทนเนอร์
  • Dagger Hilt — มาตรฐาน DI สำหรับ Android ที่สร้างจาก Dagger ของ Google
  • Swinject — เฟรมเวิร์ก DI ยอดนิยมสำหรับ iOS และ Swift
  • ลดการเชื่อมโยง — คลาสขึ้นอยู่กับนามธรรม ไม่ใช่การนำไปใช้ที่เป็นรูปธรรม

Dependency Injection คืออะไร: แก่นแท้และประเภทของ DI

การฉีดพึ่งพา เป็นเทคนิคที่ออบเจ็กต์ได้รับการพึ่งพา (บริการ, พื้นที่เก็บข้อมูล, การกำหนดค่า) ผ่านคอนสตรัคเตอร์, เซตเตอร์ หรืออินเทอร์เฟซ แทนที่จะสร้างขึ้นเองด้วย new เป้าหมายของ DI คือลดการเชื่อมโยงระหว่างคลาส หากคลาสสร้างการพึ่งพาด้วยตัวเอง มันจะถูกผูกติดกับการนำไปใช้เฉพาะอย่างแน่นหนา ทำให้การทดสอบและการแก้ไขทำได้ยาก ด้วย DI คลาสทำงานกับนามธรรม (โปรโตคอล/อินเทอร์เฟซ) และการนำไปใช้ที่เป็นรูปธรรมจะถูกจัดหาจากภายนอก

สามวิธีในการฉีด — การฉีดผ่านคอนสตรัคเตอร์ (ผ่าน init/constructor), การฉีดผ่านเซตเตอร์ (ผ่านคุณสมบัติ/setter), การฉีดผ่านอินเทอร์เฟซ (ผ่านเมธอดของอินเทอร์เฟซ) การฉีดผ่านคอนสตรัคเตอร์เป็นวิธีที่ต้องการ: การพึ่งพามองเห็นได้ชัดเจนในลายเซ็นและออบเจ็กต์ถูกสร้างขึ้นในสถานะที่ถูกต้องเสมอ การฉีดผ่านเซตเตอร์ใช้สำหรับการพึ่งพาทางเลือกที่มีค่าเริ่มต้น การฉีดผ่านอินเทอร์เฟซพบได้ยาก ส่วนใหญ่ใช้สำหรับคอนเทนเนอร์ DI

ประเภท DIวิธีเมื่อใดควรใช้ตัวอย่าง
คอนสตรัคเตอร์พารามิเตอร์ตัวเริ่มต้นการพึ่งพาที่จำเป็นinit(service: ServiceProtocol)
คุณสมบัติคุณสมบัติของคลาสการพึ่งพาทางเลือกvar service: ServiceProtocol?
เมธอดพารามิเตอร์ของเมธอดการพึ่งพาชั่วคราวfunc doWork(with service: Service)

คอนเทนเนอร์ DI — ไลบรารีที่จัดการการสร้างและวงจรชีวิตของการพึ่งพา คอนเทนเนอร์ประกอบด้วยการลงทะเบียนประเภท (แต่ละประเภทนามธรรมจับคู่กับการนำไปใช้ที่เป็นรูปธรรม) และโรงงานสำหรับสร้างออบเจ็กต์ที่มีการพึ่งพาที่ถูกแก้ไขแล้ว บน Android — Dagger/Hilt, บน iOS — Swinject, Needle, Dip คอนเทนเนอร์สามารถจัดการขอบเขต (Scope): ซิงเกิลตัน (หนึ่งอินสแตนซ์ต่อแอปพลิเคชัน), ขอบเขตฟีเจอร์ (ต่อหน้าจอ) หรือออบเจ็กต์ใหม่ในแต่ละคำขอ

Dagger Hilt: DI สำหรับ Android พร้อมการสร้างโค้ด

Dagger Hilt เป็นตัวหุ้มรอบ Dagger ของ Google ไลบรารี DI มาตรฐานสำหรับ Android Hilt ทำให้ Dagger ง่ายขึ้น: ลบการสร้างคอมโพเนนต์ด้วยตนเอง เพิ่ม @HiltAndroidApp, @AndroidEntryPoint และ @Module Hilt ผสานรวมกับวงจรชีวิตของ Android: ViewModel, Activity, Fragment, Service, BroadcastReceiver สามารถรับการพึ่งพาผ่านคำอธิบายประกอบ การสร้างโค้ดเกิดขึ้นในเวลาคอมไพล์ — Dagger สร้างการนำไปใช้ของคอมโพเนนต์ ซึ่งให้ค่าใช้จ่าย runtime เป็นศูนย์

kotlin
// คลาสแอปพลิเคชัน
@HiltAndroidApp
class MyApp : Application()

// โมดูล — กำหนดวิธีสร้างการพึ่งพา
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
    @Provides
    @Singleton
    fun provideOkHttpClient(): OkHttpClient = OkHttpClient.Builder().build()

    @Provides
    @Singleton
    fun provideApiService(client: OkHttpClient): ApiService {
        return Retrofit.Builder()
            .baseUrl("https://api.example.com")
            .client(client)
            .build()
            .create(ApiService::class.java)
    }
}

// ViewModel รับการพึ่งพาผ่านคอนสตรัคเตอร์
@HiltViewModel
class MainViewModel @Inject constructor(
    private val apiService: ApiService
) : ViewModel() {
    private val _state = MutableStateFlow(MainState.Loading)
    val state: StateFlow<MainState> = _state.asStateFlow()

    fun loadData() {
        viewModelScope.launch {
            _state.value = MainState.Success(apiService.getData())
        }
    }
}

// Activity — @AndroidEntryPoint เปิดใช้งาน DI
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    private val viewModel: MainViewModel by viewModels()
}

คอมโพเนนต์และขอบเขตของ Dagger — @Singleton (ทั้งแอปพลิเคชัน), @ActivityScoped (ต่อ Activity), @FragmentScoped (ต่อ Fragment), @ViewModelScoped (ต่อ ViewModel) การเลือกขอบเขตกำหนดอายุของออบเจ็กต์ @Singleton — หนึ่งอินสแตนซ์ต่อกระบวนการ เหมาะสำหรับ OkHttpClient และฐานข้อมูล @ActivityScoped — ออบเจ็กต์มีอายุตราบเท่าที่ Activity มีอายุ สำหรับการพึ่งพาระดับหน้าจอ @ViewModelScoped — ใหม่ใน Hilt 2.45+ ออบเจ็กต์มีอายุตราบเท่าที่ ViewModel มีอายุ สะดวกสำหรับขอบเขต coroutine

Swinject: DI สำหรับ iOS ด้วย Swift

Swinject เป็นเฟรมเวิร์ก DI โอเพนซอร์สยอดนิยมสำหรับ iOS Swinject มี Container, Assemblies และขอบเขตต่างๆ แตกต่างจาก Dagger, Swinject ทำงานในรันไทม์ — การพึ่งพาจะถูกแก้ไขแบบไดนามิกโดยไม่ต้องสร้างโค้ด สิ่งนี้ทำให้ Swinject ตั้งค่าได้ง่ายขึ้น แต่การดีบักทำได้ยากกว่า: ข้อผิดพลาดการพึ่งพาที่ยังไม่ถูกแก้ไขจะปรากฏเฉพาะในรันไทม์ Swinject รองรับการฉีดผ่านคอนสตรัคเตอร์ การฉีดผ่านคุณสมบัติ และการฉีดผ่านเมธอด

swift
import Swinject

// Assembly — กลุ่มของการลงทะเบียน
class NetworkAssembly: Assembly {
    func assemble(container: Container) {
        container.register(NetworkServiceProtocol.self) { _ in
            NetworkService()
        }.inObjectScope(.container) // singleton

        container.register(UserRepositoryProtocol.self) { r in
            UserRepository(
                networkService: r.resolve(NetworkServiceProtocol.self)!
            )
        }
    }
}

// ViewModel ผ่านการฉีดคอนสตรัคเตอร์
class ProfileViewModel: ObservableObject {
    private let repository: UserRepositoryProtocol

    init(repository: UserRepositoryProtocol) {
        self.repository = repository
    }

    @Published var user: User?
    func loadUser() {
        repository.fetchUser { [weak self] user in
            self?.user = user
        }
    }
}

// การตั้งค่า DI ใน AppDelegate หรือ App
let assembler = Assembler([NetworkAssembly(), ServiceAssembly()])
let viewModel = assembler.resolver.resolve(ProfileViewModel.self)!

// การฉีดคุณสมบัติสำหรับ UIKit ViewController
container.register(UserViewController.self) { r in
    let vc = UserViewController()
    vc.viewModel = r.resolve(ProfileViewModel.self)
    return vc
}

ขอบเขตของ Swinject — .transient (ออบเจ็กต์ใหม่ทุกครั้ง), .container (ซิงเกิลตันต่อคอนเทนเนอร์), .graph (ค่าเริ่มต้น — ออบเจ็กต์ถูกแชร์ภายในกราฟการพึ่งพาเดียว) สำหรับแอปพลิเคชัน iOS, .container และ .transient ก็เพียงพอ Swinject ยังรองรับ Assembler — การจัดกลุ่ม Assembly สำหรับสถาปัตยกรรมแบบโมดูลาร์ สำหรับการทดสอบ Assembly จะถูกแทนที่ด้วย MockAssembly ซึ่งช่วยให้สามารถแทนที่การพึ่งพาได้โดยไม่ต้องเปลี่ยนโค้ดการผลิต

การเปรียบเทียบ DI กับ Service Locator และการฉีดแบบด้วยตนเอง

DI vs Service Locator — ทั้งสองแพทเทิร์นแก้ปัญหาการจัดการการพึ่งพา แต่ต่างกัน DI ฉีดการพึ่งพาเข้าไปในออบเจ็กต์ Service Locator จัดเตรียมทะเบียนส่วนกลางที่ออบเจ็กต์ขอการพึ่งพาด้วยตัวเอง DI ประกาศการพึ่งพาอย่างชัดเจนผ่านคอนสตรัคเตอร์ (หรือเซตเตอร์) Service Locator ซ่อนการพึ่งพา — การพึ่งพาจะถูกขอภายในเมธอด ทำให้ลายเซ็นมีข้อมูลน้อยลง DI ทดสอบได้ง่ายกว่า: เพียงแค่ส่ง mock ไปยังคอนสตรัคเตอร์ Service Locator ต้องตั้งค่าทะเบียนส่วนกลางสำหรับการทดสอบแต่ละครั้ง

คุณลักษณะการฉีดพึ่งพาService Locatorการฉีดแบบด้วยตนเอง
ความชัดเจนของการพึ่งพาในคอนสตรัคเตอร์ซ่อนในเนื้อหาของเมธอดชัดเจน
การทดสอบMock ในคอนสตรัคเตอร์การตั้งค่า LocatorMock ในคอนสตรัคเตอร์
ความซับซ้อนในการตั้งค่าต้องการคอนเทนเนอร์ DIทะเบียนส่วนกลางการสร้างด้วยตนเอง
ค่าใช้จ่าย runtimeDagger — เวลาคอมไพล์การค้นหาใน runtimeไม่มี

DI vs การฉีดแบบด้วยตนเอง — หากไม่มีคอนเทนเนอร์ DI การพึ่งพาจะถูกสร้างขึ้นด้วยตนเองในโรงงานหรือ AppDelegate สำหรับ 5-10 คลาส การฉีดแบบด้วยตนเองง่ายกว่า — ไม่จำเป็นต้องเรียนรู้ Dagger หรือ Swinject สำหรับ 50+ คลาส การฉีดแบบด้วยตนเองกลายเป็นปัญหา: คอนสตรัคเตอร์ที่มี 5-6 พารามิเตอร์ ลำดับการสร้างที่ซับซ้อน การทำซ้ำโค้ด คอนเทนเนอร์ DI ทำให้กระบวนการเหล่านี้เป็นอัตโนมัติและให้การจัดการวงจรชีวิตที่ชัดเจน การฉีดแบบด้วยตนเองโดยไม่มีคอนเทนเนอร์เป็นตัวเลือกที่ดีสำหรับโปรเจกต์ขนาดเล็กและต้นแบบ

แนวทางปฏิบัติที่ดีที่สุดสำหรับการฉีดพึ่งพา

การฉีดผ่านคอนสตรัคเตอร์ — มาตรฐาน ใช้การฉีดผ่านคอนสตรัคเตอร์เสมอสำหรับการพึ่งพาที่จำเป็น สิ่งนี้ทำให้การพึ่งพาชัดเจนและออบเจ็กต์พร้อมทำงานเสมอ การฉีดผ่านเซตเตอร์ — เฉพาะสำหรับการพึ่งพาทางเลือก (เช่น delegate หรือ listener) การฉีดผ่านอินเทอร์เฟซ — อย่าใช้เว้นแต่คุณจะเขียนไลบรารี DI ของคุณเอง การฉีดผ่านคอนสตรัคเตอร์เป็นวิธีเดียวที่จะรับประกันว่าออบเจ็กต์ถูกสร้างขึ้นในสถานะที่ถูกต้อง

หนึ่งคลาส — หนึ่งความรับผิดชอบ หากคอนสตรัคเตอร์ของคลาสต้องการ 5+ พารามิเตอร์ คลาสนั้นอาจละเมิดหลักการความรับผิดชอบเดียว แบ่งคลาสออกเป็นหลายคลาสที่มีการพึ่งพาน้อยลง สัญญาณ: หากคุณกำลังเขียนคลาส ServiceManager ที่มี 6 บริการต่างกัน — นี่คือแอนตี้แพทเทิร์น God Object แยกตรรกะทางธุรกิจออกเป็น Use Cases (Interactors) โดยแต่ละอันมีการพึ่งพา 1-2 รายการ

kotlin
// ❌ ไม่ดี: 6 การพึ่งพา — God Object
class ProfileViewModel @Inject constructor(
    private val api: ApiService,
    private val db: Database,
    private val analytics: Analytics,
    private val prefs: Preferences,
    private val location: LocationProvider,
    private val notification: NotificationManager
)

// ✅ ดี: Use Cases ที่มีการพึ่งพา 1-2 รายการ
class ProfileViewModel @Inject constructor(
    private val loadProfileUseCase: LoadProfileUseCase,
    private val trackAnalyticsUseCase: TrackAnalyticsUseCase
)

ขอบเขตและวงจรชีวิต — เลือกขอบเขตที่ถูกต้องสำหรับการพึ่งพาแต่ละรายการ ซิงเกิลตัน: OkHttpClient, ฐานข้อมูล, SharedPreferences ขอบเขตฟีเจอร์: พื้นที่เก็บข้อมูล, Use Cases (หากไม่มีสถานะ) Transient: Value Objects, DateFormatter, ตัวแยกวิเคราะห์ ข้อผิดพลาดของขอบเขตเป็นปัญหาที่พบบ่อย: ซิงเกิลตันที่เก็บสถานะหน้าจอทำให้เกิดการรั่วไหล ใน Android, @ActivityScoped ของ Hilt แก้ปัญหานี้ ใน Swinject ให้ใช้ .container ด้วยความระมัดระวัง

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

ทำไมต้องใช้ DI ในเมื่อฉันสามารถใช้ new ได้?

new สร้างการเชื่อมโยงที่แน่นหนาระหว่างคลาส — คุณไม่สามารถเปลี่ยนการนำไปใช้ได้โดยไม่แก้ไขโค้ด การทดสอบทำได้ยาก: คุณไม่สามารถฉีด mock แทนบริการจริงได้ SRP ถูกละเมิด: คลาสรับผิดชอบทั้งตรรกะทางธุรกิจและการสร้างการพึ่งพา DI แก้ปัญหาเหล่านี้โดยการฉีดการพึ่งพาจากภายนอกและการทำงานกับนามธรรม

Dagger Hilt หรือ Koin — เลือกอะไรสำหรับ Android?

Dagger Hilt เป็นมาตรฐานจาก Google, DI แบบเวลาคอมไพล์พร้อมการสร้างโค้ด, ประสิทธิภาพที่ดีกว่าและการผสานรวมกับ Jetpack Koin เป็น DI แบบรันไทม์, ตั้งค่าง่ายกว่า แต่ช้ากว่าและมีข้อผิดพลาดในรันไทม์ เลือก Hilt สำหรับโปรเจกต์การผลิต Koin เหมาะสำหรับต้นแบบและแอปพลิเคชันขนาดเล็ก

Swinject เป็นตัวเลือก DI เพียงอย่างเดียวสำหรับ iOS หรือไม่?

ไม่ สำหรับ iOS มี: Swinject (รันไทม์, เป็นที่นิยม), Needle (เวลาคอมไพล์จาก Uber), Dip (น้ำหนักเบา), Weaver (ที่ใช้ Sourcery) Apple ไม่มีคอนเทนเนอร์ DI ในตัว แต่การฉีดแบบด้วยตนเองผ่าน init เป็นแนวทางปฏิบัติมาตรฐาน สำหรับ SwiftUI, DI แบบด้วยตนเองผ่าน Environment หรือ @StateObject โดยไม่มีไลบรารีภายนอกมักจะเพียงพอ

สามารถใช้ DI โดยไม่มีเฟรมเวิร์กได้หรือไม่?

ได้ การฉีดแบบด้วยตนเองผ่านคอนสตรัคเตอร์คือ DI ที่ไม่มีเฟรมเวิร์ก Service Locator เป็นทางเลือกที่ไม่มีเฟรมเวิร์ก โรงงานและ Factory Method ก็เป็นรูปแบบของ DI เช่นกัน เฟรมเวิร์ก (Dagger, Swinject) ทำให้การลงทะเบียนตามปกติและการแก้ไขการพึ่งพาเป็นอัตโนมัติ แต่สำหรับ 10-20 คลาส, DI แบบด้วยตนเองก็เพียงพอ

DI เป็นแพทเทิร์นหรือหลักการ?

DI เป็นเทคนิค (แม่แบบ) ที่นำหลักการ Inversion of Control มาใช้ แตกต่างจากแพทเทิร์น GoF, DI ไม่มีโครงสร้างที่ตายตัวของ 3-4 คลาส DI เป็นวิธีการจัดระเบียบการพึ่งพา ไม่ใช่แพทเทิร์นการออกแบบ คอนเทนเนอร์ DI (Dagger, Swinject) เป็นเฟรมเวิร์กที่ทำให้เทคนิคนี้เป็นอัตโนมัติ

สรุป

  • DI — เทคนิคการฉีดการพึ่งพาจากภายนอกผ่านคอนสตรัคเตอร์, เซตเตอร์ หรือเมธอด
  • Dagger Hilt — มาตรฐาน DI สำหรับ Android พร้อมการสร้างโค้ดเวลาคอมไพล์และ @HiltViewModel
  • Swinject — DI แบบรันไทม์สำหรับ iOS พร้อม Container, Assembly และขอบเขต
  • การฉีดผ่านคอนสตรัคเตอร์ — วิธีที่ต้องการสำหรับการพึ่งพาที่จำเป็น
  • ขอบเขต — ซิงเกิลตันสำหรับบริการไร้สถานะ, ขอบเขตฟีเจอร์สำหรับการพึ่งพระดับหน้าจอ
  • การทดสอบ — DI ทำให้การแทนที่การพึ่งพาด้วย mock ง่ายขึ้นโดยไม่ต้องเปลี่ยนโค้ด

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

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

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

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