Skip to content
~/sph.sh

Yapısal Patternler ve Component Composition

Decorator, Adapter, Facade, Composite ve Proxy patternlerinin React ve TypeScript'te nasıl evrildiğini keşfedin. HOC'ların ne zaman hook'lara yol verdiğini, adapterlerin third-party API'ları nasıl izole ettiğini ve facade'ların karmaşıklığı nasıl basitleştirdiğini öğrenin.

Yapısal patternler, objeler ve classlar arasındaki ilişkileri organize eder. Gang of Four, 1994'te Decorator, Adapter, Facade, Composite ve Proxy patternlerini C++ ve Smalltalk için belgeledi. Modern TypeScript ve React ekosistemlerinde bu patternler kaybolmadı - framework konvansiyonlarına, hook'lara ve type-safe wrapper'lara dönüştü.

Bu yazıda yapısal patternlerin React component composition'da nasıl ortaya çıktığını, higher-order componentlerin ne zaman hala önemli olduğunu ve TypeScript'in type system'ının klasik implementasyonları nasıl geliştirdiğini inceliyoruz.

Decorator Pattern: TypeScript'te Üç Farklı Anlam

"Decorator" terimi TypeScript ekosisteminde üç farklı şey ifade ediyor:

  1. Gang of Four Decorator Pattern: Objelere dinamik olarak davranış ekleme
  2. React Higher-Order Components (HOCs): Componentlere ek fonksiyonalite kazandırma
  3. TypeScript Decorator Syntax: Class/method decorator'lar için Stage 3 proposal

Her biri farklı problemleri çözüyor. Her yaklaşımın ne zaman değer kattığını inceleyelim.

React HOC'lar: Klasik Decorator Implementasyonu

Higher-order componentler, componentleri ek fonksiyonalite ile wrap ederek güçlendirir:

typescript
// Authentication HOCfunction withAuth<P extends object>(  Component: React.ComponentType<P>): React.FC<P> {  return (props: P) => {    const { user, loading } = useAuth();
    if (loading) {      return <div className="spinner">Yükleniyor...</div>;    }
    if (!user) {      return <Navigate to="/login" replace />;    }
    return <Component {...props} />;  };}
// Kullanımconst Dashboard = ({ data }: DashboardProps) => {  return <div>Dashboard'a hoş geldin</div>;};
export default withAuth(Dashboard);

Bu çalışıyor, ama HOC'lar sorunlar yaratıyor:

Wrapper Hell: Birden fazla HOC stack'lemek çok derin component tree'leri oluşturuyor:

typescript
export default withAuth(  withTracking(    withErrorBoundary(      withLoading(        Dashboard      )    )  ));

Props Collision: Birden fazla HOC aynı isimdeki prop'ları inject edebilir, çakışmalara sebep olur.

Ref Forwarding Karmaşıklığı: HOC katmanları arasında ref geçirmek explicit forwarding gerektirir. React 19'un forwardRef'i deprecated ettiğini unutma - ref'ler artık standart prop olarak geçirilebiliyor, bu pattern'i basitleştiriyor.

Belirsiz Data Flow: HOC'lar tarafından inject edilen prop'lar component signature'ında görünmüyor.

Modern Alternatif: Custom Hooks

Hook'lar aynı fonksiyonaliteyi daha temiz composition ile sağlıyor:

typescript
function Dashboard({ data }: DashboardProps) {  // Her hook belirli fonksiyonalite ekliyor  const { user, loading } = useAuth();  const tracking = usePageTracking('dashboard_view');  const errorBoundary = useErrorBoundary();
  if (loading) {    return <div className="spinner">Yükleniyor...</div>;  }
  if (!user) {    return <Navigate to="/login" replace />;  }
  return <div>Dashboard'a hoş geldin</div>;}

Hook'lar wrapper hell'i ortadan kaldırıyor, data flow'u explicit yapıyor ve doğal olarak compose oluyor. Her hook'un fonksiyonalitesi return değerlerinden açıkça görülüyor.

HOC'ların Hala Anlamlı Olduğu Durumlar

Hook'ların avantajlarına rağmen, HOC'lar şunlar için değerli:

Hook Kullanılamayan Library Kodları: Bazı context'ler component'leri internal'larını değiştirmeden wrap etmeyi gerektirir.

Visual Wrapper'lar: Component'lerin etrafına sadece görsel element ekleyen HOC'lar:

typescript
function withCard<P extends object>(  Component: React.ComponentType<P>): React.FC<P> {  return (props: P) => (    <div className="card">      <div className="card-body">        <Component {...props} />      </div>    </div>  );}

Legacy Class Component Entegrasyonu: Class component'lerden migrate ederken, HOC'lar modern hook'lara köprü oluyor.

TypeScript Decorator Syntax

TypeScript decorator'ları (Stage 3 proposal, TypeScript 5.0+ desteği) declarative metadata ve behavior modification sağlıyor:

typescript
// Method decorator for loggingfunction log(  target: any,  propertyKey: string,  descriptor: PropertyDescriptor) {  const originalMethod = descriptor.value;
  descriptor.value = async function(...args: any[]) {    console.log(`[${propertyKey}] Called with:`, args);    const start = Date.now();
    try {      const result = await originalMethod.apply(this, args);      const duration = Date.now() - start;      console.log(`[${propertyKey}] ${duration}ms'de tamamlandı`);      return result;    } catch (error) {      console.error(`[${propertyKey}] Başarısız:`, error);      throw error;    }  };
  return descriptor;}
class ApiClient {  @log  async fetchUser(id: string): Promise<User> {    const response = await fetch(`/api/users/${id}`);    return response.json();  }
  @log  async updateUser(id: string, data: Partial<User>): Promise<User> {    const response = await fetch(`/api/users/${id}`, {      method: 'PATCH',      body: JSON.stringify(data),    });    return response.json();  }}

TypeScript decorator'ları şunlar için iyi çalışıyor:

  • Logging ve monitoring
  • Validation ve authorization
  • Caching ve memoization
  • Performance tracking

Gerçek Senaryo: Analytics Tracking

Birden fazla component'e analytics eklemeyi düşünelim. Yaklaşımları karşılaştıralım:

HOC Yaklaşımı (verbose):

typescript
const DashboardWithTracking = withTracking(Dashboard, 'dashboard_view');const ProfileWithTracking = withTracking(Profile, 'profile_view');const SettingsWithTracking = withTracking(Settings, 'settings_view');

Hook Yaklaşımı (daha temiz):

typescript
function Dashboard() {  usePageTracking('dashboard_view');  // component logic}
function usePageTracking(pageName: string) {  useEffect(() => {    analytics.track('page_view', { page: pageName });
    return () => {      // Cleanup gerekirse    };  }, [pageName]);}

Hook yaklaşımı component logic'ine doğal entegre oluyor, extra wrapper component'lerden kaçınıyor ve tracking'i component body'de explicit yapıyor.

Adapter Pattern: Uyumsuz Interface'leri Köprüleme

Adapter'lar bir interface'i diğerine çeviriyor. TypeScript'te third-party API'ları izole ediyorlar, daha kolay test ve gelecekte migration sağlıyorlar.

Problem: Third-Party API Uyuşmazlığı

External kütüphaneler genellikle domain modelinle eşleşmeyen interface'lere sahip:

typescript
// Stripe'ın API yapısı (kontrol edemiyoruz)interface StripeCustomer {  id: string;  email: string;  metadata: Record<string, string>;  created: number; // Unix timestamp  description: string | null;}
// Senin domain modelininterface Customer {  customerId: string;  email: string;  organizationId: string;  createdAt: Date;  notes?: string;}

Bu temsiller farklı amaçlara hizmet ediyor. Stripe'ın API'si onların backend'i için optimize, senin domain modelin ise business logic'in için.

Çözüm: Adapter Katmanı

Temsiller arasında çeviri yapan adapter'lar oluştur:

typescript
class StripeCustomerAdapter {  static toDomain(stripeCustomer: StripeCustomer): Customer {    return {      customerId: stripeCustomer.id,      email: stripeCustomer.email,      organizationId: stripeCustomer.metadata.organizationId,      createdAt: new Date(stripeCustomer.created * 1000),      notes: stripeCustomer.description || undefined,    };  }
  static toStripe(customer: Customer): Partial<StripeCustomer> {    return {      email: customer.email,      metadata: {        organizationId: customer.organizationId,      },      description: customer.notes || null,    };  }}
// Service layer'da kullanımclass CustomerService {  constructor(private stripe: Stripe) {}
  async getCustomer(id: string): Promise<Customer> {    const stripeCustomer = await this.stripe.customers.retrieve(id);    return StripeCustomerAdapter.toDomain(stripeCustomer);  }
  async createCustomer(customer: Customer): Promise<Customer> {    const stripeData = StripeCustomerAdapter.toStripe(customer);    const created = await this.stripe.customers.create(stripeData);    return StripeCustomerAdapter.toDomain(created);  }}

Stripe API'sini değiştirdiğinde, sadece adapter'ı güncellemen gerekiyor. Domain modelin stabil kalıyor.

TypeScript Mapped Types ile Adapter

TypeScript'in type system'ı mapped types kullanarak compile-time adapter'ları sağlıyor:

typescript
// API response wrapper'ları için generic adaptertype ApiResponse<T> = {  data: T;  status: number;  message: string;  metadata: {    timestamp: number;    requestId: string;  };};
// API response type'ını unwrap ettype UnwrapApiResponse<T> = T extends ApiResponse<infer U> ? U : T;
// Data type'ını otomatik extract ettype UserData = UnwrapApiResponse<  ApiResponse<{ id: string; name: string }>>;// Sonuç: { id: string; name: string }
// Runtime adapter fonksiyonufunction unwrapApiResponse<T>(response: ApiResponse<T>): T {  if (response.status >= 400) {    throw new Error(`API Error: ${response.message}`);  }  return response.data;}

Bu pattern birçok API benzer wrapper yapılarını paylaştığında iyi çalışıyor.

React Component Props için Adapter

Adapter'lar third-party UI kütüphanelerini design system'ına entegre etmeye yardımcı oluyor:

typescript
// Internal design system button interfaceinterface InternalButtonProps {  label: string;  variant: 'primary' | 'secondary' | 'danger';  onClick: () => void;  disabled?: boolean;}
// Material-UI için adapter componentfunction InternalButton({  label,  variant,  onClick,  disabled,}: InternalButtonProps) {  // Internal variant'ı Material-UI color'a adapt et  const muiColor = {    primary: 'primary',    secondary: 'secondary',    danger: 'error',  }[variant] as 'primary' | 'secondary' | 'error';
  return (    <MuiButton      variant="contained"      color={muiColor}      onClick={onClick}      disabled={disabled}    >      {label}    </MuiButton>  );}
// Internal API ile kullanım<InternalButton  label="Sil"  variant="danger"  onClick={handleDelete}/>

Bu izolasyon, Material-UI'dan başka bir kütüphaneye geçişin sadece adapter component'i güncellemeyi gerektirmesi demek, her kullanım yerini değil.

Ne Zaman Adapter Oluşturmalı

Adapter oluştur:

  • Değiştirebileceğin third-party servisler için (payment processor'lar, cloud provider'lar)
  • Kötü TypeScript desteği olan API'lar için
  • Karmaşık domain model transformasyonları gerektiren external servisler için
  • Sık breaking change'ler yaşayan kütüphaneler için

Adapter oluşturma:

  • Stabil, iyi type'lanmış kütüphaneler için (lodash, date-fns)
  • Kontrolün altındaki internal utility'ler için
  • Basit bire bir mapping'ler için (utility fonksiyonlar kullan)

Facade Pattern: Karmaşık Subsystem'ları Basitleştirme

Facade'lar karmaşık subsystem'lara basitleştirilmiş interface'ler sağlıyor. TypeScript'te initialization karmaşıklığını gizliyorlar, birden fazla servisi koordine ediyorlar ve implementation detaylarına coupling'i azaltıyorlar.

Problem: Karmaşık API Setup

AWS SDK v3 her servis için özel configuration, command objeler ve dikkatli error handling gerektiriyor:

typescript
// Facade olmadan - dağınık karmaşıklıkimport { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';import { DynamoDBClient, PutItemCommand } from '@aws-sdk/client-dynamodb';import { marshall } from '@aws-sdk/util-dynamodb';import { SQSClient, SendMessageCommand } from '@aws-sdk/client-sqs';
// Setup codebase'e dağılmışconst s3 = new S3Client({ region: 'us-east-1' });const dynamodb = new DynamoDBClient({ region: 'us-east-1' });const sqs = new SQSClient({ region: 'us-east-1' });
// Kullanım AWS SDK detaylarını anlamayı gerektiriyorawait s3.send(new PutObjectCommand({  Bucket: 'my-bucket',  Key: 'file.txt',  Body: buffer,}));
await dynamodb.send(new PutItemCommand({  TableName: 'my-table',  Item: marshall({ id: '123', data: 'value' }),}));
await sqs.send(new SendMessageCommand({  QueueUrl: process.env.QUEUE_URL,  MessageBody: JSON.stringify({ task: 'process' }),}));

Her operasyon AWS SDK konvansiyonları, command objeler ve data marshalling bilgisi gerektiriyor.

Çözüm: Birleşik Facade

Domain-specific operasyonlar sağlayan bir facade oluştur:

typescript
class CloudStorage {  private s3: S3Client;  private dynamodb: DynamoDBClient;  private sqs: SQSClient;
  constructor(private region: string) {    this.s3 = new S3Client({ region });    this.dynamodb = new DynamoDBClient({ region });    this.sqs = new SQSClient({ region });  }
  async uploadFile(    bucket: string,    key: string,    data: Buffer  ): Promise<void> {    await this.s3.send(new PutObjectCommand({      Bucket: bucket,      Key: key,      Body: data,    }));  }
  async saveMetadata(    table: string,    item: Record<string, any>  ): Promise<void> {    await this.dynamodb.send(new PutItemCommand({      TableName: table,      Item: marshall(item),    }));  }
  async enqueueTask(    queueUrl: string,    task: Record<string, any>  ): Promise<void> {    await this.sqs.send(new SendMessageCommand({      QueueUrl: queueUrl,      MessageBody: JSON.stringify(task),    }));  }}
// Basit kullanımconst storage = new CloudStorage('us-east-1');await storage.uploadFile('my-bucket', 'file.txt', buffer);await storage.saveMetadata('my-table', { id: '123', data: 'value' });await storage.enqueueTask(queueUrl, { task: 'process' });

Facade, AWS SDK karmaşıklığını domain'e uygun methodların arkasında gizliyor. Tüketiciler uygulamanın vocabulary'si ile çalışıyor, AWS'inki ile değil.

Karmaşık Form İşlemleri için Facade

Formlar genellikle validation, submission, error handling ve analytics içeriyor:

typescript
interface FormFacadeOptions {  formik: FormikHelpers<any>;  analytics: AnalyticsService;  api: ApiClient;}
class RegistrationFormFacade {  constructor(private options: FormFacadeOptions) {}
  async submitRegistration(values: RegistrationFormValues): Promise<User> {    const { formik, analytics, api } = this.options;
    // Denemeyi track et    analytics.track('registration_attempt', {      referrer: values.referrer,    });
    try {      // Validate et      await this.validateEmail(values.email);
      // User oluştur      const user = await api.createUser({        email: values.email,        name: values.name,        password: values.password,      });
      // Verification email gönder      await api.sendVerificationEmail(user.email);
      // Başarıyı track et      analytics.track('registration_success', {        userId: user.id,      });
      return user;    } catch (error) {      // Error'u track et      analytics.track('registration_error', {        error: error.message,      });
      // API error'ları form error'larına map et      const formErrors = this.mapApiErrorsToFormErrors(error);      formik.setErrors(formErrors);
      throw error;    }  }
  private async validateEmail(email: string): Promise<void> {    const available = await this.options.api.checkEmailAvailability(email);    if (!available) {      throw new Error('Email zaten kayıtlı');    }  }
  private mapApiErrorsToFormErrors(    error: ApiError  ): FormikErrors<RegistrationFormValues> {    // Karmaşık error mapping logic    if (error.code === 'EMAIL_TAKEN') {      return { email: 'Bu email zaten kayıtlı' };    }    if (error.code === 'WEAK_PASSWORD') {      return { password: 'Şifre daha güçlü olmalı' };    }    return { _form: 'Kayıt başarısız. Lütfen tekrar dene.' };  }}
// Component'te kullanımfunction RegistrationForm() {  const formik = useFormik({ /* ... */ });  const analytics = useAnalytics();  const api = useApiClient();
  const facade = useMemo(    () => new RegistrationFormFacade({ formik, analytics, api }),    [formik, analytics, api]  );
  const handleSubmit = async (values: RegistrationFormValues) => {    try {      const user = await facade.submitRegistration(values);      navigate(`/welcome/${user.id}`);    } catch (error) {      // Error zaten facade tarafından handle edildi    }  };
  return <form onSubmit={formik.handleSubmit(handleSubmit)}>    {/* Form alanları */}  </form>;}

Facade birden fazla subsystem'ı - API client, analytics, form state - tek bir method call'un arkasında koordine ediyor.

Barrel Export'lar: Tartışmalı Bir Facade

Barrel export'lar (index.ts dosyaları) moduller için public facade'lar oluşturuyor:

typescript
// lib/index.ts - public API facadeexport { User, type UserRole } from './models/user';export { createClient } from './client';export { authenticate, type AuthOptions } from './auth';export { ApiError } from './errors';
// Tüketiciler temiz interface görüyorimport { User, createClient, authenticate } from 'my-lib';

Bu internal yapıyı gizliyor, tüketicileri bozmadan internal'ları refactor etmeni sağlıyor.

Ancak: Atlassian'dan gelen 2024 araştırması, barrel export'ların build performance'ı önemli ölçüde zarar verebileceğini gösteriyor. Barrel dosyalarını kaldırdıktan sonra build sürelerini %75 azalttılar. Barrel'lar aracılığıyla 11k modül import eden Next.js sayfaları 3.5k direct import'a düştü (%68 azalma).

Öneri: Barrel export'ları sadece public library API'leri için kullan. Build performance'ın önemli olduğu internal proje modullerinde kullanma.

Facade'lar Ne Zaman Değer Katıyor

Facade oluştur:

  • Ortak operasyonlar için birden fazla servisi koordine ederken
  • Karmaşık initialization sequence'larını basitleştirirken
  • Teknik API'lara domain-specific interface'ler sağlarken
  • Uygulamaları third-party API değişikliklerinden izole ederken

Facade oluşturma:

  • Basit obje oluşturma için (bu sadece gereksiz indirection)
  • Tek methodları ek logic olmadan wrap ederken
  • Zaten basit olan internal utility'ler için

Composite Pattern: React'in Doğal Yapısı

Composite pattern, bireysel objeleri ve composition'ları uniform olarak ele alıyor. React'in component modeli doğal olarak composite - component'ler başka component'leri içerebiliyor ve her ikisi de aynı şekilde ele alınıyor.

Klasik Composite Pattern

Ders kitabı örneği dosya sistemleri gibi hiyerarşik yapıları içeriyor:

typescript
// Component interfaceinterface FileSystemNode {  name: string;  size: number;  render(): JSX.Element;}
// Leaf - Dosyaclass File implements FileSystemNode {  constructor(    public name: string,    public size: number,    public type: string  ) {}
  render() {    return (      <div className="file">        <FileIcon type={this.type} />        <span>{this.name}</span>        <span>{formatSize(this.size)}</span>      </div>    );  }}
// Composite - Klasörclass Folder implements FileSystemNode {  constructor(    public name: string,    private children: FileSystemNode[]  ) {}
  get size(): number {    return this.children.reduce((sum, child) => sum + child.size, 0);  }
  render() {    return (      <div className="folder">        <FolderIcon />        <span>{this.name}</span>        <div className="children">          {this.children.map((child, i) => (            <div key={i}>{child.render()}</div>          ))}        </div>      </div>    );  }}
// Dosyalar ve klasörler için uniform interfaceconst root = new Folder('root', [  new File('document.txt', 1024, 'text'),  new Folder('images', [    new File('photo1.jpg', 2048, 'image'),    new File('photo2.jpg', 3072, 'image'),  ]),  new File('README.md', 512, 'markdown'),]);

Bu çalışıyor, ama React için verbose. Pattern sağlam - bireysel item'lar ve collection'ları uniform şekilde ele alma - ama implementation React'in doğal composition'ını kullanmıyor.

Modern React Yaklaşımı

React component'leri doğal olarak composite. Aynı logic'i idiomatic React ile:

typescript
interface FileSystemNodeData {  name: string;  type: 'file' | 'folder';  size?: number;  mimeType?: string;  children?: FileSystemNodeData[];}
function FileSystemNode({ node }: { node: FileSystemNodeData }) {  if (node.type === 'file') {    return (      <div className="file">        <FileIcon type={node.mimeType!} />        <span>{node.name}</span>        <span>{formatSize(node.size!)}</span>      </div>    );  }
  const totalSize = node.children?.reduce(    (sum, child) => sum + (child.size || 0),    0  ) || 0;
  return (    <div className="folder">      <FolderIcon />      <span>{node.name}</span>      <span>{formatSize(totalSize)}</span>      <div className="children">        {node.children?.map((child, i) => (          <FileSystemNode key={i} node={child} />        ))}      </div>    </div>  );}
// Data yapısı ile kullanımconst fileSystem: FileSystemNodeData = {  name: 'root',  type: 'folder',  children: [    { name: 'document.txt', type: 'file', size: 1024, mimeType: 'text' },    {      name: 'images',      type: 'folder',      children: [        { name: 'photo1.jpg', type: 'file', size: 2048, mimeType: 'image' },        { name: 'photo2.jpg', type: 'file', size: 3072, mimeType: 'image' },      ],    },    { name: 'README.md', type: 'file', size: 512, mimeType: 'markdown' },  ],};
<FileSystemNode node={fileSystem} />

Bu, React'in doğal recursion'ını ve type safety için discriminated union'ları kullanıyor. Composite doğası explicit class'lar yerine component yapısından geliyor.

Compound Components Pattern

Compound component pattern implicit state sharing ile esnek API'lar sağlıyor:

typescript
interface SelectContextValue {  value: string | null;  onChange: (value: string) => void;  isOpen: boolean;  setIsOpen: (open: boolean) => void;}
const SelectContext = createContext<SelectContextValue | null>(null);
function Select({ children, value, onChange }: SelectProps) {  const [isOpen, setIsOpen] = useState(false);
  return (    <SelectContext.Provider value={{ value, onChange, isOpen, setIsOpen }}>      <div className="select">{children}</div>    </SelectContext.Provider>  );}
function SelectTrigger({ children }: { children: ReactNode }) {  const context = useContext(SelectContext);  if (!context) throw new Error('SelectTrigger Select içinde kullanılmalı');
  return (    <button      onClick={() => context.setIsOpen(!context.isOpen)}      className="select-trigger"    >      {context.value || children}    </button>  );}
function SelectContent({ children }: { children: ReactNode }) {  const context = useContext(SelectContext);  if (!context) throw new Error('SelectContent Select içinde kullanılmalı');
  if (!context.isOpen) return null;
  return <div className="select-content">{children}</div>;}
function SelectOption({ value, children }: OptionProps) {  const context = useContext(SelectContext);  if (!context) throw new Error('SelectOption Select içinde kullanılmalı');
  return (    <div      className={context.value === value ? 'selected' : ''}      onClick={() => {        context.onChange(value);        context.setIsOpen(false);      }}    >      {children}    </div>  );}
// Ergonomik kullanım için namespaceSelect.Trigger = SelectTrigger;Select.Content = SelectContent;Select.Option = SelectOption;
// Esnek composition<Select value={selected} onChange={setSelected}>  <Select.Trigger>Seçenek seç</Select.Trigger>  <Select.Content>    <Select.Option value="1">Seçenek 1</Select.Option>    <Select.Option value="2">Seçenek 2</Select.Option>    <Select.Option value="3">Seçenek 3</Select.Option>  </Select.Content></Select>

Bu pattern Radix UI, Headless UI ve Reach UI'da görünüyor. Esnek, composable API'lar için composite yapıyı context-based state sharing ile birleştiriyor.

Proxy Pattern: Lazy Loading ve Access Control

Proxy'ler objelere erişimi kontrol ediyor, lazy loading, caching, validation veya access control gibi davranışlar ekliyor. TypeScript hem class-based proxy'ler hem de runtime interception için JavaScript'in Proxy API'sini sağlıyor.

React.lazy ve Suspense

React'in built-in lazy loading'i bir proxy pattern - talep üzerine kod yüklemek için component render'ını intercept ediyor:

typescript
// Code-splitting için proxyconst Dashboard = React.lazy(() => import('./Dashboard'));const Settings = React.lazy(() => import('./Settings'));const Profile = React.lazy(() => import('./Profile'));
function App() {  return (    <Suspense fallback={<div>Yükleniyor...</div>}>      <Routes>        <Route path="/dashboard" element={<Dashboard />} />        <Route path="/settings" element={<Settings />} />        <Route path="/profile" element={<Profile />} />      </Routes>    </Suspense>  );}

React.lazy component render'ını intercept ediyor, modülü sadece gerektiğinde yüklüyor. Suspense boundary proxy kodu fetch ederken loading state sağlıyor.

API Caching için Custom Proxy

Proxy'ler calling code'u değiştirmeden caching katmanları ekliyor:

typescript
interface ApiClient {  fetchUser(id: string): Promise<User>;  fetchPosts(userId: string): Promise<Post[]>;}
class CachedApiClient implements ApiClient {  private cache = new Map<string, {    data: any;    timestamp: number;  }>();  private ttl = 60000; // 1 dakika
  constructor(private realClient: ApiClient) {}
  async fetchUser(id: string): Promise<User> {    const cacheKey = `user:${id}`;    const cached = this.cache.get(cacheKey);
    if (cached && Date.now() - cached.timestamp < this.ttl) {      console.log('[Cache] Hit:', cacheKey);      return cached.data;    }
    console.log('[Cache] Miss:', cacheKey);    const data = await this.realClient.fetchUser(id);    this.cache.set(cacheKey, {      data,      timestamp: Date.now(),    });
    return data;  }
  async fetchPosts(userId: string): Promise<Post[]> {    const cacheKey = `posts:${userId}`;    const cached = this.cache.get(cacheKey);
    if (cached && Date.now() - cached.timestamp < this.ttl) {      console.log('[Cache] Hit:', cacheKey);      return cached.data;    }
    console.log('[Cache] Miss:', cacheKey);    const data = await this.realClient.fetchPosts(userId);    this.cache.set(cacheKey, {      data,      timestamp: Date.now(),    });
    return data;  }}
// Transparent proxy - real client ile aynı interfaceconst realClient = new RealApiClient();const cachedClient = new CachedApiClient(realClient);
// Kullanım değişmediconst user = await cachedClient.fetchUser('123');const posts = await cachedClient.fetchPosts('123');

Proxy, real client ile aynı interface'i implement ediyor, caching'i transparent şekilde ekliyor. Calling code cached mi fresh data mı kullandığını bilmiyor.

Validation için JavaScript Proxy API

JavaScript'in Proxy API'si runtime interception sağlıyor:

typescript
import { z } from 'zod';
const userSchema = z.object({  name: z.string().min(2),  email: z.string().email(),  age: z.number().positive().int(),});
function createValidatedProxy<T extends object>(  target: T,  schema: z.ZodSchema<T>): T {  return new Proxy(target, {    set(obj, prop, value) {      // Değişiklikten sonra tüm objeyi validate et      const updated = { ...obj, [prop]: value };      const result = schema.safeParse(updated);
      if (!result.success) {        throw new Error(          `Validation ${String(prop)} için başarısız: ${result.error.message}`        );      }
      obj[prop as keyof T] = value;      return true;    },
    get(obj, prop) {      const value = obj[prop as keyof T];      console.log(`[Access] ${String(prop)}:`, value);      return value;    },  });}
// Kullanımconst user = createValidatedProxy(  { name: '', email: '', age: 0 },  userSchema);
user.name = 'Ahmet'; // OKuser.email = '[email protected]'; // OKuser.age = 30; // OK
// user.age = -5; // Validation error fırlatır// user.email = 'gecersiz'; // Validation error fırlatır

Proxy, property access ve assignment'ı intercept ediyor, runtime'da validation kurallarını enforce ediyor. Configuration objeleri veya invariant gerektiren domain entity'ler için iyi çalışıyor.

React Query bir Proxy Pattern

React Query, data fetching için proxy görevi görüyor, caching, loading state'leri ve refetching'i yönetiyor:

typescript
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
function UserProfile({ userId }: { userId: string }) {  // React Query fetch operasyonunu proxy'liyor  const { data, isLoading, error } = useQuery({    queryKey: ['user', userId],    queryFn: () => fetchUser(userId),    staleTime: 60000, // 1 dakika cache    gcTime: 300000, // 5 dakika sonra garbage collect  });
  const queryClient = useQueryClient();
  const updateMutation = useMutation({    mutationFn: (updates: Partial<User>) => updateUser(userId, updates),    onSuccess: () => {      // Başarılı mutation'dan sonra cache'i invalidate et      queryClient.invalidateQueries({ queryKey: ['user', userId] });    },  });
  if (isLoading) return <div>Yükleniyor...</div>;  if (error) return <div>Hata: {error.message}</div>;
  return (    <div>      <h2>{data.name}</h2>      <button onClick={() => updateMutation.mutate({ name: 'Yeni İsim' })}>        İsmi Güncelle      </button>    </div>  );}

React Query data fetching'i intercept ediyor, şunları ekliyor:

  • Configurable TTL ile otomatik caching
  • Loading ve error state'leri
  • Background refetching
  • Cache invalidation
  • Request deduplication

Component, React Query'nin useQuery'sini basit bir data fetch gibi ele alıyor, ama proxy karmaşık caching ve synchronization logic'i handle ediyor.

Access Control için Proxy

Proxy'ler authorization enforce edebilir:

typescript
interface AdminActions {  deleteUser(id: string): Promise<void>;  modifyPermissions(userId: string, permissions: string[]): Promise<void>;  accessAuditLogs(): Promise<AuditLog[]>;}
class AuthorizedAdminProxy implements AdminActions {  constructor(    private realAdmin: AdminActions,    private currentUser: User  ) {}
  private checkAuthorization(action: string): void {    if (!this.currentUser.roles.includes('admin')) {      throw new Error(`Yetkisiz: ${action} admin rolü gerektiriyor`);    }  }
  async deleteUser(id: string): Promise<void> {    this.checkAuthorization('deleteUser');    console.log(`[Audit] User ${this.currentUser.id}, ${id}'yi sildi`);    await this.realAdmin.deleteUser(id);  }
  async modifyPermissions(    userId: string,    permissions: string[]  ): Promise<void> {    this.checkAuthorization('modifyPermissions');    console.log(      `[Audit] User ${this.currentUser.id}, ${userId} için izinleri değiştirdi`    );    await this.realAdmin.modifyPermissions(userId, permissions);  }
  async accessAuditLogs(): Promise<AuditLog[]> {    this.checkAuthorization('accessAuditLogs');    console.log(`[Audit] User ${this.currentUser.id} audit log'lara erişti`);    return await this.realAdmin.accessAuditLogs();  }}
// Kullanımconst adminActions = new RealAdminActions();const authorizedProxy = new AuthorizedAdminProxy(adminActions, currentUser);
// Proxy her action'dan önce authorization kontrol ediyorawait authorizedProxy.deleteUser('user-123');

Proxy, real implementation'ı değiştirmeden authorization ve logging enforce ediyor.

Yaygın Tuzaklar ve Dersler

Tuzak 1: HOC Wrapper Hell

Problem: Birden fazla HOC stack'lemek onlarca katman derin component tree'leri oluşturuyor, debugging'i zorlaştırıyor ve performance monitoring'i challenge haline getiriyor.

Çözüm: Çoğu cross-cutting concern için hook'lara migrate et. HOC'ları legacy entegrasyon veya gerçekten visual wrapper'lar için sakla.

Tuzak 2: Leaky Facade'lar

Problem: Implementation detaylarını expose eden facade'lar amacını yitiriyor.

Kötü Örnek:

typescript
class CloudStorage {  // Leaky - S3-specific detayları expose ediyor  async upload(command: PutObjectCommand): Promise<void> {    await this.s3.send(command);  }}

İyi Örnek:

typescript
class CloudStorage {  // Proper abstraction - tüketiciler S3'ü görmüyor  async upload(bucket: string, key: string, data: Buffer): Promise<void> {    const command = new PutObjectCommand({ Bucket: bucket, Key: key, Body: data });    await this.s3.send(command);  }}

Ders: Facade'lar domain dilinle konuşmalı, underlying kütüphanenin diliyle değil.

Tuzak 3: Adapter Proliferation

Problem: Her external dependency için adapter oluşturmak net faydalar olmadan maintenance yükü yaratıyor.

Ne zaman adapter oluşturmalı:

  • Swap etmeyi planladığın dependency'ler için (farklı cloud provider'lar, payment processor'lar)
  • Type safety katmanları gerektiren kötü TypeScript desteği olan API'lar için
  • Önemli domain model transformasyonu gerektiren external servisler için

Ne zaman adapter oluşturMAmalı:

  • Stabil, iyi type'lanmış kütüphaneler için (lodash, date-fns)
  • Kontrolün altındaki internal utility'ler için
  • Basit bire bir mapping'ler için (utility fonksiyonlar kullan)

Tuzak 4: Composite Overengineering

Problem: Basit component hiyerarşileri için full composite pattern implement etmek.

Overkill:

typescript
interface Component {  render(): JSX.Element;  getSize(): number;  add(child: Component): void;  remove(child: Component): void;}

Daha İyi:

typescript
// React'in doğal olarak composition'ı handle etmesine izin verfunction List({ items }: { items: Item[] }) {  return (    <ul>      {items.map(item => <ListItem key={item.id} item={item} />)}    </ul>  );}

Ders: React'in component modeli zaten composite davranış sağlıyor. Explicit pattern implementation nadiren gerekli.

Tuzak 5: Barrel Export Performance

Problem: Internal moduller için barrel export'lar (index.ts dosyaları) kullanmak build performance'ı zarar veriyor.

Araştırma: Atlassian barrel dosyalarını kaldırdıktan sonra build sürelerini %75 azalttı. Next.js uygulamaları modül import'larının 11k'dan 3.5k'ya düştüğünü gördü (%68 azalma).

Öneri: Barrel export'ları sadece public library API'leri için kullan. Internal moduller için direkt import et:

typescript
// ❌ Barrel'larla yavaşimport { Button, Input, Select } from '@/components';
// ✅ Direct import'larla hızlıimport { Button } from '@/components/button';import { Input } from '@/components/input';import { Select } from '@/components/select';

Önemli Çıkarımlar

HOC'lar → Hook'lar: Higher-order componentler (decorator pattern) büyük ölçüde daha temiz composition için hook'larla değiştirildi. HOC'lar legacy kod ve purely visual wrapper'lar için faydalı kalıyor.

Boundary'ler için Adapter: Sistem boundary'lerinde - external API'lar, third-party kütüphaneler - değişiklikleri izole etmek ve temiz domain modelleri sürdürmek için adapter'lar kullan. Stabil, iyi type'lanmış dependency'leri adapt etme.

Karmaşıklık için Facade: Facade'lar birden fazla subsystem'ı koordine etmede veya karmaşık initialization'ı basitleştirmede mükemmel. Basit obje oluşturma için facade oluşturmaktan kaçın - bu gereksiz indirection.

Composite React'in Doğası: React'in component modeli doğal olarak composite. Explicit composite pattern implementation nadiren gerekli - React'in doğal composition'ından yararlan.

Cross-Cutting Concern'ler için Proxy: Caching, lazy loading, access control ve validation için proxy'leri kullan. JavaScript'in Proxy API'si ve React Query gibi patternler, orijinal implementation'ları değiştirmeden güçlü interception sağlıyor.

Barrel Export'lar Tartışmalı: Sadece public library API'leri için dikkatli kullan. Internal barrel export'lar build performance'ı önemli ölçüde zarar veriyor.

TypeScript Pattern'leri Geliştiriyor: Mapped types, conditional types ve discriminated union'lar yapısal patternleri klasik implementasyonlardan daha güçlü ve type-safe yapıyor.

Decoration Yerine Composition: Modern React, daha iyi okunabilirlik ve maintainability için decoration patternleri (HOC'lar) yerine composition patternlerini (hook'lar, compound componentler, render props) tercih ediyor.

1994'teki yapısal patternler kaybolmadı. Framework konvansiyonlarına, type system özelliklerine ve modern mimari yaklaşımlara evrildi. Temel prensipleri anlamak - davranışı wrap etme, interface'leri adapt etme, karmaşıklığı basitleştirme, parça ve bütünü uniform ele alma, erişimi kontrol etme - bu patternleri modern codebase'lerde tanımana ve yeni sistemler inşa ederken uygun şekilde uygulamana yardımcı oluyor.

Klasik Tasarım Kalıplarına Modern Bakış

Klasik Gang of Four tasarım kalıplarının modern TypeScript, React ve fonksiyonel programlama bağlamında nasıl evrildiğini inceleyen kapsamlı bir seri. Klasik kalıpların hala ne zaman geçerli olduğunu, ne zaman yerini yeni yaklaşımlara bıraktığını ve temel prensiplerin modern kod tabanlarında nasıl ortaya çıktığını öğren.

İlerleme2/4 yazı tamamlandı

İlgili Yazılar