# Clean Architecture Examples Complete examples of proper layer separation, MVVM patterns, and dependency injection. ## Complete Architecture Example ### Proper 3-Layer Implementation #### Presentation Layer - ViewModel ```swift class PaymentViewModel: BaseViewModel { // ✅ Depends only on UseCase (Domain layer) private let paymentUC: PaymentUseCase private let disposeBag = DisposeBag() // UI State let paymentAmount = BehaviorRelay(value: "") let isProcessing = BehaviorRelay(value: false) // ✅ Constructor injection init(paymentUC: PaymentUseCase) { self.paymentUC = paymentUC super.init() } func processPayment() { isProcessing.accept(true) // ✅ Delegates to UseCase, no business logic here paymentUC.execute(amount: paymentAmount.value) .subscribeOn(ConcurrentScheduler.background) .observeOn(MainScheduler.instance) .subscribe( onNext: { [weak self] result in self?.setState(.success(result)) }, onError: { [weak self] error in self?.setState(.error(error)) }, onCompleted: { [weak self] in self?.isProcessing.accept(false) } ) .disposed(by: disposeBag) } } ``` #### Domain Layer - UseCase ```swift // ✅ Protocol in Domain layer protocol PaymentUseCase { func execute(amount: String) -> Single } // ✅ Implementation in Domain layer class PaymentUseCaseImpl: PaymentUseCase { // ✅ Depends on Repository protocol (Domain layer) private let paymentRepository: PaymentRepository private let validationService: ValidationService init(paymentRepository: PaymentRepository, validationService: ValidationService) { self.paymentRepository = paymentRepository self.validationService = validationService } func execute(amount: String) -> Single { // ✅ Business logic in UseCase return validationService.validateAmount(amount) .flatMap { validatedAmount in // ✅ Calls Repository, not API directly return self.paymentRepository.processPayment(amount: validatedAmount) } .map { response in // ✅ Business rules applied here return self.applyBusinessRules(response) } } private func applyBusinessRules(_ response: PaymentResponse) -> PaymentResult { // Business logic here return PaymentResult(from: response) } } ``` #### Data Layer - Repository ```swift // ✅ Protocol in Domain layer protocol PaymentRepository { func processPayment(amount: Double) -> Single } // ✅ Implementation in Data layer class PaymentRepositoryImpl: PaymentRepository { private let apiService: PaymentApiService private let localStorage: PaymentLocalStorage init(apiService: PaymentApiService, localStorage: PaymentLocalStorage) { self.apiService = apiService self.localStorage = localStorage } func processPayment(amount: Double) -> Single { // ✅ Data access only, no business logic return apiService.processPayment(amount: amount) .do(onSuccess: { [weak self] response in // Save to local storage self?.localStorage.savePaymentRecord(response) }) } } ``` #### DI Setup - Swinject ```swift extension Container { func registerPaymentModule() { // Register Use Cases register(PaymentUseCase.self) { resolver in PaymentUseCaseImpl( paymentRepository: resolver.resolve(PaymentRepository.self)!, validationService: resolver.resolve(ValidationService.self)! ) } // Register Repositories register(PaymentRepository.self) { resolver in PaymentRepositoryImpl( apiService: resolver.resolve(PaymentApiService.self)!, localStorage: resolver.resolve(PaymentLocalStorage.self)! ) } // Register ViewModels register(PaymentViewModel.self) { resolver in PaymentViewModel( paymentUC: resolver.resolve(PaymentUseCase.self)! ) } } } ``` --- ## Common Anti-Patterns ### ❌ Anti-Pattern 1: ViewModel Calling API Directly ```swift class PaymentViewModel: BaseViewModel { private let apiService: PaymentApiService // ❌ Wrong layer! func processPayment(amount: Double) { // ❌ Direct API call bypasses business logic apiService.processPayment(amount: amount) .subscribe(onNext: { result in // Handle result }) .disposed(by: disposeBag) } } ``` **Problems**: - Business logic scattered or missing - Hard to test - Violates layer separation - Cannot reuse logic elsewhere **Fix**: Add UseCase layer ```swift // 1. Create UseCase protocol PaymentUseCase { func execute(amount: Double) -> Single } // 2. Update ViewModel class PaymentViewModel: BaseViewModel { private let paymentUC: PaymentUseCase // ✅ Correct! func processPayment(amount: Double) { paymentUC.execute(amount: amount) // ✅ Through UseCase .subscribe(/* ... */) .disposed(by: disposeBag) } } ``` --- ### ❌ Anti-Pattern 2: Business Logic in ViewModel ```swift class PaymentViewModel: BaseViewModel { func processPayment(amount: String) { // ❌ Validation logic in ViewModel guard let amt = Double(amount), amt > 1000 else { setState(.showError("Invalid amount")) return } // ❌ Business rules in ViewModel let fee = amt * 0.01 let total = amt + fee if total > 50_000_000 { setState(.showError("Amount too high")) return } // Process... } } ``` **Problems**: - Cannot reuse validation/business logic - Hard to test logic independently - ViewModel becomes complex - Violates Single Responsibility **Fix**: Move to UseCase ```swift // ✅ Business logic in UseCase class PaymentUseCaseImpl: PaymentUseCase { func execute(amount: String) -> Single { return validateAmount(amount) .flatMap { validatedAmount in return self.calculateFeesAndProcess(validatedAmount) } } private func validateAmount(_ amount: String) -> Single { guard let amt = Double(amount), amt > 1000 else { return .error(PaymentError.invalidAmount) } let fee = amt * 0.01 let total = amt + fee guard total <= 50_000_000 else { return .error(PaymentError.amountTooHigh) } return .just(amt) } } // ✅ ViewModel simplified class PaymentViewModel: BaseViewModel { func processPayment(amount: String) { paymentUC.execute(amount: amount) .subscribe( onNext: { [weak self] result in self?.setState(.success(result)) }, onError: { [weak self] error in self?.setState(.error(error)) } ) .disposed(by: disposeBag) } } ``` --- ### ❌ Anti-Pattern 3: UseCase Calling API Directly ```swift class PaymentUseCaseImpl: PaymentUseCase { private let apiService: PaymentApiService // ❌ Bypasses Repository! func execute(amount: Double) -> Single { // ❌ Direct API call return apiService.processPayment(amount: amount) .map { response in return PaymentResult(from: response) } } } ``` **Problems**: - Cannot swap data sources (API/local/mock) - Hard to test independently - Violates Dependency Inversion **Fix**: Add Repository layer ```swift // ✅ Repository protocol in Domain protocol PaymentRepository { func processPayment(amount: Double) -> Single } // ✅ UseCase depends on protocol class PaymentUseCaseImpl: PaymentUseCase { private let paymentRepository: PaymentRepository // ✅ Correct! func execute(amount: Double) -> Single { return paymentRepository.processPayment(amount: amount) .map { response in return PaymentResult(from: response) } } } // ✅ Implementation in Data layer class PaymentRepositoryImpl: PaymentRepository { private let apiService: PaymentApiService func processPayment(amount: Double) -> Single { return apiService.processPayment(amount: amount) } } ``` --- ## Refactoring Examples ### Example: Refactoring ViewModel with Business Logic #### Before (Violates Clean Architecture) ```swift class TransactionViewModel: BaseViewModel { private let apiService: TransactionApiService func loadTransactions(from startDate: Date, to endDate: Date) { // ❌ Date validation in ViewModel guard startDate <= endDate else { setState(.showError("Invalid date range")) return } // ❌ Business rule in ViewModel let daysDiff = Calendar.current.dateComponents([.day], from: startDate, to: endDate).day! guard daysDiff <= 90 else { setState(.showError("Date range too large")) return } // ❌ Direct API call apiService.getTransactions(from: startDate, to: endDate) .subscribe(onNext: { [weak self] transactions in // ❌ Business logic: filtering let filtered = transactions.filter { $0.amount > 0 } self?.transactions.accept(filtered) }) .disposed(by: disposeBag) } } ``` #### After (Clean Architecture) ```swift // DOMAIN LAYER - UseCase protocol TransactionUseCase { func getTransactions(from: Date, to: Date) -> Single<[Transaction]> } class TransactionUseCaseImpl: TransactionUseCase { private let repository: TransactionRepository func getTransactions(from startDate: Date, to endDate: Date) -> Single<[Transaction]> { // ✅ Validation in UseCase return validateDateRange(startDate, endDate) .flatMap { _ in return self.repository.getTransactions(from: startDate, to: endDate) } .map { transactions in // ✅ Business logic in UseCase return self.filterValidTransactions(transactions) } } private func validateDateRange(_ start: Date, _ end: Date) -> Single { guard start <= end else { return .error(TransactionError.invalidDateRange) } let daysDiff = Calendar.current.dateComponents([.day], from: start, to: end).day! guard daysDiff <= 90 else { return .error(TransactionError.dateRangeTooLarge) } return .just(()) } private func filterValidTransactions(_ transactions: [Transaction]) -> [Transaction] { return transactions.filter { $0.amount > 0 } } } // PRESENTATION LAYER - ViewModel class TransactionViewModel: BaseViewModel { private let transactionUC: TransactionUseCase // ✅ Depends on UseCase func loadTransactions(from startDate: Date, to endDate: Date) { // ✅ Simply delegates to UseCase transactionUC.getTransactions(from: startDate, to: endDate) .subscribe( onNext: { [weak self] transactions in self?.transactions.accept(transactions) self?.setState(.loaded) }, onError: { [weak self] error in self?.setState(.error(error)) } ) .disposed(by: disposeBag) } } ``` **Benefits**: - ✅ Clear separation of concerns - ✅ Business logic testable in isolation - ✅ ViewModel is simple coordinator - ✅ Can reuse UseCase elsewhere --- ## Testing Benefits ### With Clean Architecture ```swift class PaymentUseCaseTests: XCTestCase { func testExecute_WithValidAmount_ProcessesPayment() { // ✅ Can test UseCase in isolation let mockRepository = MockPaymentRepository() mockRepository.processPaymentResult = .just(PaymentResponse.success) let useCase = PaymentUseCaseImpl(paymentRepository: mockRepository) // Test business logic directly let result = try! useCase.execute(amount: "10000").toBlocking().first()! XCTAssertTrue(result.isSuccess) } func testExecute_WithInvalidAmount_ReturnsError() { let mockRepository = MockPaymentRepository() let useCase = PaymentUseCaseImpl(paymentRepository: mockRepository) // Test validation logic XCTAssertThrowsError( try useCase.execute(amount: "500").toBlocking().first() ) { error in XCTAssertEqual(error as? PaymentError, .invalidAmount) } } } ``` ### Without Clean Architecture ```swift // ❌ Hard to test business logic without UI/Network class PaymentViewModelTests: XCTestCase { func testProcessPayment() { // Need to mock UI, network, and test everything together // Business logic mixed with presentation logic // Hard to isolate what's being tested } } ``` --- ## Checklist for Reviews ```markdown ## Clean Architecture Checklist ### Layer Separation - [ ] ViewModel only depends on UseCase (not API/Repository) - [ ] UseCase contains all business logic - [ ] UseCase only depends on Repository protocol - [ ] Repository implementation is in Data layer - [ ] No business logic in ViewModel - [ ] No business logic in Repository - [ ] No UI code in Domain/Data layers ### Patterns - [ ] ViewModel extends BaseViewModel - [ ] UseCase follows protocol + implementation pattern - [ ] Repository follows protocol + implementation pattern - [ ] State management uses setState() ### Dependency Injection - [ ] All dependencies injected via constructor - [ ] No direct instantiation of dependencies - [ ] Swinject container properly configured - [ ] Dependencies are protocol-based (not concrete types) ### Data Flow - [ ] UI → ViewController → ViewModel → UseCase → Repository → API/DB - [ ] Never skips layers - [ ] Observables/Singles for async operations - [ ] Errors properly propagated through layers ```