Mobil uygulamaları nasıl kuruyoruz: Expo için bir Clean Architecture el kitabı
Ç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-nativeveyaexpo-*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çindereact,react-nativeveyaexpo-*import etmekpresentation/içinde doğrudan HTTP veya storage çağırmakfeatures/A'nınfeatures/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.
Diğer yazılar
Devlog: preunec.de yeniden inşa edildi
preunec.de'yi baştan sona yeniden inşa ettik. İşte bir ziyaretçi olarak sizin için ne değiştiği — ve nasıl çalıştığına dair anlaşılır bir perde arkası.
OkuBir Web Sitesini Kapatılabilir Modüllere Dönüştürmek
Bu sitenin bölümleri çalışma anında açılıp kapatılabilen modüllere nasıl dönüştü — ve bu altyapı neden buna değer.
Okuİyi Bir AB Projesi Yaygınlaştırma Sitesini Ne Belirler
Erasmus+ konsorsiyumları için çok dilli yaygınlaştırma platformları geliştirirken öğrendiklerimiz.
Oku