การฉีดพึ่งพา (DI) เป็นเทคนิคที่ออบเจ็กต์ได้รับการพึ่งพาจากภายนอกแทนที่จะสร้างขึ้นเอง DI เป็นการนำหลักการ IoC (Inversion of Control) มาใช้และเป็นพื้นฐานของ Dagger, Hilt และ Swinject การฉีดพึ่งพาช่วยลดการเชื่อมโยงของโค้ด ทำให้การทดสอบง่ายขึ้น และทำให้สถาปัตยกรรมมีความยืดหยุ่น บน Android, DI เป็นมาตรฐานผ่าน Dagger Hilt ของ Google บน iOS ผ่าน Swinject หรือการฉีดแบบด้วยตนเอง เรียนรู้เพิ่มเติมใน คู่มือ DI สำหรับ Android
ประเด็นสำคัญ
การฉีดพึ่งพา เป็นเทคนิคที่ออบเจ็กต์ได้รับการพึ่งพา (บริการ, พื้นที่เก็บข้อมูล, การกำหนดค่า) ผ่านคอนสตรัคเตอร์, เซตเตอร์ หรืออินเทอร์เฟซ แทนที่จะสร้างขึ้นเองด้วย 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 เป็นตัวหุ้มรอบ Dagger ของ Google ไลบรารี DI มาตรฐานสำหรับ Android Hilt ทำให้ Dagger ง่ายขึ้น: ลบการสร้างคอมโพเนนต์ด้วยตนเอง เพิ่ม @HiltAndroidApp, @AndroidEntryPoint และ @Module Hilt ผสานรวมกับวงจรชีวิตของ Android: ViewModel, Activity, Fragment, Service, BroadcastReceiver สามารถรับการพึ่งพาผ่านคำอธิบายประกอบ การสร้างโค้ดเกิดขึ้นในเวลาคอมไพล์ — Dagger สร้างการนำไปใช้ของคอมโพเนนต์ ซึ่งให้ค่าใช้จ่าย runtime เป็นศูนย์
// คลาสแอปพลิเคชัน
@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 Swinject มี Container, Assemblies และขอบเขตต่างๆ แตกต่างจาก Dagger, Swinject ทำงานในรันไทม์ — การพึ่งพาจะถูกแก้ไขแบบไดนามิกโดยไม่ต้องสร้างโค้ด สิ่งนี้ทำให้ Swinject ตั้งค่าได้ง่ายขึ้น แต่การดีบักทำได้ยากกว่า: ข้อผิดพลาดการพึ่งพาที่ยังไม่ถูกแก้ไขจะปรากฏเฉพาะในรันไทม์ Swinject รองรับการฉีดผ่านคอนสตรัคเตอร์ การฉีดผ่านคุณสมบัติ และการฉีดผ่านเมธอด
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 vs Service Locator — ทั้งสองแพทเทิร์นแก้ปัญหาการจัดการการพึ่งพา แต่ต่างกัน DI ฉีดการพึ่งพาเข้าไปในออบเจ็กต์ Service Locator จัดเตรียมทะเบียนส่วนกลางที่ออบเจ็กต์ขอการพึ่งพาด้วยตัวเอง DI ประกาศการพึ่งพาอย่างชัดเจนผ่านคอนสตรัคเตอร์ (หรือเซตเตอร์) Service Locator ซ่อนการพึ่งพา — การพึ่งพาจะถูกขอภายในเมธอด ทำให้ลายเซ็นมีข้อมูลน้อยลง DI ทดสอบได้ง่ายกว่า: เพียงแค่ส่ง mock ไปยังคอนสตรัคเตอร์ Service Locator ต้องตั้งค่าทะเบียนส่วนกลางสำหรับการทดสอบแต่ละครั้ง
| คุณลักษณะ | การฉีดพึ่งพา | Service Locator | การฉีดแบบด้วยตนเอง |
|---|---|---|---|
| ความชัดเจนของการพึ่งพา | ในคอนสตรัคเตอร์ | ซ่อนในเนื้อหาของเมธอด | ชัดเจน |
| การทดสอบ | Mock ในคอนสตรัคเตอร์ | การตั้งค่า Locator | Mock ในคอนสตรัคเตอร์ |
| ความซับซ้อนในการตั้งค่า | ต้องการคอนเทนเนอร์ DI | ทะเบียนส่วนกลาง | การสร้างด้วยตนเอง |
| ค่าใช้จ่าย runtime | Dagger — เวลาคอมไพล์ | การค้นหาใน 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 รายการ
// ❌ ไม่ดี: 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 ด้วยความระมัดระวัง
คำถามที่พบบ่อย
new สร้างการเชื่อมโยงที่แน่นหนาระหว่างคลาส — คุณไม่สามารถเปลี่ยนการนำไปใช้ได้โดยไม่แก้ไขโค้ด การทดสอบทำได้ยาก: คุณไม่สามารถฉีด mock แทนบริการจริงได้ SRP ถูกละเมิด: คลาสรับผิดชอบทั้งตรรกะทางธุรกิจและการสร้างการพึ่งพา DI แก้ปัญหาเหล่านี้โดยการฉีดการพึ่งพาจากภายนอกและการทำงานกับนามธรรม
Dagger Hilt เป็นมาตรฐานจาก Google, DI แบบเวลาคอมไพล์พร้อมการสร้างโค้ด, ประสิทธิภาพที่ดีกว่าและการผสานรวมกับ Jetpack Koin เป็น DI แบบรันไทม์, ตั้งค่าง่ายกว่า แต่ช้ากว่าและมีข้อผิดพลาดในรันไทม์ เลือก Hilt สำหรับโปรเจกต์การผลิต Koin เหมาะสำหรับต้นแบบและแอปพลิเคชันขนาดเล็ก
ไม่ สำหรับ iOS มี: Swinject (รันไทม์, เป็นที่นิยม), Needle (เวลาคอมไพล์จาก Uber), Dip (น้ำหนักเบา), Weaver (ที่ใช้ Sourcery) Apple ไม่มีคอนเทนเนอร์ DI ในตัว แต่การฉีดแบบด้วยตนเองผ่าน init เป็นแนวทางปฏิบัติมาตรฐาน สำหรับ SwiftUI, DI แบบด้วยตนเองผ่าน Environment หรือ @StateObject โดยไม่มีไลบรารีภายนอกมักจะเพียงพอ
ได้ การฉีดแบบด้วยตนเองผ่านคอนสตรัคเตอร์คือ DI ที่ไม่มีเฟรมเวิร์ก Service Locator เป็นทางเลือกที่ไม่มีเฟรมเวิร์ก โรงงานและ Factory Method ก็เป็นรูปแบบของ DI เช่นกัน เฟรมเวิร์ก (Dagger, Swinject) ทำให้การลงทะเบียนตามปกติและการแก้ไขการพึ่งพาเป็นอัตโนมัติ แต่สำหรับ 10-20 คลาส, DI แบบด้วยตนเองก็เพียงพอ
DI เป็นเทคนิค (แม่แบบ) ที่นำหลักการ Inversion of Control มาใช้ แตกต่างจากแพทเทิร์น GoF, DI ไม่มีโครงสร้างที่ตายตัวของ 3-4 คลาส DI เป็นวิธีการจัดระเบียบการพึ่งพา ไม่ใช่แพทเทิร์นการออกแบบ คอนเทนเนอร์ DI (Dagger, Swinject) เป็นเฟรมเวิร์กที่ทำให้เทคนิคนี้เป็นอัตโนมัติ
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ
อ่านเพิ่มเติม