Instruction file imported from dursunkoc/demo1 (
.github/instructions/012-clean-code-concurrency.instructions.md). Copyright stays with the author.
Clean Code: Concurrency
Robert Martin'in "Clean Code" kitabının Chapter 13: Concurrency bölümünden türetilen kurallar.
"Writing clean concurrent programs is hard — very hard."
JavaScript/TypeScript tek thread'li (Node.js event loop) olmakla birlikte, asenkron kod ve paralel işlemler temiz concurrency prensiplerini gerektirir.
1. Concurrency Mitleri ve Gerçekler
Mit → Gerçek
| Mit | Gerçek |
|---|---|
| Concurrency her zaman performansı artırır | Bazen, sadece bekleme süresi varsa |
| Concurrent programlar tasarım açısından değişmez | Tasarımı ciddi ölçüde değiştirebilir |
| Container/framework tüm concurrency sorunlarını çözer | Ne yapıldığını anlamak hâlâ gerekli |
2. Concurrency'i Diğer Koddan Ayır (Keep Concurrency-Related Code Separate)
Kötü — İş mantığı ve async yönetimi karışık
async function processOrders(): Promise<void> {
const orders = await db.orders.findPending();
// İş mantığı ve async retry logic iç içe
for (const order of orders) {
let attempts = 0;
let success = false;
while (!success && attempts < 3) {
try {
await paymentService.charge(order);
await db.orders.markPaid(order.id);
await emailService.sendConfirmation(order.userId);
success = true;
} catch (error) {
attempts++;
await sleep(1000 * attempts);
}
}
if (!success) await db.orders.markFailed(order.id);
}
}
İyi — Sorumluluklar ayrı
// İş mantığı — async yönetimden bağımsız
async function processOrder(order: Order): Promise<void> {
await paymentService.charge(order);
await db.orders.markPaid(order.id);
await emailService.sendConfirmation(order.userId);
}
// Retry logic — ayrı utility
async function withRetry<T>(
operation: () => Promise<T>,
maxAttempts = 3
): Promise<T> {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await operation();
} catch (error) {
if (attempt === maxAttempts) throw error;
await sleep(1000 * attempt);
}
}
throw new Error("Unreachable");
}
// Orchestration — her şeyi bir araya getirir
async function processOrders(): Promise<void> {
const orders = await db.orders.findPending();
await Promise.allSettled(
orders.map(order =>
withRetry(() => processOrder(order))
.catch(() => db.orders.markFailed(order.id))
)
);
}
3. Paylaşılan Veriyi Koru (Limit the Scope of Shared Data)
Kural
- Paylaşılan mutable state → race condition kaynağı.
- Paylaşımı minimize et; paylaşmak zorundaysan immutable yap.
// Kötü: paylaşılan mutable state
class Counter {
count = 0; // aynı anda birden fazla yerde değiştiriliyor
increment(): void { this.count++; }
}
// İyi: React'ta — immutable state update
function useCounter() {
const [count, setCount] = useState(0);
// setState callback form → her zaman son state üzerinden işlem
const increment = useCallback(() => {
setCount(prev => prev + 1); // closure ile stale state riski yok
}, []);
return { count, increment };
}
4. Veri Kopyalarını Kullan (Use Copies of Data)
Paylaşılan veriyi kopyalayıp bağımsız işle, sonuçları tek thread'de birleştir:
// Redux/Immer pattern — state immutable kopyalanıyor
const productSlice = createSlice({
name: "products",
initialState,
reducers: {
updatePrice: (state, action: PayloadAction<{ id: string; price: number }>) => {
// Immer sayesinde draft üzerinde "mutasyon" → aslında yeni state
const product = state.items.find(p => p.id === action.payload.id);
if (product) {
product.price = action.payload.price; // draft mutasyonu → immutable sonuç
}
},
},
});
5. Paralel İşlemleri Doğru Yönet (JavaScript/TypeScript Spesifik)
5.1 Promise.all — Bağımsız işlemleri paralel çalıştır
// Kötü: sıralı (toplamda 3× gecikme)
async function loadDashboard(userId: string): Promise<Dashboard> {
const user = await userService.getUser(userId); // 100ms bekle
const orders = await orderService.getOrders(userId); // 100ms bekle
const products = await productService.getFeatured(); // 100ms bekle
return buildDashboard(user, orders, products);
}
// İyi: paralel (toplamda ~100ms)
async function loadDashboard(userId: string): Promise<Dashboard> {
const [user, orders, products] = await Promise.all([
userService.getUser(userId),
orderService.getOrders(userId),
productService.getFeatured(),
]);
return buildDashboard(user, orders, products);
}
5.2 Promise.allSettled — Hataları tolere et
// Bir servis başarısız olursa diğerlerini engelleme
async function loadProductDetails(productId: string): Promise<ProductDetailPageData> {
const [productResult, reviewsResult, relatedResult] = await Promise.allSettled([
productService.getById(productId),
reviewService.getProductReviews(productId),
productService.getRelated(productId),
]);
if (productResult.status === "rejected") {
throw new NotFoundError(`Product ${productId}`);
}
return {
product: productResult.value,
reviews: reviewsResult.status === "fulfilled" ? reviewsResult.value : [],
relatedProducts: relatedResult.status === "fulfilled" ? relatedResult.value : [],
};
}
5.3 Race Condition'a Dikkat — useEffect Cleanup
// Kötü: eski request sonucu yeni isteği geçersiz kılabilir (stale closure)
function ProductSearch() {
const [results, setResults] = useState<Product[]>([]);
useEffect(() => {
searchProducts(query).then(setResults); // cleanup yok → stale result
}, [query]);
}
// İyi: cleanup ile iptal et
function ProductSearch({ query }: { query: string }) {
const [results, setResults] = useState<Product[]>([]);
useEffect(() => {
let cancelled = false;
searchProducts(query).then(products => {
if (!cancelled) setResults(products); // sadece güncel istek sonucu kabul edilir
});
return () => { cancelled = true; }; // cleanup
}, [query]);
}
5.4 AbortController ile İptal
function useProducts(categoryId: string) {
const [products, setProducts] = useState<Product[]>([]);
useEffect(() => {
const controller = new AbortController();
fetch(`/api/products?category=${categoryId}`, { signal: controller.signal })
.then(res => res.json())
.then(data => setProducts(data))
.catch(err => {
if (err.name !== "AbortError") console.error(err); // abort hatalarını yoksay
});
return () => controller.abort(); // component unmount veya categoryId değişiminde iptal
}, [categoryId]);
return products;
}
6. Küçük Senkronizasyon Bölgeleri (Keep Synchronized Sections Small)
Synchronized/locked bölgeyi mümkün olduğunca küçük tut:
// Kötü: gereksiz yere büyük kritik bölge (Node.js mutex örneği)
class OrderService {
private mutex = new Mutex();
async processOrder(order: Order): Promise<void> {
const release = await this.mutex.acquire();
try {
// Tüm işlem kilitli — başka order işlenemiyor
const user = await userService.getUser(order.userId); // bu kilitlenmeli mi?
const product = await productService.get(order.productId); // bu?
await db.orders.save(order); // sadece bu kritik
await emailService.send(user.email, "Order confirmed"); // bu değil
} finally {
release();
}
}
}
// İyi: sadece kritik bölge kilitlenmiş
class StockService {
private mutex = new Mutex();
async reserveStock(productId: string, quantity: number): Promise<boolean> {
const release = await this.mutex.acquire();
try {
// Sadece bu atomik olmalı
const current = await db.stock.get(productId);
if (current < quantity) return false;
await db.stock.decrement(productId, quantity);
return true;
} finally {
release();
}
}
}
7. Async/Await İyi Pratikler
// Kötü: unhandled promise rejection
function loadData() {
fetchProducts(); // await yok, hata kaybolur
}
// Kötü: sequential await (gereksiz yavaşlık)
async function bad() {
const a = await fetchA(); // 100ms
const b = await fetchB(); // 100ms (a'yı beklemek zorunda değil)
}
// İyi: paralel await
async function good() {
const [a, b] = await Promise.all([fetchA(), fetchB()]); // 100ms toplam
}
// Kötü: async void (hata kaybolur)
const handleClick = async () => {
await doSomething(); // void döndüren async → hata kaybolur
};
// İyi: hata handle et
const handleClick = async () => {
try {
await doSomething();
} catch (error) {
setError(getErrorMessage(error));
}
};
8. Testlerle Concurrency Sorunlarını Yakala
// Eş zamanlı erişim testi
test("concurrent increments produce consistent results", async () => {
const counter = new AtomicCounter(0);
// 100 eş zamanlı increment
await Promise.all(Array.from({ length: 100 }, () => counter.increment()));
expect(counter.value).toBe(100);
});
// Race condition testi
test("only reserves stock once when concurrent requests arrive", async () => {
const stockService = new StockService({ stock: 1 });
const [result1, result2] = await Promise.all([
stockService.reserve("product-1", 1),
stockService.reserve("product-1", 1),
]);
// Sadece biri başarılı olmalı
const successes = [result1, result2].filter(r => r === true).length;
expect(successes).toBe(1);
});
Özet Kontrol Listesi
- Async/concurrency mantığı iş mantığından ayrılmış mı?
- Paylaşılan mutable state minimize edilmiş mi?
- State güncellemeleri immutable pattern ile mi yapılıyor?
- Bağımsız async işlemler
Promise.allile paralel çalışıyor mu? - Hata toleransı gereken yerlerde
Promise.allSettledkullanılıyor mu? -
useEffectiçinde async çağrı cleanup ile iptal ediliyor mu? -
AbortControllerile fetch iptal destekleniyor mu? -
async voidfonksiyon var mı? (Hata yutulmasın diye handle et) - Unhandled promise rejection var mı?
- Kritik bölgeler (mutex) mümkün olduğunca küçük mü?