Tüm yazılar
architecturemobileexpoclean-architecture

Mobil uygulamaları nasıl kuruyoruz: Expo için bir Clean Architecture el kitabı

preunec team13 Temmuz 20266 dk okuma
Paylaş

Çoğu mobil kod tabanı aynı şekilde çürür: iş mantığı ekranlara sızar, bir ekran doğrudan ağ katmanına uzanır ve iki geliştirici aynı hafta içinde bir merge savaşı çıkarmadan uygulamaya dokunamaz. Bundan kaçınmak için kullandığımız el kitabı bu — Clean Architecture, özellik modülleri ve elle bağımlılık enjeksiyonu; Expo + TypeScript için ayarlanmış.

Özet — Tek yönlü bir bağımlılık kuralına sahip dört katman: Domain çerçeveden bağımsızdır ve her şey içeriye, ona doğru işaret eder. Özellikler kendi içinde kapalı modüllerdir. Bağımlılıklar bir composition root'ta elle bağlanır — sihirli konteyner yok. Sonuç: paralele uygun, test edilebilir ve değiştirmesi ucuz.

Neden bir mimari el kitabı

Beş hedefimiz vardı ve aşağıdaki her kural bunlardan birine hizmet ediyor:

Hedef Bize ne kazandırır
Paralel geliştirme iki geliştirici, az merge çakışması
Bakımı kolay sınırlar UI değişiklikleri iş mantığını yeniden yazmaz
Değiştirilebilir altyapı REST ↔ Firebase'i küçük bir etkiyle takas et
Test edilebilirlik iş kuralları simülatörsüz test edilir
Hızlı iterasyon açık, basit DI — ağır çerçeve yok

Açık hedef olmayanlar bizi dürüst tutar: yansımaya dayalı DI konteynerleri yok, özellikler arası bağlaşım yok ve "her şey için global state" yok.

Dört katman ve önemli olan tek kural

Bütünü, onları bağlayan bir composition root artı dört katman. Bağımlılık kuralı taşıyıcı fikirdir: bağımlılıklar yalnızca içeriye, Domain'e doğru işaret eder.

flowchart LR
  Composition["Composition Root (DI wiring)"]
  Presentation["Presentation"]
  Domain["Domain (framework-agnostic)"]
  Data["Data"]
  Core["Core / Infrastructure"]
  Presentation --> Domain
  Data --> Domain
  Data --> Core
  Composition --> Presentation
  Composition --> Data
  Composition --> Core
  • Presentation — ekranlar, bileşenler, view-model hook'ları.
  • Domain — varlıklar, value object'ler, use case'ler ve repository port'ları (arayüzler). Bu katman asla react, react-native veya expo-* import etmez.
  • Data — repository implementasyonları, uzak/yerel datasource'lar, DTO mapper'ları.
  • Core — http, storage, config, logging, errors: kesişen altyapı.

Üç şey açıkça yasaktır ve bir lint kuralı bunları zorunlu kılar:

  • domain/ içinde react, react-native veya expo-* import etmek
  • presentation/ içinde doğrudan HTTP veya storage çağırmak
  • features/A'nın features/B'nin iç yapısını import etmesi

UI veriye ihtiyaç duyduğunda ne olur

Tek bir istek öngörülebilir bir yol izler. Presentation asla bir datasource ile konuşmaz; bir use case'e sorar, use case bir port'a sorar ve port — başlangıçta — I/O'yu yapan ve DTO'yu bir domain varlığına eşleyen somut bir repository'ye bağlanır.

sequenceDiagram
  participant UI as Screen (Presentation)
  participant UC as Use Case (Domain)
  participant Port as Repository Port (Domain)
  participant Repo as Repository Impl (Data)
  participant DS as Datasource
  UI->>UC: call(input)
  UC->>Port: request(params)
  Port->>Repo: bound via DI
  Repo->>DS: fetch DTO
  DS-->>Repo: DTO
  Repo-->>UC: Domain entity (mapped)
  UC-->>UI: result

Port bir arayüz olduğu için, arkasındaki datasource REST'ten Firebase'e, oradan bir çevrimdışı önbelleğe Domain ya da Presentation fark etmeden değişebilir.

İki geliştiricinin paylaşabileceği bir klasör yapısı

Ağaç, katmanları tam olarak yansıtır; böylece sahiplik yoldan bellidir:

src/
  app/                 # expo-router routes
  composition/         # DI wiring only  →  container.ts, modules/*.module.ts
  core/                # http · storage · logging · errors · config
  shared/              # UI kit + generic utils + theme
  features/
    auth/
      presentation/    # screens · components · hooks
      domain/          # entities · ports · usecases
      data/            # datasources · repositories · mappers
    profile/           # ...aynı desen...

Her yeni özellik aynı şablondan basılır; kod tabanını ölçeklenebilir tutan asıl mekanizma budur:

flowchart TD
  X["features/x"] --> D["domain"]
  X --> DT["data"]
  X --> P["presentation"]
  D --> D1["ports"]
  D --> D2["usecases"]
  D --> D3["entities"]
  DT --> T1["datasources"]
  DT --> T2["repositories"]
  DT --> T3["mappers"]
  P --> P1["screens"]
  P --> P2["components"]
  P --> P3["hooks"]

Dev A features/auth/**'ı kurarken Dev B features/profile/**'ı kurabilir ve tek ortak zemin — core/, composition/, shared/ — küçük ve bilinçli olarak kontrol edilir.

Elle DI, sihir yok

Bağımlılıklar elle bağlanır. composition/container.ts nesne grafiğini başlangıçta bir kez kurar ve tek bir useDeps() hook'u üzerinden sunar. Composition yalnızca nesneleri bağlar — iş mantığı içermez — ve özellikler ham repository değil, use case dışa aktarır.

// composition/container.ts — bir kez kurulur, context ile sunulur.
export function buildContainer() {
  const http = new HttpClient(config.apiBaseUrl);
  const logger = new Logger();

  const authRepo = new AuthRepositoryImpl(new AuthRemoteDatasource(http));
  return {
    auth: { signIn: makeSignInUseCase(authRepo, logger) },
  };
}

Yansıma yok, decorator yok, konteyner kütüphanesi yok — sadece nesne döndüren fonksiyonlar. Sıkıcı, ve sıkıcı olması da amaç.

State'in nerede yaşayacağı

State hataları, state'in yanlış yerde yaşamasından doğar. Karar mekaniktir:

flowchart TD
  Q{"What kind of state?"}
  Q -->|Server-owned| RQ["React Query"]
  Q -->|UI / ephemeral| Z["Zustand"]
  Q -->|Persisted / offline| S["Storage datasource + repository"]
  Q -->|Secrets| SS["Secure store only"]
State Yeri Kural
Sunucuya ait React Query yanıtları global store'lara kopyalama
UI / geçici Zustand toggle'lar, sihirbazlar, geçici akışlar
Kalıcı / çevrimdışı storage repository ad hoc değil, datasource olarak
Sırlar secure store asla MMKV, asla log

Birinci sınıf domain kavramları olarak hatalar

Ham ağ hataları asla UI'ye ulaşmaz. core/errors içinde bir kez, anlam taşıyan tipli domain hatalarına normalize edilir:

Katman Biçim Örnek
Domain tipli, anlam odaklı InvalidCredentials
Data ham hatalar normalize HTTP 401 → InvalidCredentials
Presentation dostça mesaj + yeniden dene "Hatalı e-posta veya parola"

Kural: bir HTTP hatasından doğrudan alert(error.message) asla yok.

İki geliştirici, az çakışma

Bilgi silolarını önlemek için sahiplik her sprint döner ve PR'lar dar kalır — PR başına bir özellik modülü. core/ ve birkaç özelliğe dokunan bir PR, bölmek için bir işarettir. Her PR şu kontrol listesini geçer:

  • domain/ → React/Expo import yok
  • UI doğrudan HTTP/storage çağırmıyor
  • Bir özellik, başka bir özelliğin iç yapısını import etmiyor
  • DI bağlama yalnızca composition/ içinde
  • Use case + mapper için testler
  • Log'larda sır veya token yok

İlk beş gün

Başlangıç bilinçli olarak, sınırlar özelliklerden önce var olacak şekilde sıralanır:

timeline
  title First five working days
  Day 1 Foundation : Repo and TS strict : core http client : DI container
  Day 2 App shell : expo-router groups : theme baseline : error boundary
  Day 3 Auth slice : port and use case : datasource and mapper : screen and hook
  Day 4 Profile slice : React Query caching policy
  Day 5 Hardening : error normalization : CI boundary lint rules

Container ve DI sağlayıcısını önce kur — "singleton yayılmasını" bu önler — sonra ölçeği büyütmeden önce döngüyü tek bir dikey dilimle kanıtla.

Yapay zekâ ajanının yapabildiği (ve yapamadığı)

Bir ajanın sıkıcı kısımları hızlandırmasına izin veriyoruz — özellikleri iskeletleme, bir sözleşmeden repository implementasyonu, test yazma, sınır ihlalleri için PR incelemesi. Ama katı kısıtlar altında çalışır: sınır kurallarını koru, özellikler arası bağlaşımdan kaçın, minimal diff'leri tercih et ve varsayımı sesli söylemeden asla bir API sözleşmesi uydurma.

Çıkarımlar

  • Bağımlılık kuralı tüm oyundur: her şeyi çerçevesiz bir Domain'e doğru içeriye yönelt.
  • Özellik modülleri + elle DI, paralel çalışmayı ucuz ve altyapı takasını yerel kılar.
  • State'i ait olduğu yere koy, hataları bir kez normalize et, PR'ları tek modülde tut.

Mimari, diagramlar değildir — onların ucuzlattığı değişiklikler kümesidir. Bu mimari, "arka ucu takas et" ve "merge savaşı olmadan özellik ekle"nin ikisini de sıkıcı kılar. Amaç da bu.