Section 10/171 menit
10. Best Practice: Architecture & Dependency Boundaries
10. Best Practice: Architecture & Dependency Boundaries
BP 10.1: Dependency Injection via Initializer, Bukan Singleton
Singleton bikin testing susah dan menyembunyikan dependency. Inject lewat initializer.
swift
// ❌ Singleton sulit di-test
@MainActor
final class CartViewModel {
func checkout() async {
try await APIClient.shared.purchase(...) // hardcoded
}
}
// ✓ Injected
@MainActor
final class CartViewModel {
private let api: any APIClientProtocol
init(api: any APIClientProtocol) {
self.api = api
}
func checkout() async {
try await api.purchase(...)
}
}
BP 10.2: Protocol untuk Boundary, Bukan untuk Reusability
Banyak developer over-protocol-ize. Pakai protocol di boundary (bukan internal):
- Service yang akan di-mock di test → protocol.
- Repository yang punya banyak implementasi (real, in-memory, mock) → protocol.
- Internal helper yang hanya satu implementasi → struct/class langsung.
BP 10.3: Hindari @MainActor di Service Layer
Service / repository / use-case tidak di main thread. Mereka punya isolation sendiri atau nonisolated. Caller (ViewModel) yang @MainActor.
swift
// ❌ Service di MainActor
@MainActor
final class UserService {
func loadUsers() async throws -> [User] { ... }
}
// ✓ Service nonisolated atau di actor sendiri
actor UserService {
func loadUsers() async throws -> [User] { ... }
}
// ViewModel @MainActor memanggil service:
@MainActor
final class UserListVM {
private let service: UserService
func reload() async {
do {
self.users = try await service.loadUsers() // hop ke service, balik ke main
} catch { ... }
}
}
BP 10.4: Repository Pattern dengan Actor
Repository = abstraksi data source. Cocok jadi actor karena dipakai dari banyak ViewModel paralel.
swift
protocol UserRepository: Sendable {
func findAll() async throws -> [User]
func find(id: UUID) async throws -> User?
func save(_ user: User) async throws
}
actor LocalUserRepository: UserRepository {
private let store: ModelContainer
init(store: ModelContainer) { self.store = store }
func findAll() async throws -> [User] { /* ... */ }
func find(id: UUID) async throws -> User? { /* ... */ }
func save(_ user: User) async throws { /* ... */ }
}
BP 10.5: Modular App dengan Isolation Boundary
Tiap feature module di SPM = boundary alami. Module hanya expose Sendable types dan @MainActor view.
swift
// Module LoginFeature:
public struct LoginCredentials: Sendable {
public let email: String
public let password: String
}
@MainActor
public final class LoginCoordinator {
public init(...) { ... }
public func start() { ... }
}
// Internal implementation tidak di-expose.