- Published on
Clean Architecture — qatlamlar bo'linishi
- Authors
- Name
- ShoxruxC
- @iOSdasturchi
Clean Architecture — Robert C. Martin (Uncle Bob) ning mashhur arxitektura yondashuvi. Asosiy g'oya: biznes logika hech qanday framework ga bog'liq bo'lmasligi kerak. SwiftUI bugun, ertaga boshqa narsa — biznes logika o'zgarmaydi.
Qatlamlar diagrammasi
┌─────────────────────────────────────────┐
│ Frameworks & Drivers (tashqi) │
│ SwiftUI, URLSession, CoreData │
│ ┌─────────────────────────────────┐ │
│ │ Interface Adapters │ │
│ │ ViewModels, Presenters, Gateways│ │
│ │ ┌─────────────────────────┐ │ │
│ │ │ Use Cases (Application) │ │ │
│ │ │ GetMaqolalar, Login │ │ │
│ │ │ ┌─────────────────┐ │ │ │
│ │ │ │ Entities │ │ │ │
│ │ │ │ (Domain) │ │ │ │
│ │ │ │ Biznes logika │ │ │ │
│ │ │ └─────────────────┘ │ │ │
│ │ └─────────────────────────┘ │ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────────┘
Dependency Rule: → faqat ichkariga
Tashqi → Interface Adapters → Use Cases → Entities
SwiftUI da Clean Architecture
// ═══════════════════════════════════════════════════════════════
// 🏛 ENTITY (Domain) — eng ichki qatlam
//
// Entity — biznes ob'ekt. Hech qanday framework ga bog'liq EMAS.
// import SwiftUI ❌, import UIKit ❌, import Foundation ✅ (minimal)
//
// Entity VA biznes qoidalari shu yerda.
// Agar ilovani SwiftUI dan UIKit ga o'tkazsangiz —
// Entity HECH O'ZGARMAYDI. Bu Clean Architecture ning kuchi.
//
// Dependency Rule:
// Entity hech narsani bilmaydi (UseCase, ViewModel, View — yo'q!)
// Lekin BARCHASI Entity ni biladi va ishlatadi.
// ═══════════════════════════════════════════════════════════════
// Import: faqat Foundation (yoki hech narsa)
struct Maqola {
let id: UUID // Noyob identifikator
var sarlavha: String // Maqola nomi
var tanasi: String // To'liq matn
var nashrEtildi: Bool // Nashr qilindimi?
let yaratilganSana: Date // Qachon yaratildi
// ── BIZNES QOIDASI ──
// Bu Entity ichidagi logika — DOMAIN logika.
// UI logika emas! Biznes qoidasi:
// "Maqola nashr etish uchun sarlavha bo'sh bo'lmasligi
// va tanasi kamida 100 ta belgidan iborat bo'lishi kerak."
//
// Bu qoida QAYERDA ishlatilsa ham BIR XIL ishlaydi.
// SwiftUI da ham, UIKit da ham, test da ham.
var nashrEtishMumkin: Bool {
!sarlavha.isEmpty && tanasi.count >= 100
}
}
// ═══════════════════════════════════════════════════════════════
// ⚡️ USE CASE — bitta biznes amali
//
// UseCase = "ilova nima qila oladi?"
// Har UseCase BITTA vazifa bajaradi (Single Responsibility):
// • MaqolalarniOlish — maqolalarni olish va filtrlash
// • MaqolaniSaqlash — maqolani validatsiya qilib saqlash
// • FoydalanuvchiniKirish — login jarayoni
//
// UseCase HECH QANDAY framework bilmaydi:
// ❌ SwiftUI, UIKit, Alamofire — bilmaydi
// ✅ Entity va Gateway protokolini biladi
//
// Nima uchun alohida UseCase?
// 1. Test oson — bitta ishni tekshirish
// 2. Qayta ishlatish — boshqa joyda ham chaqirish mumkin
// 3. Biznes logika bitta joyda — tarqalmagan
// ═══════════════════════════════════════════════════════════════
// Protokol — mock yaratish uchun
protocol MaqolalarniOlishUseCase {
/// Nashr etilgan maqolalarni olish (yangi birinchi)
func bajarish() async throws -> [Maqola]
}
class MaqolalarniOlish: MaqolalarniOlishUseCase {
// Gateway — ma'lumot manbasi (tarmoq, bazasi)
// Protokol turi — aniq implementatsiyani bilmaydi
private let gateway: MaqolaGatewayProtocol
// DI — Gateway tashqaridan uzatiladi
init(gateway: MaqolaGatewayProtocol) {
self.gateway = gateway
}
func bajarish() async throws -> [Maqola] {
// 1. Gateway dan HAMMA maqolalarni olish
let maqolalar = try await gateway.hammasi()
// 2. BIZNES logika:
// - Faqat nashr etilganlarni filtrlash
// - Yangi birinchi tartibda saralash
return maqolalar
.filter(\.nashrEtildi) // Faqat nashr etilganlar
.sorted { $0.yaratilganSana > $1.yaratilganSana } // Yangi birinchi
// Bu logika VIEW da bo'lmasligi KERAK!
// View faqat "ko'rsatadi", biznes qoidalari UseCase da
}
}
// ── SAQLASH USE CASE ──
protocol MaqolaniSaqlashUseCase {
func bajarish(_ maqola: Maqola) async throws
}
class MaqolaniSaqlash: MaqolaniSaqlashUseCase {
private let gateway: MaqolaGatewayProtocol
init(gateway: MaqolaGatewayProtocol) {
self.gateway = gateway
}
func bajarish(_ maqola: Maqola) async throws {
// ── BIZNES VALIDATSIYA ──
// Entity ning o'z biznes qoidasini tekshirish
// Sarlavha bo'sh bo'lmasligi VA tanasi 100+ belgi
guard maqola.nashrEtishMumkin else {
throw DomainXato.nashrEtishMumkinEmas
// Xato — UseCase to'xtatadi, saqlash bo'lmaydi
}
// Validatsiyadan o'tdi — saqlash
try await gateway.saqlash(maqola)
}
}
// ── DOMAIN XATOLARI ──
// Biznes qoidalari buzilganda — aniq xato turi
enum DomainXato: LocalizedError {
case nashrEtishMumkinEmas
var errorDescription: String? {
switch self {
case .nashrEtishMumkinEmas:
return "Maqola sarlavhasi bo'sh yoki tanasi 100 ta belgidan kam"
}
}
}
// ═══════════════════════════════════════════════════════════════
// 🚪 GATEWAY PROTOKOL — Domain aniqlaydi!
//
// Gateway = "eshik". Domain tashqi dunyoga shu "eshik" orqali chiqadi.
// Domain PROTOKOLNI aniqlaydi — "menga shu narsalar kerak".
// Data qatlam IMPLEMENTATSIYANI yozadi — "men senga shu yerdan beraman".
//
// Bu "Dependency Inversion Principle":
// Domain (ichki) protokol yaratadi
// Data (tashqi) protokolga mos keladi
// Domain Data ni bilmaydi — faqat protokolni biladi
// ═══════════════════════════════════════════════════════════════
protocol MaqolaGatewayProtocol {
func hammasi() async throws -> [Maqola]
func saqlash(_ maqola: Maqola) async throws
func ochirish(id: UUID) async throws
}
// ═══════════════════════════════════════════════════════════════
// 🔌 GATEWAY IMPLEMENTATSIYA (Data qatlam)
//
// Bu class Gateway protokolini AMALGA OSHIRADI.
// API dan DTO (Data Transfer Object) oladi va
// Entity ga aylantiradi.
//
// DTO → Entity: tashqi format → ichki format
// Entity → DTO: ichki format → tashqi format
//
// Nima uchun alohida DTO?
// Chunki server formati va biznes ob'ekt BOSHQACHA bo'lishi mumkin.
// Server "title" deydi, biz "sarlavha" deymiz.
// Server "published" deydi, biz "nashrEtildi" deymiz.
// ═══════════════════════════════════════════════════════════════
class MaqolaGateway: MaqolaGatewayProtocol {
private let api: MaqolaAPIProtocol
init(api: MaqolaAPIProtocol) {
self.api = api
}
func hammasi() async throws -> [Maqola] {
// 1. API dan DTO lar olish (server formati)
let dtoLar = try await api.maqolalarniYukla()
// 2. DTO → Entity konvertatsiya (biznes formati)
return dtoLar.map { dto in
Maqola(
id: dto.id,
sarlavha: dto.title, // "title" → "sarlavha"
tanasi: dto.body, // "body" → "tanasi"
nashrEtildi: dto.published, // "published" → "nashrEtildi"
yaratilganSana: dto.createdAt
)
}
}
func saqlash(_ maqola: Maqola) async throws {
// Entity → DTO konvertatsiya (server uchun)
let dto = MaqolaDTO(
id: maqola.id,
title: maqola.sarlavha,
body: maqola.tanasi,
published: maqola.nashrEtildi,
createdAt: maqola.yaratilganSana
)
try await api.maqolaniSaqlash(dto)
}
func ochirish(id: UUID) async throws {
try await api.maqolaniOchirish(id: id)
}
}
// ═══════════════════════════════════════════════════════════════
// 🧠 VIEWMODEL (Presentation qatlam)
//
// ViewModel UseCase ni chaqiradi (to'g'ridan-to'g'ri Gateway emas!)
// ViewModel biznes logikani BILMAYDI — UseCase biladi.
//
// Oqim:
// View → ViewModel → UseCase → Gateway → API
// ↑ ↓
// └──────── natija ──────────────┘
//
// Har qatlam faqat KEYINGI qatlamni biladi.
// View ← ViewModel ← UseCase ← Gateway ← API
// ═══════════════════════════════════════════════════════════════
@MainActor
class MaqolalarViewModel: ObservableObject {
@Published var maqolalar: [Maqola] = []
@Published var xato: String?
// UseCase — PROTOKOL turi (mock almashish uchun)
private let olishUseCase: MaqolalarniOlishUseCase
// DI — UseCase tashqaridan uzatiladi
init(olishUseCase: MaqolalarniOlishUseCase) {
self.olishUseCase = olishUseCase
}
func yuklash() async {
do {
// ViewModel FAQAT UseCase ni chaqiradi
// Filtrlash, saralash — UseCase ishi
// ViewModel bu tafsilotlarni bilmaydi!
maqolalar = try await olishUseCase.bajarish()
} catch {
xato = error.localizedDescription
}
}
}
Fayl tuzilmasi
MeningIlovam/
├── Domain/ ← Eng ichki qatlam
│ ├── Entities/
│ │ └── Maqola.swift
│ ├── UseCases/
│ │ ├── MaqolalarniOlish.swift
│ │ └── MaqolaniSaqlash.swift
│ └── Gateways/ ← faqat protokollar
│ └── MaqolaGatewayProtocol.swift
├── Data/ ← Gateway implementatsiya
│ ├── Gateways/
│ │ └── MaqolaGateway.swift
│ ├── DTOs/
│ │ └── MaqolaDTO.swift
│ └── API/
│ └── MaqolaAPI.swift
├── Presentation/ ← UI qatlam
│ ├── ViewModels/
│ │ └── MaqolalarViewModel.swift
│ └── Views/
│ └── MaqolalarKorinishi.swift
└── DI/ ← Dependency Injection
└── Container.swift
🎯 Topshiriq: Use Case ajratish
MVVM loyihangizdan bitta amalni alohida Use Case sifatida ajrating. GetMaqolalarUseCase protokol va sinf yarating. ViewModel ni Use Case ga bog'lang (init da). Test da Mock Use Case ishlatib test yozing. Qat'iy qoida: Use Case ichida import SwiftUI bo'lmasin — faqat sof Swift.