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.