Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:17:22 +08:00
commit 2c8fd6d9a0
22 changed files with 8353 additions and 0 deletions

View File

@@ -0,0 +1,181 @@
---
name: clean-architecture-review
description: Validate Clean Architecture implementation in iOS. Checks layer separation (Presentation/Domain/Data), MVVM patterns, dependency injection with Swinject, and UseCase/Repository patterns. Use when reviewing architecture, checking layer boundaries, or validating DI.
allowed-tools: Read, Grep, Glob
---
# Clean Architecture Validator
Verify Clean Architecture and MVVM implementation in iOS code following Payoo Merchant patterns.
## When to Activate
- "architecture review", "layer separation", "clean architecture"
- "MVVM", "dependency injection", "DI"
- "use case", "repository pattern"
- Reviewing module structure or refactoring
## Architecture Layers
**Presentation** → ViewControllers, ViewModels, Views
**Domain** → UseCases (business logic), Models, Repository protocols
**Data** → Repository implementations, API Services, Local Storage
**Correct Flow**:
```
UI → ViewController → ViewModel → UseCase → Repository → API/DB
```
## Review Process
### Step 1: Map Architecture
Classify files into layers:
- Presentation: `*ViewController.swift`, `*ViewModel.swift`
- Domain: `*UseCase.swift`, `*Repository.swift` (protocols)
- Data: `*RepositoryImpl.swift`, `*ApiService.swift`
### Step 2: Check Layer Violations
**Critical Issues**:
- 🔴 ViewModel calling API directly (bypassing UseCase)
- 🔴 Business logic in ViewModel (should be in UseCase)
- 🔴 UseCase calling API directly (bypassing Repository)
- 🔴 Direct instantiation (no DI)
### Step 3: Verify Patterns
**BaseViewModel**:
```swift
class PaymentViewModel: BaseViewModel<PaymentState>
class PaymentViewModel // Should extend BaseViewModel
```
**UseCase Pattern**:
```swift
protocol PaymentUseCase { }
class PaymentUseCaseImpl: PaymentUseCase { }
class PaymentUseCase { } // Should be protocol + impl
```
**Repository Pattern**:
```swift
protocol PaymentRepository { } // In Domain
class PaymentRepositoryImpl: PaymentRepository { } // In Data
```
**Dependency Injection**:
```swift
init(paymentUC: PaymentUseCase) { // Constructor injection
self.paymentUC = paymentUC
}
let paymentUC = PaymentUseCaseImpl() // Direct instantiation
```
### Step 4: Generate Report
Provide:
- Architecture compliance score
- Layer violations by severity
- Current vs. should-be architecture
- Refactoring steps
- Effort estimate
## Common Violations
### ❌ ViewModel Bypassing UseCase
```swift
class PaymentViewModel {
private let apiService: PaymentApiService // WRONG LAYER!
}
```
**Should be**:
```swift
class PaymentViewModel {
private let paymentUC: PaymentUseCase // CORRECT!
}
```
### ❌ Business Logic in ViewModel
```swift
class PaymentViewModel {
func processPayment(amount: Double) {
// Validation in ViewModel
guard amount > 1000 else { return }
// Business rules in ViewModel
let fee = amount * 0.01
}
}
```
**Should be in UseCase**:
```swift
class PaymentUseCaseImpl {
func execute(amount: Double) -> Single<PaymentResult> {
// Validation in UseCase
return validateAmount(amount)
.flatMap { processPayment($0) }
}
}
```
## Output Format
```markdown
# Clean Architecture Review
## Compliance Score: X/100
## Critical Violations: X
### 1. ViewModel Bypassing UseCase
**File**: `PaymentViewModel.swift:15`
**Current**: ViewModel → API
**Should be**: ViewModel → UseCase → Repository → API
**Fix**: [Refactoring steps]
---
## Dependency Graph
### Current (Problematic)
ViewModel → ApiService ❌
### Should Be
ViewModel → UseCase → Repository → ApiService ✅
## Recommendations
1. Create missing UseCases
2. Move business logic to Domain layer
3. Setup DI container
4. Add Repository layer
## Effort Estimate
- Module refactoring: X hours
- DI setup: X hours
- Testing: X hours
```
## Quick Checks
**Layer Boundaries**:
- [ ] ViewModels only depend on UseCases
- [ ] UseCases contain all business logic
- [ ] Repositories handle data access only
- [ ] No UI code in Domain/Data layers
**Dependency Injection**:
- [ ] All dependencies via constructor
- [ ] No direct instantiation
- [ ] Swinject container registration
- [ ] Protocol-based dependencies
**Patterns**:
- [ ] ViewModels extend BaseViewModel
- [ ] UseCases follow protocol + impl
- [ ] Repositories follow protocol + impl
- [ ] State management via setState()
## Reference
**Detailed Examples**: See `examples.md` for complete architecture patterns and refactoring guides.

View File

@@ -0,0 +1,505 @@
# 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<PaymentState> {
// Depends only on UseCase (Domain layer)
private let paymentUC: PaymentUseCase
private let disposeBag = DisposeBag()
// UI State
let paymentAmount = BehaviorRelay<String>(value: "")
let isProcessing = BehaviorRelay<Bool>(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<PaymentResult>
}
// 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<PaymentResult> {
// 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<PaymentResponse>
}
// 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<PaymentResponse> {
// 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<PaymentState> {
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<PaymentResult>
}
// 2. Update ViewModel
class PaymentViewModel: BaseViewModel<PaymentState> {
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<PaymentState> {
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<PaymentResult> {
return validateAmount(amount)
.flatMap { validatedAmount in
return self.calculateFeesAndProcess(validatedAmount)
}
}
private func validateAmount(_ amount: String) -> Single<Double> {
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<PaymentState> {
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<PaymentResult> {
// 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<PaymentResponse>
}
// UseCase depends on protocol
class PaymentUseCaseImpl: PaymentUseCase {
private let paymentRepository: PaymentRepository // Correct!
func execute(amount: Double) -> Single<PaymentResult> {
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<PaymentResponse> {
return apiService.processPayment(amount: amount)
}
}
```
---
## Refactoring Examples
### Example: Refactoring ViewModel with Business Logic
#### Before (Violates Clean Architecture)
```swift
class TransactionViewModel: BaseViewModel<TransactionState> {
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<Void> {
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<TransactionState> {
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<State>
- [ ] 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
```