Skip to content
~/sph.sh

RFC'den Production'a: Implementation Hakkında Anlatmadıkları

Güzel RFC tasarımları ile karmaşık production gerçekliği arasındaki boşluk üzerine samimi bir değerlendirme ve bildirim sistemleri örneğinden gerçek dersler

Özet

RFC'ler production'la karşılaştığında nadiren değişmeden kalır ve bu mutlaka bir problem değil. Bildirim sistemi implementasyonlarını inceleyerek, zarif tasarımların organizasyonel kısıtlar, timeline baskıları ve beklenmeyen gereksinimlerle karşılaştığında nasıl evrimleştiğini öğrenebiliriz. Bu inceleme, teorik tasarım ile pratik implementasyon arasındaki boşluğu kapatmaya yardımcı olan kalıpları ortaya çıkarır.

Durum: Güzel RFC vs Production Gerçekliği

Güzelce hazırlanmış bir RFC okurken, zarif mimari diyagramlara bakıp "İşte bu, sonunda mükemmel çalışacak tasarım bu" diye düşündüğün o anı bilir misin? Sonra altı ay sonra kendini production sorunlarının içinde bulursun, timeline iki katına çıkmıştır ve o tertemiz database şeması sanki blender'dan geçmiş gibi görünür.

Bu kalıp sistem implementasyonlarında tekrar tekrar ortaya çıkar. RFC ile production arasındaki boşluk bir bug değil - gerçek takımlarla, gerçek iş baskıları altında karmaşık sistemler inşa etmenin bir özelliği. Bu boşluğu anlamak daha etkili planlama yapmamıza ve gerçekçi beklentiler belirlememize yardımcı olur.

Not: Aşağıdaki örnekler farklı organizasyonlardaki birden fazla bildirim sistemi implementasyonundan uyarlanmıştır. Spesifik detaylar değişebilse de, açıklanan kalıplar ve zorluklar bu alandaki yaygın deneyimleri temsil eder.

Görev: RFC'den Gerçekliğe Bildirim Sistemi İnşa Etmek

Her RFC iyimserlikle başlar. Aklımdaki bildirim sistemi RFC'si bir şaheserdi: temiz mimari diyagramlar, kapsamlı database şemaları, aşamalı dağıtım planları. Yaşadığımız her bildirim sorununu çözeceğini vaat ediyordu:

typescript
// RFC'nin güzel vaadiinterface NotificationSystemGoals {  deliveryTime: 'in-app için <100ms, email için <5s',  throughput: 'Saniyede 10,000+ bildirim',  uptime: '%99.9 erişilebilirlik',  timeline: '2 developer ile 12 hafta',  budget: '$120,000-180,000'}
// Gerçekte olaninterface ProductionReality {  deliveryTime: 'İyi günlerde in-app için 2-3s, peak'lerde 30s+',  throughput: '500/sn ile başladı, 5,000/sn\'ye ulaşmak 6 ay sürdü',  uptime: 'İlk çeyrek %97, bir yıl sonra %99',  timeline: '4 developer + 2 contractor ile 8 ay',  budget: '$400,000+ ve maintenance maliyetleri hala devam ediyor'}

RFC kusursuz görünüyordu. Her şeyi düşünmüştük: rate limiting, deduplication, preference management, hatta sessiz saatler bile. Aşamalı yaklaşım muhafazakar görünüyordu - core infrastructure için 4 hafta kesinlikle yeterli olmalıydı?

Aksiyon: Implementasyon Zorlukları ve Adaptasyonlar

Database Şeması Evrimi

RFC'nin database şeması güzelliğin ta kendisiydi. Temiz, normalize edilmiş, düzgün foreign key'ler ve constraint'ler. RFC'nin önerdiği buydu:

sql
-- RFC'nin tertemiz şemasıCREATE TABLE notification_events (    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),    user_id UUID REFERENCES users(id) ON DELETE CASCADE,    notification_type VARCHAR(100) NOT NULL,    template_id UUID REFERENCES notification_templates(id),    data JSONB DEFAULT '{}',    status VARCHAR(20) DEFAULT 'pending',    sent_at TIMESTAMP,    delivered_at TIMESTAMP,    read_at TIMESTAMP,    created_at TIMESTAMP DEFAULT NOW());

Production'a geçtikten üç ay sonra, o tablo aslında şöyle görünüyordu:

sql
-- 20+ migration sonrası production gerçekliğiCREATE TABLE notification_events (    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),    user_id UUID, -- Performance sorunları nedeniyle foreign key kaldırıldı    notification_type VARCHAR(100),    notification_type_v2 VARCHAR(255), -- Migration devam ediyor    template_id UUID,    template_id_v2 BIGINT, -- Farklı takım farklı ID tipi kullandı    data JSONB DEFAULT '{}',    data_compressed BYTEA, -- JSONB çok büyüyünce eklendi    status VARCHAR(20) DEFAULT 'pending',    status_v2 VARCHAR(50), -- Beklenenden fazla status    priority INTEGER DEFAULT 0, -- RFC'de yok, production için kritik    retry_count INTEGER DEFAULT 0, -- RFC'de yok, debugging için şart    channel VARCHAR(50), -- Query performance için denormalize edildi    correlation_id UUID, -- Distributed tracing için eklendi    partition_key INTEGER, -- Sharding için eklendi    sent_at TIMESTAMP,    delivered_at TIMESTAMP,    read_at TIMESTAMP,    failed_at TIMESTAMP, -- RFC'de yok, çok gerekli    expires_at TIMESTAMP, -- RFC'de yok, sonsuz büyümeyi önledi    created_at TIMESTAMP DEFAULT NOW(),    updated_at TIMESTAMP DEFAULT NOW() -- Debugging kabuslarından sonra eklendi);
-- Tahmin etmediğimiz 15 indexCREATE INDEX CONCURRENTLY idx_notification_events_user_created ON notification_events(user_id, created_at DESC) WHERE status != 'deleted';CREATE INDEX CONCURRENTLY idx_notification_events_correlation ON notification_events(correlation_id) WHERE correlation_id IS NOT NULL;-- ... ve 13 tane daha

Bu şema değişikliklerinin her biri bir production incident'ı, bir performance krizi veya tahmin edemediğimiz bir feature request'i temsil ediyor. Tertemiz RFC tasarımı gerçeklikle buluştu ve gerçeklik kazandı.

WebSocket Connection Management Karmaşıklığı

RFC kendinden emin bir şekilde real-time bildirimleri WebSocket'lerle handle edeceğimizi söylüyordu. Örnek kod çok temiz görünüyordu:

typescript
// RFC'nin WebSocket implementation'ıclass NotificationWebSocketManager {  private connections: Map<string, WebSocket> = new Map();    async sendNotification(userId: string, notification: NotificationEvent) {    const connection = this.connections.get(userId);    if (connection && connection.readyState === WebSocket.OPEN) {      connection.send(JSON.stringify({        type: 'notification',        data: notification      }));    }  }}

Altı ay sonra, mobil uygulama deployment'ının yanlış gittiği meşhur "50,000 zombie connection" felaketi dahil birden fazla production incident'ından sonra, elimizde aslında şu vardı:

typescript
// Tüm edge case'lerle production gerçekliğiclass NotificationWebSocketManager {  private connections: Map<string, Set<WebSocketConnection>> = new Map();  private connectionMetadata: Map<string, ConnectionMetadata> = new Map();  private healthChecks: Map<string, NodeJS.Timeout> = new Map();  private rateLimiters: Map<string, RateLimiter> = new Map();  private deadLetterQueue: Queue<FailedNotification>;  private circuit: CircuitBreaker;    async sendNotification(userId: string, notification: NotificationEvent) {    // 200+ satır defensive programming    const connections = this.connections.get(userId);    if (!connections || connections.size === 0) {      await this.queueForLaterDelivery(userId, notification);      return;    }        // Kullanıcı başına birden fazla connection (mobile + web + tablet)    const results = await Promise.allSettled(      Array.from(connections).map(async (conn) => {        try {          // Connection sağlığını kontrol et          if (!this.isConnectionHealthy(conn)) {            await this.reconnectOrEvict(conn);            throw new Error('Unhealthy connection');          }                    // Connection başına rate limiting          const limiter = this.getRateLimiter(conn.id);          if (!await limiter.tryAcquire()) {            await this.backpressure(conn, notification);            return;          }                    // Cascading failure'lar için circuit breaker          return await this.circuit.fire(async () => {            // Mesaj boyutu validasyonu (bunu zor yoldan öğrendik)            const message = this.serializeNotification(notification);            if (message.length > MAX_MESSAGE_SIZE) {              const chunks = this.chunkMessage(message);              for (const chunk of chunks) {                await this.sendChunk(conn, chunk);              }            } else {              await this.sendMessage(conn, message);            }          });        } catch (error) {          await this.handleDeliveryFailure(conn, notification, error);        }      })    );        // Delivery metriklerini takip et    await this.recordDeliveryMetrics(userId, notification, results);  }    // Edge case'leri handle etmek için 50+ başka method}

Bu eklemelerin her biri bir production incident'ından geldi. Circuit breaker? Redis cluster'ımızı çökerttiğimizde eklendi. Chunking logic? Gömülü resimli bir marketing bildirimi mobile client'ları çökerttiğinde keşfettik. Rate limiting? 100,000 support ticket üreten bir notification storm sırasında öğrendik.

Zaman Çizelgesi ve Kapsam Evrimi

RFC'nin aşamalı yaklaşımı çok mantıklı görünüyordu:

  • Phase 1 (Hafta 1-4): Core Infrastructure
  • Phase 2 (Hafta 5-8): Advanced Features
  • Phase 3 (Hafta 9-12): Integration & Optimization

Gerçekte olan:

Hafta 1-4: Infrastructure Sürprizi

Core infrastructure inşa etmek yerine, üç hafta sadece environment'ları ayarlamak ve "standart" database setup'ımızın write throughput'u kaldıramadığını keşfetmekle geçti. 4. hafta tamamen başka bir sistemdeki production incident ile takımımızı çeken bir olayla tükendi.

Hafta 5-12: Scope Creep Senfonisi

Product management, erken demo'ları görünce heyecanlandı. "Slack bildirimleri ekleyebilir miyiz?" Tabii, çok zor değil. "Kritik alert'ler için SMS?" Mantıklı. "Ah, bir de marketing kampanyaları için 90 gün önceden bildirim schedule etmeyi desteklememiz gerekiyor." Pardon, ne?

typescript
// Orijinal scopeconst originalChannels = ['in_app', 'email', 'push'];
// 3. ay scope'uconst actualChannels = [  'in_app',   'email',   'push',   'sms',          // Hafta 6'da eklendi  'slack',        // Hafta 8'de eklendi  'teams',        // Hafta 10'da eklendi  'webhook',      // Hafta 11'de eklendi  'discord',      // Hafta 14'te eklendi (evet, çoktan gecikmiştik)  'voice_call'    // Hafta 20'de eklendi (kritik güvenlik alert'leri için)];

Ay 4-6: Integration Cehennemi

RFC'deki o temiz API tasarımını hatırlıyor musun? Tüm servislerimizin aynı authentication sistemini kullandığını varsayıyordu. Plot twist: kullanmıyorlardı. Üç farklı auth sistemimiz vardı (JWT, OAuth2 ve legacy session-based sistem) ve bildirimlerin hepsiyle çalışması gerekiyordu.

typescript
// RFC varsayımıinterface AuthContext {  userId: string;  token: string;}
// Production gerçekliğitype AuthContext =   | { type: 'jwt'; userId: string; token: string; claims: JWTClaims }  | { type: 'oauth2'; userId: string; accessToken: string; refreshToken: string; expiresAt: Date }  | { type: 'legacy'; sessionId: string; userId?: string; cookieData: LegacyCookie }  | { type: 'service_account'; serviceId: string; apiKey: string }  | { type: 'anonymous'; temporaryId: string; ipAddress: string };
// Her auth tipi farklı handling, farklı rate limit, // farklı güvenlik kontrolleri ve farklı audit logging gerektiriyordu

Ay 7-8: Performance Hesaplaşması

Sistem "çalışıyordu" ama production yükünü kaldıramıyordu. RFC saniyede 10,000 bildirim vaat ediyordu. Biz 500 ile uğraşıyorduk. Sonraki iki ay profiling, caching, query optimization ve mimari değişikliklerin bulanıklığıydı.

En büyük sürpriz? Darboğaz beklediğimiz yerde değildi (database write'ları). Template rendering'di. Süslü personalization sistemimiz kullanıcı context'i toplamak için bildirim başına 20+ API çağrısı yapıyordu.

Takım Ölçekleme ve Organizasyonel Değişiklikler

RFC "12 hafta için 2 developer" diyordu. Gerçekte kim çalıştı:

  • 2 senior engineer (full-time olması gerekiyordu, production support yüzünden aslında %60)
  • 1 junior engineer (2. ayda eklendi, 3. ayı codebase'i öğrenmekle geçirdi)
  • 2 contractor (4. ayda "hızlı kazançlar" için eklendi, 5. ayı kodlarını düzeltmekle geçirdik)
  • 1 DevOps engineer (sözde "danışmanlık", 3. ayda aslında full-time)
  • 1 database uzmanı (5. ayda performance krizi için getirildi)
  • Product manager (proje boyunca iki kez değişti)
  • 3 farklı engineering manager (6. ayda reorg oldu)

Her takım değişikliği context kaybı, mimari tartışmaları ve yeniden çalışma anlamına geliyordu. Contractor kodu code review'da iyi görünüyordu ama hala ödediğimiz teknik borç yarattı. 6. aydaki reorg, yeni engineering manager "mimariyi yeniden gözden geçirmek" istediğinde projeyi neredeyse öldürüyordu.

Monitoring Gereksinimleri Keşfi

RFC'nin monitoring üzerine bir bölümü vardı. Delivery rate, response time ve error rate gibi metrikleri listeliyordu. Mantıklı, değil mi? Production'da gerçekten monitor etmemiz gerekenler:

typescript
// RFC monitoring planıconst plannedMetrics = [  'delivery_rate',  'response_time',   'error_rate',  'throughput'];
// Gerçekte monitor ettiğimizconst productionMetrics = [  // Temel metrikler (RFC'den)  'delivery_rate_by_channel_by_priority_by_user_segment',  'response_time_p50_p95_p99_p999',  'error_rate_by_type_by_service_by_retry_count',    // Gerçekten önemli olan metrikler  'template_render_time_by_template_by_variables_count',  'database_connection_pool_wait_time',  'redis_operation_time_by_operation_type',  'webhook_retry_backoff_effectiveness',  'notification_staleness_at_delivery',  'user_preference_cache_hit_rate',  'deduplication_effectiveness_by_time_window',  'rate_limit_rejection_by_reason',  'circuit_breaker_state_transitions',  'message_size_distribution_by_channel',  'websocket_reconnection_storms',  'push_token_invalidation_rate',  'email_bounce_classification',  'notification_feedback_loop_latency',  'cost_per_notification_by_channel',  'regulatory_compliance_audit_completeness',    // Spesifik incident'lardan sonra ihtiyaç duyduğumuz garip metrikler  'mobile_app_version_vs_notification_compatibility',  'timezone_calculation_accuracy',  'emoji_rendering_failures_by_client',  'notification_delivery_during_database_failover',  'memory_leak_in_template_cache',  'thundering_herd_detection'];

Bu metriklerin her biri bir şeyler ters gittiği için var ve çok geç olana kadar geleceğini görmedik.

Teknik Borç Birikim Desenleri

RFC teknik borçtan bahsetmiyordu. 8. aya geldiğimizde uğraştığımız şey:

Template System Frankenstein'ı

Basit bir template sistemi ile başladık. Production'a geldiğimizde, farklı takımların farklı gereksinimleri olduğu ve hiç birleştirmeye vaktimiz olmadığı için aynı anda çalışan üç farklı template engine'imiz vardı.

typescript
// Hala ödediğimiz teknik borçclass NotificationTemplateManager {  private mustacheTemplates: Map<string, MustacheTemplate>;    // Orijinal sistem  private handlebarsTemplates: Map<string, HandlebarsTemplate>; // Marketing için eklendi  private reactEmailTemplates: Map<string, ReactEmailTemplate>; // Güzel email'ler için eklendi    async render(templateId: string, data: any): Promise<string> {    // Hangi template engine'i kullanacağını bulmak,    // edge case'leri handle etmek, backward compatibility sağlamak,    // ve production'ı bozmadan düzeltemediğimiz bug'ları atlatmak için    // 150 satır logic        // Bu yorum 4. aydan beri burada:    // TODO: Template sistemlerini birleştir (tahmini: 2 hafta)    // Araştırmadan sonraki gerçek tahmin: 3 ay + migration planı  }}

Hiç Bitmeyen Migration

O güzel database şemasını hatırlıyor musun? Altı aydır "v2"ye migrate ediyoruz. Her iki şemayı da paralel çalıştırıyoruz, ara sıra bildirim kaybeden karmaşık bir sync sistemi ile.

sql
-- Migration kabusuBEGIN;  -- Migration planındaki 47 adımdan 1. adım  INSERT INTO notification_events_v2   SELECT     id,    user_id,    -- 50 satır karmaşık transformation logic    CASE       WHEN notification_type IN ('old_type_1', 'old_type_2') THEN 'new_type_1'      WHEN notification_type LIKE 'legacy_%' THEN REPLACE(notification_type, 'legacy_', 'classic_')      -- 20 WHEN clause daha    END as notification_type_v2,    -- Daha fazla transformation...  FROM notification_events   WHERE created_at > NOW() - INTERVAL '1 hour'    AND status != 'migrated'    AND NOT EXISTS (      SELECT 1 FROM notification_events_v2       WHERE notification_events_v2.id = notification_events.id    );    -- Migration status'ü güncelle  UPDATE migration_status   SET last_run = NOW(),       records_migrated = records_migrated + row_count,      estimated_completion = NOW() + (remaining_records / current_rate * INTERVAL '1 second')  WHERE migration_name = 'notification_schema_v2';    -- Conflict'leri kontrol et  -- Rollback senaryolarını handle et  -- Monitoring metriklerini güncelle  -- 100 satır daha...COMMIT;

Sonuç: Implementasyon Deneyiminden Dersler

RFC net success kriterleri tanımlamıştı: %99.9 uptime, <100ms delivery, saniyede 10,000 bildirim. Sonunda bu sayılardan bazılarına ulaştık, ama yanlış metrikler olduğu ortaya çıktı.

Aslında önemli olan:

  • Kullanıcı mutluluğu: %99 delivery rate'imiz vardı ama kullanıcılar kötü zamanlandığı için bildirimleri sevmiyordu
  • Developer verimliliği: Diğer takımlar "temiz" API'mıza kapsamlı yardım olmadan entegre olamıyordu
  • Operasyonel yük: Tüm otomasyonumuza rağmen sistem sürekli bakım gerektiriyordu
  • İş değeri: Marketing özelliklerin yarısını kullanamıyordu çünkü çok karmaşıktı
typescript
// Optimize ettiğimiz (RFC'den)const technicalMetrics = {  uptime: 99.9,  deliveryTime: 95, // ms  throughput: 10000, // saniyede  errorRate: 0.1 // yüzde};
// Aslında önemli olanconst businessMetrics = {  userNotificationDisableRate: 45, // yüzde - çok yüksek  developerIntegrationTime: 3, // hafta - saat olmalı  supportTicketsPerWeek: 150, // bildirimlerle ilgili  marketingCampaignSetupTime: 2, // gün - dakika olmalı  monthlyOperationalCost: 25000, // dolar - tahminin 5 katı  engineersPagedPerWeek: 12 // kez - sürdürülemez};

Anahtar Implementasyon İçgörüleri

Bildirim sistemi implementasyonlarında tutarlı olarak birkaç kalıp ortaya çıkar:

1. RFC'ler Hipotez, Spesifikasyon Değil

RFC'ni başlangıç hipotezi olarak değerlendir. Production'a çarptığı an, sürekli revizyon gerektiren yaşayan bir belge haline gelir. RFC'mizi çok uzun süre "spec" olarak dondurmuştuk, gerçeklik ayrıldığında sonsuz karışıklığa neden oldu.

2. Bilinmeyen Bilinmeyenler İçin Bütçe Ayır

Timeline ve bütçen ne olursa olsun, iki katına çıkar, sonra bilmediğini bilmediğin şeyler için %50 ekle. Bu kötümserlik değil; düzinelerce projeden pattern tanıma.

3. İlk Günden Migration İçin Tasarla

Her güzel şema migration'a ihtiyaç duyacak. Her temiz API versiyonlamaya ihtiyaç duyacak. Her basit sistem backward compatibility'ye ihtiyaç duyacak. Bu yetenekleri baştan inşa et, sonradan düşünme.

4. Edge Case'ler Norm'dur

RFC review'da tartıştığın o edge case? Herkesin "ortaya çıkarsa hallederiz" dediği? Ortaya çıkacak, muhtemelen production'da, muhtemelen en kötü zamanda. Tartışıyorsan, edge case değildir.

5. Organizasyonel Dinamikler Teknik Mükemmelliği Yener

En iyi teknik tasarım takım dinamiklerini, politik gerçeklikleri ve organizasyonel kısıtlamaları hesaba katmazsa başarısız olur. 3. ayda katılan contractor senin güzel mimarini umursamıyor. 6. aydaki reorg her kararı sorgulayacak.

6. Gerçekten Debug Edeceğin Şeyleri Monitor Et

RFC'nin söylediğini monitor etme. Her şey yanarken bir incident sırasında neye ihtiyacın olacağını monitor et. Bu business metrikler, kullanıcı deneyimi metrikleri ve detaylı operasyonel metrikler demek, sadece teknik istatistikler değil.

Tasarım ve Implementasyon Arasında Köprü Kurma

Peki RFC ile production arasındaki boşluğu nasıl kapatırız? Yıllarca farklı yaklaşımları denedikten sonra, gerçekten işe yarayan:

Minimum Sevilebilir Ürün ile Başla

Minimum yaşayabilir değil - minimum sevilebilir. Kullanıcıların gerçekten kullanmak istediği küçük bir şey inşa et, sonra iterate et. Bildirim sistemimiz her şeyi bir kerede inşa etmeye çalışmak yerine sadece mükemmel çalışan email bildirimleri ile başlasaydık daha iyi olurdu.

Mükemmellik İçin Değil, Değişim İçin Tasarla

Database şeman değişecek. API'n evrimleşecek. Mimarin kayacak. Mükemmel son durumu tahmin etmeye çalışmak yerine zarif bir şekilde evrimleşebilen sistemler tasarla.

Developer Experience'a Erken Yatırım Yap

Sistemin entegre edilmesi ve işletilmesi ne kadar kolaysa, o kadar başarılı olur. Performance optimize ederken aylar harcadık, API'yi kullanımı kolaylaştırıyor olmalıydık.

Yaşayan Dokümantasyon Oluştur

O RFC tarihi bir eser olmamalı. Sistemle birlikte evrimleşmeli. Artık RFC'lerimizi "Orijinal Tasarım", "Mevcut Implementation" ve "Öğrenilen Dersler" bölümleriyle yaşayan belgeler olarak tutuyoruz.

Her Seviyede Feedback Loop'ları İnşa Et

Kullanıcı feedback'inden operasyonel metriklere, developer deneyim anketlerine kadar, sürecine feedback loop'ları inşa et. Neyin çalışmadığını ne kadar hızlı öğrenirsen, o kadar hızlı düzeltebilirsin.

Sonuç: Implementasyon Gerçekliğini Kucaklamak

Implementasyon evrimine karşı çalışmak yerine onunla çalışmayı öğrenmek sonuçları iyileştirir. Tertemiz RFC'ler kullanıcı ihtiyaçlarını ele aldıkça doğal olarak karmaşık hale gelir. Güzel mimariler pratik uzantılar geliştirir. Temiz codebase'ler gerekli teknik borç biriktirir. Bu başarılı problem çözmeyi temsil eder, tasarım başarısızlığını değil.

RFC-production boşluğu eliminasyon değil yönetim gerektirir. Etkili engineering sistem uyumunu ve kullanıcı değerini korurken ortaya çıkan gerçekliğe adapte olur.

Bildirim sistemi implementasyonlarını yansıttığımızda, final sistemler nadiren ilk tasarımlarla eşleşir. Tipik olarak daha karmaşıktırlar ve inşa etmesi daha uzun sürer, ama aynı zamanda daha yeteneklidirler ve ilk planlama sırasında belirgin olmayan problemleri çözerler.

RFC yazarken şunu hatırla: sabit spesifikasyonlar tanımlamaktan ziyade implementasyon gerçekliği ile bir konuşma başlatıyorsun. Bu perspektif daha iyi planlama ve daha gerçekçi beklentileri sağlar.

Projelerinde RFC-production evrimini deneyimledin mi? Implementasyon zorluklarından hangi dersleri öğrendin ve tasarım-gerçeklik geçişini yönetmeye hangi stratejiler yardımcı oluyor?

İlgili Yazılar