06. Clean Architecture — Uncle Bob Qoidalari va Dependency Rule
Arxitektura
iOS Arxitektura 6: Clean Architecture — Uncle Bob Qoidalari, Qatlamlar va Dependency Rule
Holat: 2026-yil oktabr holatiga ko'ra yozilgan. Zamonaviy iOS dasturlashda (Swift 6, iOS 17+) Clean Architecture prinsiplari loyihani uzoq muddat barqaror saqlash, masshtablash va testlashni osonlashtirish uchun eng kuchli yechimlardan biri hisoblanadi.
Mundarija
- Clean Architecture nima?
- Dependency Rule (ENG MUHIM QOIDA!)
- iOS'da Clean Architecture qatlamlari
- Use Case (Interactor)
- Repository Pattern
- Amaliy loyiha: E-Commerce (Clean Architecture)
- DTO vs Entity
- Dependency Injection
- Clean Architecture + MVVM kombinatsiyasi
- Unit Testing har bir qatlamda
- Xulosa
- Manbalar
1. Clean Architecture nima? {#clean-architecture-nima}
Clean Architecture — bu Robert C. Martin (Uncle Bob) tomonidan taklif qilingan dasturiy ta'minot arxitekturasi. Uning asosiy maqsadi tizimni qismlarga (qatlamlarga) ajratish orqali quyidagi xususiyatlarga erishishdir:
- Framework-independent: Arxitektura qandaydir tashqi kutubxona yoki framework'ga (masalan, UIKit, SwiftUI, CoreData) bog'lanib qolmaydi.
- Testable: Biznes mantiq (business rules) UI, Database yoki Web Server'siz oson test qilinishi mumkin.
- UI-independent: UI osongina o'zgarishi mumkin. Web UI Console UI bilan almashishi mumkin, bunda biznes mantiq o'zgarmaydi.
- Database-independent: Oracle yoki SQL Server o'rniga Mongo, CoreData yoki Realm ishlatilishi mumkin.
- External Agency-independent: Biznes mantiq tashqi dunyo haqida hech narsani bilmaydi.
Konsentrik doiralar (Qatlamlar):
Clean Architecture Konsentrik Doiralar
2. Dependency Rule (ENG MUHIM QOIDA!) {#dependency-rule}
Muhim: Clean Architecture'ning yadrosi bu — Dependency Rule (Bog'liqlik qoidasi).
Qoida shunday deydi: Manba kodining bog'liqliklari faqat ICHKARIGA, yuqori darajadagi siyosatlar (policy) tomon yo'naltirilgan bo'lishi kerak.
- Entity (Ichki qatlam): Hech qanday tashqi qatlamni bilmaydi. U faqat sof ma'lumotlar va biznes qoidalarini saqlaydi.
- Use Case: Entity'larni biladi, lekin UI yoki DB'ni bilmaydi.
- Interface Adapters (ViewModel/Presenter): Use Case'larni biladi, UI qanday chizilishini bilmaydi, lekin UI'ga qulay formatga o'tkazib beradi.
- Frameworks (UIKit, CoreData, Alamofire): Hammasini biladi va tashqi dunyo bilan aloqa qiladi.
Strelkalar faqat ICHKARIGA qaraydi: UI (View) -> ViewModel -> Use Case -> Entity
Clean Architecture Dependency Rule
3. iOS'da Clean Architecture qatlamlari {#qatlamlar}
iOS loyihalarida Clean Architecture odatda 3 ta asosiy qatlamga (Layer) bo'linadi. Kattaroq loyihalarda bu qatlamlar alohida Swift Package (SPM) modullari bo'lishi mumkin.
- Domain Layer (Yadro): Eng ichki qatlam. O'ziga boshqa hech qaysi qatlamni import qilmaydi (hatto Foundation'dan tashqari boshqa framework'larni ham, masalan UIKit'ni mutlaqo kiritmaslik kerak).
- Entity'lar (Biznes modellar)
- Use Case Protocol'lari (Biznes logikasi)
- Repository Protocol'lari (Data saqlash qoidalari, lekin implementation emas!)
- Data Layer: Tashqi ma'lumotlar manbai bilan ishlash.
- Repository Implementation'lar
- API Service'lar (Network requests)
- Local Storage (CoreData, UserDefaults)
- DTO (Data Transfer Objects) va ularni Entity'ga o'giruvchi Mapper'lar.
- Presentation Layer: Foydalanuvchi interfeysi.
- Views (SwiftUI Views, UIViewController)
- ViewModels / Presenters (Holatni boshqarish)
- Bu qatlam Domain layer'dagi Use Case'larni chaqiradi.
4. Use Case (Interactor) {#use-case}
Use Case — bu dasturdagi bitta aniq harakat yoki biznes operatsiya. Masalan, foydalanuvchini ro'yxatdan o'tkazish, mahsulotlarni yuklash.
Nima uchun kerak? U ViewModel'ni juda semirib ketishdan asraydi va qandaydir amalni bajarish ketma-ketligini (business flow) o'zida saqlaydi.
import Foundation
// Domain Layer
protocol FetchUsersUseCaseProtocol {
func execute() async throws -> [User]
}
// Interactor Implementation
final class FetchUsersUseCase: FetchUsersUseCaseProtocol {
private let userRepository: UserRepositoryProtocol
// DI orqali Repository qabul qilinadi
init(userRepository: UserRepositoryProtocol) {
self.userRepository = userRepository
}
func execute() async throws -> [User] {
// Bu yerda biznes logika bo'lishi mumkin. Masalan, faqat aktiv userlarni filtrlash.
let users = try await userRepository.fetchUsers()
return users.filter { $0.isActive }
}
}
5. Repository Pattern {#repository-pattern}
Repository data layer va domain layer o'rtasidagi ko'prik vazifasini bajaradi. Domain layer "Menga ma'lumot kerak" deydi (Protocol orqali), Data layer esa "Men buni API yoki DB dan olib kelib beraman" deydi (Implementation).
// 1. Domain Layer'dagi Protocol
protocol UserRepositoryProtocol {
func fetchUsers() async throws -> [User]
}
// 2. Data Layer'dagi Implementation (Real API)
final class UserRepository: UserRepositoryProtocol {
let apiService: APIService
init(apiService: APIService) {
self.apiService = apiService
}
func fetchUsers() async throws -> [User] {
let userDTOs = try await apiService.request(endpoint: "/users")
return userDTOs.map { $0.toDomain() } // DTO'ni Entity'ga o'giramiz
}
}
// 3. Test'da ishlatish uchun Mock Repository
final class MockUserRepository: UserRepositoryProtocol {
var shouldReturnError = false
func fetchUsers() async throws -> [User] {
if shouldReturnError {
throw URLError(.badServerResponse)
}
return [User(id: "1", name: "Eshmat", isActive: true)]
}
}
6. Amaliy loyiha: E-Commerce (Clean Architecture) {#amaliy-loyiha}
Endi hamma qismlarni birlashtirib, E-Commerce ilovasidagi "Mahsulotlar ro'yxatini ko'rsatish" ekranini yaratamiz.
import UIKit
// SnapKit o'rniga misol uchun oddiy Constraints yozamiz, lekin tasavvur qiling SnapKit ishlatilyapti.
// ==========================================
// MARK: - 1. DOMAIN LAYER
// ==========================================
struct Product {
let id: String
let title: String
let price: Double
}
protocol ProductRepositoryProtocol {
func fetchProducts() async throws -> [Product]
}
protocol FetchProductsUseCaseProtocol {
func execute() async throws -> [Product]
}
final class FetchProductsUseCase: FetchProductsUseCaseProtocol {
private let repository: ProductRepositoryProtocol
init(repository: ProductRepositoryProtocol) {
self.repository = repository
}
func execute() async throws -> [Product] {
return try await repository.fetchProducts()
}
}
// ==========================================
// MARK: - 2. DATA LAYER
// ==========================================
struct ProductDTO: Codable {
let productId: String
let name: String
let priceString: String // API dan string kelyapti
func toDomain() -> Product {
return Product(
id: productId,
title: name,
price: Double(priceString) ?? 0.0
)
}
}
final class ProductRepository: ProductRepositoryProtocol {
func fetchProducts() async throws -> [Product] {
// Aslida URLSession orqali tarmoqqa so'rov yuboriladi
try await Task.sleep(nanoseconds: 1_000_000_000)
let mockDTOs = [
ProductDTO(productId: "1", name: "iPhone 16", priceString: "999.0"),
ProductDTO(productId: "2", name: "MacBook Pro", priceString: "1999.0")
]
return mockDTOs.map { $0.toDomain() }
}
}
// ==========================================
// MARK: - 3. PRESENTATION LAYER (MVVM)
// ==========================================
@MainActor
final class ProductListViewModel {
private let fetchProductsUseCase: FetchProductsUseCaseProtocol
// Outputs
var onStateChanged: ((ViewState) -> Void)?
private(set) var products: [Product] = []
enum ViewState {
case loading
case success
case error(String)
}
init(fetchProductsUseCase: FetchProductsUseCaseProtocol) {
self.fetchProductsUseCase = fetchProductsUseCase
}
func loadProducts() {
onStateChanged?(.loading)
Task {
do {
self.products = try await fetchProductsUseCase.execute()
onStateChanged?(.success)
} catch {
onStateChanged?(.error(error.localizedDescription))
}
}
}
}
// ==========================================
// MARK: - 4. UI (VIEW CONTROLLER)
// ==========================================
final class ProductListViewController: UIViewController {
private let viewModel: ProductListViewModel
private let tableView = UITableView()
private let activityIndicator = UIActivityIndicatorView(style: .large)
init(viewModel: ProductListViewModel) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
bindViewModel()
viewModel.loadProducts()
}
private func setupUI() {
title = "Products"
view.backgroundColor = .systemBackground
tableView.dataSource = self
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
view.addSubview(tableView)
view.addSubview(activityIndicator)
// SnapKit style (using Autolayout directly here for out-of-the-box compiling)
tableView.translatesAutoresizingMaskIntoConstraints = false
activityIndicator.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: view.topAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
activityIndicator.centerXAnchor.constraint(equalTo: view.centerXAnchor),
activityIndicator.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
}
private func bindViewModel() {
viewModel.onStateChanged = { [weak self] state in
guard let self = self else { return }
switch state {
case .loading:
self.activityIndicator.startAnimating()
self.tableView.isHidden = true
case .success:
self.activityIndicator.stopAnimating()
self.tableView.isHidden = false
self.tableView.reloadData()
case .error(let message):
self.activityIndicator.stopAnimating()
print("Error: \(message)")
}
}
}
}
extension ProductListViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return viewModel.products.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
let product = viewModel.products[indexPath.row]
cell.textLabel?.text = "\(product.title) - $\(product.price)"
return cell
}
}
// ==========================================
// MARK: - 5. DI CONTAINER (COMPOSITION ROOT)
// ==========================================
final class DIContainer {
func makeProductListViewController() -> UIViewController {
let repository = ProductRepository()
let useCase = FetchProductsUseCase(repository: repository)
let viewModel = ProductListViewModel(fetchProductsUseCase: useCase)
let viewController = ProductListViewController(viewModel: viewModel)
return viewController
}
}
7. DTO vs Entity {#dto-vs-entity}
Nima uchun API'dan kelgan Codable modelni (DTO) to'g'ridan-to'g'ri loyiha bo'ylab ishlatmaymiz? Nima uchun Entity va DTO alohida?
- DTO (Data Transfer Object): API'dan keluvchi raw (xom) ma'lumot. API'ning formati bizga mos tushmasligi mumkin (masalan
pricestring ko'rinishida kelishi, sanalar har xil formatda bo'lishi). - Entity: Dasturning ichki, "toza" modeli. Barcha mantiq unga asoslanadi.
- Mapper: DTO'ni Entity'ga o'giruvchi funksiya.
Senior eslatmasi: Tasavvur qiling backend komandasi API response'ni o'zgartirdi (
user_nameo'rnigafirst_nameqildi). Agar siz hamma joyda DTO ishlatsangiz, barcha ekranlar va kodlarni o'zgartirib chiqishingiz kerak. Clean Architecture'da esa faqat DTO va Mapper o'zgaradi, qolgan kod buzilmaydi.
8. Dependency Injection {#dependency-injection}
Clean Architecture amalga oshishi uchun obyektlar o'zlariga kerakli narsalarni o'zi yaratmasligi, balki tashqaridan (Protocol orqali) qabul qilishi kerak. Bu Dependency Injection (DI) deyiladi.
- Constructor Injection (Eng yaxshi amaliyot): Barcha bog'liqliklar
initorqali beriladi (Yuqoridagi kodlarda shu ishlatilgan). - Property Injection: Bog'liqliklar propertiyalar orqali o'rnatiladi. (Odatda Storyboard ishlatilganda kerak bo'ladi).
Loyiha kattalashganda Swinject yoki Factory kabi DI framework'lardan foydalanish tavsiya etiladi.
9. Clean Architecture + MVVM kombinatsiyasi {#clean-mvvm}
iOS'da odatda Clean Architecture bilan birgalikda Presentation layer'da MVVM (Model-View-ViewModel) ishlatiladi.
Oqim (Data Flow):
- User tugmani bosadi (View).
- View ViewModel'ga
viewModel.loadData()buyrug'ini beradi. - ViewModel Use Case'ga
useCase.execute()ni chaqiradi. - Use Case Repository'dan
repository.fetch()so'raydi. - Repository Data Source (API/DB) dan DTO olib, Entity'ga aylantirib qaytaradi.
- Use Case Entity'ni ViewModel'ga qaytaradi.
- ViewModel ma'lumotni tayyorlab, View'ga signal beradi (
onStateChanged). - View yangilangan ma'lumotni chizadi.
10. Unit Testing har bir qatlamda {#unit-testing}
Clean Architecture ning eng katta yutug'i — Testable (test qilinishi osonligidadir).
import XCTest
// Use Case ni test qilish
final class FetchProductsUseCaseTests: XCTestCase {
// Mock qilingan Repository yaratamiz
class MockProductRepository: ProductRepositoryProtocol {
var resultToReturn: [Product] = []
var shouldThrowError = false
func fetchProducts() async throws -> [Product] {
if shouldThrowError {
throw URLError(.notConnectedToInternet)
}
return resultToReturn
}
}
func testFetchProducts_Success() async throws {
// Arrange (Tayyorgarlik)
let mockRepository = MockProductRepository()
mockRepository.resultToReturn = [Product(id: "1", title: "Test", price: 10)]
let sut = FetchProductsUseCase(repository: mockRepository) // sut = System Under Test
// Act (Harakat)
let products = try await sut.execute()
// Assert (Tekshirish)
XCTAssertEqual(products.count, 1)
XCTAssertEqual(products.first?.title, "Test")
}
}
Har bir qatlam o'zining "Mock" obyektlari yordamida izolyatsiya qilingan holda test qilinadi. ViewModel uchun Mock Use Case, Use Case uchun Mock Repository ishlatiladi.
Xulosa {#xulosa}
- Clean Architecture sizga masshtablash (scalability) va o'zgarishlarga chidamli (maintainable) kod yozish imkonini beradi.
- Dependency Rule qoidasiga ko'ra ichki qatlam tashqi qatlamni mutlaqo bilmasligi shart.
- Kichik loyihalar uchun Clean Architecture ortiqcha bo'lishi mumkin (over-engineering). Lekin o'rta va katta korporativ loyihalar uchun u ajralmas qismdir.
- Framework'larga (UIKit, SwiftUI, CoreData) tobe bo'lib qolishdan saqlaning.