Skip to content
~/sph.sh

Gang of Four'un Ötesindeki Tasarım Kalıpları

JavaScript ve TypeScript ekosistemlerinden doğan modern kalıpları keşfedelim - hooks, compound components, render props ve GoF'un hiç karşılaşmadığı problemleri çözen repository pattern'leri.

Özet

Gang of Four, 1994'te C++ ve Smalltalk için pattern'leri dokümante etti. Asenkron programlamayı, component composition'ı, functional programming'i ya da reactive data flow'u öngöremezlerdi. JavaScript ve TypeScript ekosistemleri, 1994'te var olmayan problemleri çözmek için kendi pattern'lerini geliştirdi. Bu yazıda, gerçek web development ihtiyaçlarından doğan modern pattern'leri inceliyoruz: Stateful logic paylaşımı için React Hooks, esnek API'ler için Compound Components, data access abstraction için Repository Pattern ve doğal design pattern olarak ES Modules. Bunlar klasik pattern'lerin uyarlamaları değil - yeni problemler için yeni çözümler.

Pattern Düşüncesinin Evrimi

Yıllar içinde React codebase'leriyle çalışmak bana önemli bir şey öğretti: Gang of Four pattern'leri, sınırlı type system'lere sahip OOP dillerindeki spesifik problemleri çözüyor. JavaScript ve TypeScript'in farklı kısıtları ve yetenekleri var. First-class function'lar, closure'lar, ES module'ler, async/await ve JSX, C++ ya da Java'da imkansız veya pratik olmayan pattern'leri mümkün kılıyor.

Bu yazıdaki pattern'ler yeniden markalanmış GoF pattern'leri değil. React ve TypeScript topluluklarının gerçek problemleri çözerken organik olarak ortaya çıktılar:

  • Stateful logic'i HOC wrapper cehennemini yaşamadan nasıl paylaşırız?
  • Props patlaması olmadan esnek component API'leri nasıl oluştururuz?
  • Test edilebilirlik için data access'i nasıl soyutlarız?
  • Module-level state'i nasıl kapsülleriz?

Modern codebase'lerin gerçekte kullandığı pattern'leri keşfedelim.

Hooks Pattern: Wrapper'sız Stateful Logic

Hooks'un Çözdüğü Problem

Hook'lardan önce, React'te stateful logic paylaşmak Higher-Order Component'ler veya Render Props gerektiriyordu. Her iki yaklaşım da problemler yaratıyordu:

typescript
// HOC wrapper cehennemiexport default withAuth(  withTheme(    withAnalytics(      withErrorBoundary(        Component      )    )  ));
// Render props callback cehennemi<DataFetcher url="/api/user">  {(user, userLoading) => (    <PermissionsChecker user={user}>      {(permissions, permLoading) => (        <ThemeProvider>          {(theme) => (            <Component              user={user}              permissions={permissions}              theme={theme}              loading={userLoading || permLoading}            />          )}        </ThemeProvider>      )}    </PermissionsChecker>  )}</DataFetcher>

Bunu debug etmek acı verici. React DevTools, gerçek component'ine ulaşmadan önce altı seviye nesting gösteriyor. Birden fazla HOC benzer isimlerde prop'lar eklediğinde props collision yaygınlaşıyor.

Custom Hooks: Yeni Bir Pattern

Hook'lar, React'in state ve lifecycle'ını paylaşan function'lara yeniden kullanılabilir logic çıkarmamızı sağlıyor:

typescript
// Data fetching için custom hookfunction useUser(userId: string) {  const [user, setUser] = useState<User | null>(null);  const [loading, setLoading] = useState(true);  const [error, setError] = useState<Error | null>(null);
  useEffect(() => {    let cancelled = false;
    async function fetchData() {      try {        const data = await fetch(`/api/users/${userId}`).then(r => r.json());        if (!cancelled) setUser(data);      } catch (err) {        if (!cancelled) setError(err as Error);      } finally {        if (!cancelled) setLoading(false);      }    }
    fetchData();
    return () => {      cancelled = true;    };  }, [userId]);
  // Not: AbortController kullanan modern alternatif  // const controller = new AbortController();  // fetch(url, { signal: controller.signal })  // return () => controller.abort();
  return { user, loading, error };}
// Kullanımı temizfunction Profile({ userId }: { userId: string }) {  const { user, loading, error } = useUser(userId);  const { theme } = useTheme();  const { permissions } = usePermissions(user?.id);
  if (loading) return <Spinner />;  if (error) return <ErrorMessage error={error} />;
  return (    <div className={theme}>      <UserCard user={user!} permissions={permissions} />    </div>  );}

Fark önemli. Wrapper nesting yok, net data flow var ve mükemmel TypeScript inference. Her hook'un return type'ı otomatik olarak akıyor.

Karmaşık Logic'i Compose Etmek

Hook'lar doğal olarak compose oluyorlar. Form state yönetimi ile çalışırken karşılaşılabilecek gerçekçi bir örnek:

typescript
function useForm<T extends Record<string, any>>(initialValues: T) {  const [values, setValues] = useState<T>(initialValues);  const [errors, setErrors] = useState<Partial<Record<keyof T, string>>>({});  const [touched, setTouched] = useState<Partial<Record<keyof T, boolean>>>({});  const [isSubmitting, setIsSubmitting] = useState(false);
  const handleChange = useCallback((field: keyof T, value: any) => {    setValues(prev => ({ ...prev, [field]: value }));    // User yazmaya başladığında error'u temizle    if (errors[field]) {      setErrors(prev => {        const next = { ...prev };        delete next[field];        return next;      });    }  }, [errors]);
  const handleBlur = useCallback((field: keyof T) => {    setTouched(prev => ({ ...prev, [field]: true }));  }, []);
  const setError = useCallback((field: keyof T, error: string) => {    setErrors(prev => ({ ...prev, [field]: error }));  }, []);
  const reset = useCallback(() => {    setValues(initialValues);    setErrors({});    setTouched({});    setIsSubmitting(false);  }, [initialValues]);
  return {    values,    errors,    touched,    isSubmitting,    setIsSubmitting,    handleChange,    handleBlur,    setError,    reset  };}
// Validation hook ile compose etfunction useValidatedForm<T extends Record<string, any>>(  initialValues: T,  validationSchema: z.ZodSchema<T>) {  const form = useForm(initialValues);
  const validate = useCallback(async () => {    try {      await validationSchema.parseAsync(form.values);      return true;    } catch (error) {      if (error instanceof z.ZodError) {        error.errors.forEach(err => {          const field = err.path[0] as keyof T;          form.setError(field, err.message);        });      }      return false;    }  }, [form, validationSchema]);
  return { ...form, validate };}
// Kullanımfunction RegistrationForm() {  const form = useValidatedForm(    { email: '', password: '' },    registrationSchema  );
  const handleSubmit = async (e: FormEvent) => {    e.preventDefault();
    if (!await form.validate()) return;
    form.setIsSubmitting(true);    try {      await registerUser(form.values);    } finally {      form.setIsSubmitting(false);    }  };
  return (    <form onSubmit={handleSubmit}>      <input        value={form.values.email}        onChange={(e) => form.handleChange('email', e.target.value)}        onBlur={() => form.handleBlur('email')}      />      {form.touched.email && form.errors.email && (        <span className="error">{form.errors.email}</span>      )}      {/* ... */}    </form>  );}

Bu pattern çalışıyor çünkü hook'lar function composition yoluyla compose oluyor. Her hook, diğer hook'ların tüketebileceği değerler döndürüyor. Inheritance hierarchy yok, wrapper component yok.

Kurallar ve Kısıtlamalar

Hook'ların ESLint tarafından zorlanan spesifik kuralları var:

  1. Hook'ları sadece üst seviyede çağır: Conditional'larda, loop'larda veya nested function'larda hook yok
  2. Hook'ları sadece React function'lardan çağır: Functional component'ler veya diğer hook'lar
  3. Dependency'ler eksiksiz olmalı: useEffect ve useCallback dependency'leri referans alınan tüm değerleri içermeli

Bu kısıtlamalar, React'in re-render'lar boyunca hook state'ini korumasını sağlıyor. İmplementasyon call order'a dayanıyor:

typescript
// React internal olarak hook değerlerinin bir array'ini tutuyor// İlk render:const [name, setName] = useState('');     // hooks[0]const [age, setAge] = useState(0);        // hooks[1]const [email, setEmail] = useState('');   // hooks[2]
// İkinci render - aynı order gerekli:const [name, setName] = useState('');     // hooks[0] - aynı pozisyonconst [age, setAge] = useState(0);        // hooks[1] - aynı pozisyonconst [email, setEmail] = useState('');   // hooks[2] - aynı pozisyon
// Kuralları bozmak state corruption'a neden olur:if (showAge) {  const [age, setAge] = useState(0);  // Conditional hook - order değişiyor!}

Kurallar başta kısıtlayıcı hissettiriyor ama güçlü composition pattern'lerini mümkün kılıyor.

Compound Components: Context ile Esnek API'ler

Problem: Props Patlaması

Yeniden kullanılabilir component'ler oluşturmak genellikle props patlamasına yol açıyor. Bir Select component düşün:

typescript
// Props patlaması - extend etmesi zorinterface SelectProps {  options: Array<{ value: string; label: string; disabled?: boolean }>;  value: string | null;  onChange: (value: string) => void;  placeholder?: string;  disabled?: boolean;  searchable?: boolean;  clearable?: boolean;  renderOption?: (option: Option) => ReactNode;  renderValue?: (value: string) => ReactNode;  filterOptions?: (options: Option[], search: string) => Option[];  onOpen?: () => void;  onClose?: () => void;  // ... 20 prop daha}
<Select  options={options}  value={selected}  onChange={setSelected}  searchable  clearable  renderOption={(opt) => <CustomOption {...opt} />}/>

Her yeni özellik prop ekliyor. Component API'si yönetilemez hale geliyor.

Compound Components Pattern

Compound component'ler, prop'ları implicit state paylaşan birden fazla component'e dağıtıyor:

typescript
// Context paylaşılan state'i tutuyorinterface SelectContextValue {  value: string | null;  onChange: (value: string) => void;  isOpen: boolean;  setIsOpen: (open: boolean) => void;}
const SelectContext = createContext<SelectContextValue | null>(null);
function useSelectContext() {  const context = useContext(SelectContext);  if (!context) {    throw new Error('Select.* component\'leri <Select> içinde kullanılmalı');  }  return context;}
// Root component state'i yönetiyorinterface SelectProps {  value: string | null;  onChange: (value: string) => void;  children: ReactNode;}
function Select({ value, onChange, children }: SelectProps) {  const [isOpen, setIsOpen] = useState(false);
  return (    <SelectContext.Provider value={{ value, onChange, isOpen, setIsOpen }}>      <div className="select-container">        {children}      </div>    </SelectContext.Provider>  );}
// Alt component'ler context'e erişiyorfunction SelectTrigger({ children }: { children: ReactNode }) {  const { isOpen, setIsOpen, value } = useSelectContext();
  return (    <button      className="select-trigger"      onClick={() => setIsOpen(!isOpen)}    >      {children || value || 'Seç...'}    </button>  );}
function SelectOptions({ children }: { children: ReactNode }) {  const { isOpen } = useSelectContext();
  if (!isOpen) return null;
  return (    <div className="select-options">      {children}    </div>  );}
interface SelectOptionProps {  value: string;  children: ReactNode;}
function SelectOption({ value, children }: SelectOptionProps) {  const { value: selectedValue, onChange, setIsOpen } = useSelectContext();
  return (    <div      className={selectedValue === value ? 'selected' : ''}      onClick={() => {        onChange(value);        setIsOpen(false);      }}    >      {children}    </div>  );}
// Alt component'leri namespace'leSelect.Trigger = SelectTrigger;Select.Options = SelectOptions;Select.Option = SelectOption;
// Esnek kullanım<Select value={selected} onChange={setSelected}>  <Select.Trigger>    <span>Bir framework seç</span>  </Select.Trigger>  <Select.Options>    <Select.Option value="react">React</Select.Option>    <Select.Option value="vue">Vue</Select.Option>    <Select.Option value="svelte">Svelte</Select.Option>  </Select.Options></Select>
// Ya da rendering'i customize et<Select value={selected} onChange={setSelected}>  <Select.Trigger>    {selected ? (      <div className="custom-display">        <Icon name={selected} />        <span>{selected}</span>      </div>    ) : (      <span>Birini seç</span>    )}  </Select.Trigger>  <Select.Options>    {frameworks.map(fw => (      <Select.Option key={fw.id} value={fw.id}>        <div className="custom-option">          <Icon name={fw.icon} />          <div>            <div className="name">{fw.name}</div>            <div className="description">{fw.description}</div>          </div>        </div>      </Select.Option>    ))}  </Select.Options></Select>

Compound pattern, props patlaması olmadan esneklik sağlıyor. Consumer'lar rendering'i kontrol ederken library state ve behavior'u yönetiyor.

Gerçek Dünya Örnekleri

Bu pattern React ekosisteminin her yerinde görülüyor:

Radix UI compound component'leri yaygın kullanıyor:

typescript
<Tabs.Root value={tab} onValueChange={setTab}>  <Tabs.List>    <Tabs.Trigger value="account">Hesap</Tabs.Trigger>    <Tabs.Trigger value="password">Şifre</Tabs.Trigger>  </Tabs.List>  <Tabs.Content value="account">    <AccountSettings />  </Tabs.Content>  <Tabs.Content value="password">    <PasswordSettings />  </Tabs.Content></Tabs.Root>

Headless UI erişilebilir component'ler için aynı pattern'i takip ediyor:

typescript
<Disclosure>  <Disclosure.Button>    İade politikanız nedir?  </Disclosure.Button>  <Disclosure.Panel>    İadeler 30 gün içinde yapılabilir.  </Disclosure.Panel></Disclosure>

Pattern, esnekliğin sadelikten daha önemli olduğu component library'leri için iyi çalışıyor.

Repository Pattern: Data Access'i Soyutlama

Problem: Dağınık Data Logic

Abstraction olmadan, data access uygulamanın her yerine dağılıyor:

typescript
// UserService.tsasync function getUser(id: string) {  return prisma.user.findUnique({ where: { id } });}
// OrderService.tsasync function getOrders(userId: string) {  return prisma.order.findMany({ where: { userId } });}
// ProductService.tsasync function getProducts() {  return prisma.product.findMany();}

Bu yaklaşımın problemleri var:

  1. ORM değiştirmek codebase'in her yerinde değişiklik gerektiriyor
  2. Test etmek Prisma'yı doğrudan mock'lamayı gerektiriyor
  3. Business logic ve data access arasında net ayrım yok
  4. Caching veya logging'i uniform şekilde implement etmek zor

Repository Pattern İmplementasyonu

Repository pattern, data access'i interface'lerin arkasında merkezileştiriyor:

typescript
// Interface tanımla (abstraction)interface UserRepository {  findById(id: string): Promise<User | null>;  findByEmail(email: string): Promise<User | null>;  findAll(options?: FindOptions): Promise<User[]>;  save(user: User): Promise<User>;  delete(id: string): Promise<void>;}
// Prisma implementasyonuclass PrismaUserRepository implements UserRepository {  constructor(private prisma: PrismaClient) {}
  async findById(id: string): Promise<User | null> {    return this.prisma.user.findUnique({      where: { id },      include: { profile: true }    });  }
  async findByEmail(email: string): Promise<User | null> {    return this.prisma.user.findUnique({      where: { email }    });  }
  async findAll(options?: FindOptions): Promise<User[]> {    return this.prisma.user.findMany({      skip: options?.offset,      take: options?.limit,      orderBy: { createdAt: 'desc' }    });  }
  async save(user: User): Promise<User> {    return this.prisma.user.upsert({      where: { id: user.id },      create: user,      update: user    });  }
  async delete(id: string): Promise<void> {    await this.prisma.user.delete({ where: { id } });  }}
// Test için in-memory implementasyonclass InMemoryUserRepository implements UserRepository {  private users = new Map<string, User>();
  async findById(id: string): Promise<User | null> {    return this.users.get(id) || null;  }
  async findByEmail(email: string): Promise<User | null> {    return Array.from(this.users.values())      .find(u => u.email === email) || null;  }
  async findAll(options?: FindOptions): Promise<User[]> {    let users = Array.from(this.users.values());
    if (options?.offset) {      users = users.slice(options.offset);    }
    if (options?.limit) {      users = users.slice(0, options.limit);    }
    return users;  }
  async save(user: User): Promise<User> {    this.users.set(user.id, user);    return user;  }
  async delete(id: string): Promise<void> {    this.users.delete(id);  }}
// Business logic implementasyona değil interface'e bağımlıclass UserService {  constructor(private userRepository: UserRepository) {}
  async registerUser(email: string, password: string): Promise<User> {    const existing = await this.userRepository.findByEmail(email);    if (existing) {      throw new Error('Email zaten kayıtlı');    }
    const user: User = {      id: uuid(),      email,      password: await hashPassword(password),      createdAt: new Date()    };
    return this.userRepository.save(user);  }
  async getUser(id: string): Promise<User> {    const user = await this.userRepository.findById(id);    if (!user) {      throw new Error('User bulunamadı');    }    return user;  }}
// Production - Prisma kullanconst userRepository = new PrismaUserRepository(prisma);const userService = new UserService(userRepository);
// Test - in-memory kullanconst testRepository = new InMemoryUserRepository();const testService = new UserService(testRepository);

Faydalar ve Trade-off'lar

Faydalar:

  1. Test edilebilirlik: ORM internal'larını mock'lamadan implementasyonları değiştir
  2. Esneklik: Interface'i implement ederek Prisma'dan TypeORM'e geç
  3. Merkezileşmiş logic: Query pattern'leri, caching, logging tek yerde
  4. Net sınırlar: Business logic database detaylarını bilmiyor

Trade-off'lar:

  1. Daha fazla kod: Her entity repository interface ve implementasyon gerektiriyor
  2. Öğrenme eğrisi: Takım repository pattern'i anlamalı
  3. Abstraction maliyeti: ORM-spesifik özelliklere kolay erişemiyorsun
  4. Over-engineering riski: Basit CRUD app'lere buna gerek olmayabilir

Data-ağırlıklı uygulamalarda ekiplerle çalışmak bana repository pattern'in şu durumlarda işe yaradığını öğretti:

  • Birden fazla data source (Postgres + Redis + S3)
  • Service'lerde olmaması gereken karmaşık querying logic
  • Yüksek test coverage gereksinimleri
  • Gelecekte potansiyel ORM migration

Basit data access'li basit uygulamalar için, doğrudan ORM kullanımı genellikle daha temiz.

Provider Pattern: Context-Tabanlı Dependency Injection

Klasik Problem: Prop Drilling

Prop'ları birden fazla component katmanından geçirmek yorucu:

typescript
function App() {  const theme = useTheme();  const user = useUser();  const config = useConfig();
  return (    <Dashboard theme={theme} user={user} config={config} />  );}
function Dashboard({ theme, user, config }: DashboardProps) {  return (    <Layout theme={theme}>      <Sidebar user={user} config={config} />    </Layout>  );}
function Sidebar({ user, config }: SidebarProps) {  return (    <Navigation user={user} config={config} />  );}
function Navigation({ user, config }: NavigationProps) {  // Sonunda prop'ları kullanıyoruz  return <div>{user.name} - {config.appName}</div>;}

Üç ara component theme, user veya config kullanmıyor - sadece aşağı aktarıyorlar. Bu coupling yaratıyor ve refactoring'i acı verici hale getiriyor.

Context ile Provider Pattern

React Context, component tree'de derinlere prop drilling olmadan değer sağlıyor:

typescript
// Type ile context oluşturinterface ThemeContextValue {  theme: 'light' | 'dark';  toggleTheme: () => void;}
const ThemeContext = createContext<ThemeContextValue | null>(null);
// Context consume etmek için custom hookfunction useTheme() {  const context = useContext(ThemeContext);  if (!context) {    throw new Error('useTheme ThemeProvider içinde kullanılmalı');  }  return context;}
// Provider componentfunction ThemeProvider({ children }: { children: ReactNode }) {  const [theme, setTheme] = useState<'light' | 'dark'>('light');
  const toggleTheme = useCallback(() => {    setTheme(prev => prev === 'light' ? 'dark' : 'light');  }, []);
  const value = useMemo(    () => ({ theme, toggleTheme }),    [theme, toggleTheme]  );
  return (    <ThemeContext.Provider value={value}>      {children}    </ThemeContext.Provider>  );}
// Kullanım - prop drilling yokfunction App() {  return (    <ThemeProvider>      <Dashboard />    </ThemeProvider>  );}
function Dashboard() {  return (    <Layout>      <Sidebar />    </Layout>  );}
function Sidebar() {  return <Navigation />;}
function Navigation() {  const { theme, toggleTheme } = useTheme();
  return (    <div className={theme}>      <button onClick={toggleTheme}>        Tema değiştir      </button>    </div>  );}

Herhangi bir derinlikteki component'ler, ara component'lerin bundan haberi olmadan theme'e erişebiliyor.

Birden Fazla Provider'ı Birleştirme

Gerçek uygulamaların birden fazla context'i var:

typescript
function App() {  return (    <ThemeProvider>      <AuthProvider>        <ConfigProvider>          <I18nProvider>            <Router />          </I18nProvider>        </ConfigProvider>      </AuthProvider>    </ThemeProvider>  );}

Bu nesting çalışıyor ama verbose. Yaygın bir pattern provider'ları birleştiriyor:

typescript
interface AppProvidersProps {  children: ReactNode;}
const providers = [  ThemeProvider,  AuthProvider,  ConfigProvider,  I18nProvider];
function AppProviders({ children }: AppProvidersProps) {  return providers.reduceRight(    (acc, Provider) => <Provider>{acc}</Provider>,    children  );}
// Daha temiz kullanımfunction App() {  return (    <AppProviders>      <Router />    </AppProviders>  );}

Performance Değerlendirmeleri

Context değer değiştiğinde tüm consumer'ları re-render ediyor. Memoization ile optimize et:

typescript
function ExpensiveProvider({ children }: { children: ReactNode }) {  const [state, setState] = useState(initialState);
  // Memoization olmadan - her render'da yeni object  // State değişmese bile tüm consumer'lar re-render oluyor  const badValue = { state, setState };
  // Memoization ile - sadece dependency'ler değiştiğinde değişiyor  const goodValue = useMemo(    () => ({ state, setState }),    [state]  // Sadece state değiştiğinde yeniden oluştur  );
  return (    <ExpensiveContext.Provider value={goodValue}>      {children}    </ExpensiveContext.Provider>  );}

Sık değişen değerler için context'leri ayır:

typescript
// Hızlı ve yavaş değişen değerleri ayırconst UserContext = createContext<User>(null!);const UserActionsContext = createContext<UserActions>(null!);
function UserProvider({ children }: { children: ReactNode }) {  const [user, setUser] = useState<User>(null!);
  // Action'lar nadiren değişiyor  const actions = useMemo(    () => ({      updateProfile: (data: ProfileData) => {        setUser(prev => ({ ...prev, ...data }));      },      logout: () => setUser(null!)    }),    []  );
  return (    <UserContext.Provider value={user}>      <UserActionsContext.Provider value={actions}>        {children}      </UserActionsContext.Provider>    </UserContext.Provider>  );}
// Sadece action'ları consume eden component'ler user değiştiğinde re-render olmuyorfunction LogoutButton() {  const { logout } = useContext(UserActionsContext);  // User profile güncellendiğinde re-render olmuyor  return <button onClick={logout}>Çıkış</button>;}

Module Pattern: Design Pattern Olarak ES Module'ler

Pre-ES6: IIFE ve Revealing Module

ES module'lerden önce, JavaScript kapsülleme için IIFE kullanıyordu:

typescript
// Eski IIFE patternconst logger = (function() {  // Private variable'lar  let logLevel = 'info';  const levels = ['debug', 'info', 'warn', 'error'];
  // Private function  function shouldLog(level: string): boolean {    return levels.indexOf(level) >= levels.indexOf(logLevel);  }
  // Public API  return {    setLogLevel(level: string) {      logLevel = level;    },    log(level: string, message: string) {      if (shouldLog(level)) {        console.log(`[${level}] ${message}`);      }    }  };})();
logger.log('info', 'Uygulama başladı');// logLevel veya shouldLog'a doğrudan erişemezsiniz

Bu çalışıyordu ama boilerplate gerektiriyordu ve static analysis yoktu.

Modern: ES Module'ler

ES module'ler doğal kapsülleme sağlıyor:

typescript
// logger.ts - varsayılan olarak privatelet logLevel: 'debug' | 'info' | 'warn' | 'error' = 'info';const levels = ['debug', 'info', 'warn', 'error'] as const;
// Private function (export edilmemiş)function shouldLog(level: typeof logLevel): boolean {  return levels.indexOf(level) >= levels.indexOf(logLevel);}
// Public API (export edilmiş)export function setLogLevel(level: typeof logLevel) {  logLevel = level;}
export function log(level: typeof logLevel, message: string) {  if (shouldLog(level)) {    console.log(`[${level}] ${message}`);  }}
// Diğer dosyalarimport { log, setLogLevel } from './logger';log('info', 'Uygulama başladı');// logLevel veya shouldLog'a erişemezsiniz

IIFE'ye göre faydaları:

  1. Static analysis: TypeScript ve bundler'lar import'ları anlıyor
  2. Tree shaking: Kullanılmayan export'lar build'de eleniyor
  3. Daha iyi tooling: Auto-import, go-to-definition düzgün çalışıyor
  4. Type safety: TypeScript module boundary'lerini zorunlu kılıyor
  5. Boilerplate yok: Wrapping function gerekmiyor

Module-Scoped Singleton'lar

ES module'ler doğal olarak singleton pattern implement ediyor:

typescript
// database.tsclass DatabaseConnection {  private connected = false;
  connect() {    if (!this.connected) {      console.log('Veritabanına bağlanıyor...');      this.connected = true;    }  }
  query(sql: string) {    if (!this.connected) {      throw new Error('Bağlı değil');    }    // Query çalıştır  }}
// Tek instance export etexport const db = new DatabaseConnection();
// Diğer dosyalar her zaman aynı instance'ı alıyorimport { db } from './database';db.connect();db.query('SELECT * FROM users');

Module caching, db'nin her yerde aynı instance olmasını garanti ediyor. Bu, private constructor ve static getInstance method'lu klasik singleton pattern'den daha temiz.

Module Initialization

Module'ler ilk import edildiğinde bir kez çalışıyor. Bunu initialization için kullan:

typescript
// config.tsinterface Config {  apiUrl: string;  apiKey: string;  timeout: number;}
let config: Config | null = null;
function loadConfig(): Config {  // Environment veya dosyadan yükle  return {    apiUrl: process.env.API_URL || 'http://localhost:3000',    apiKey: process.env.API_KEY || '',    timeout: 30000  };}
// Module yüklendiğinde initialize etconfig = loadConfig();
export function getConfig(): Config {  if (!config) {    throw new Error('Config initialize edilmedi');  }  return config;}
export function reloadConfig(): void {  config = loadConfig();}
// İlk import initialization'ı tetikliyorimport { getConfig } from './config';const config = getConfig();  // Zaten yüklendi

Container/Presenter: Ayrımı Yeniden Düşünmek

Klasik Pattern

Container/Presenter (Smart/Dumb veya Stateful/Stateless olarak da bilinir), data fetching'i presentation'dan ayırıyor:

typescript
// Presenter - pure presentationinterface UserCardProps {  user: User;  onFollow: () => void;  onMessage: () => void;}
function UserCard({ user, onFollow, onMessage }: UserCardProps) {  return (    <div className="user-card">      <img src={user.avatar} alt={user.name} />      <h3>{user.name}</h3>      <p>{user.bio}</p>      <div className="actions">        <button onClick={onFollow}>Takip et</button>        <button onClick={onMessage}>Mesaj</button>      </div>    </div>  );}
// Container - data ve logicinterface UserCardContainerProps {  userId: string;}
function UserCardContainer({ userId }: UserCardContainerProps) {  const { user, loading, error } = useUser(userId);  const { followUser } = useFollowUser();  const { startConversation } = useMessaging();
  if (loading) return <Spinner />;  if (error) return <ErrorMessage error={error} />;
  return (    <UserCard      user={user!}      onFollow={() => followUser(userId)}      onMessage={() => startConversation(userId)}    />  );}

Ayrımın faydaları:

  1. Test edilebilirlik: UserCard mock prop'larla test etmesi kolay
  2. Yeniden kullanılabilirlik: UserCard herhangi bir user object'iyle çalışıyor
  3. Storybook: Data fetching olmadan tüm state'lerde UserCard göster

Modern Alternatif: Co-location

Hook'lar, fedakarlık yapmadan data ve presentation'ı birlikte tutmayı mümkün kılıyor:

typescript
function UserCard({ userId }: { userId: string }) {  const { user, loading, error } = useUser(userId);  const { followUser } = useFollowUser();  const { startConversation } = useMessaging();
  if (loading) return <Spinner />;  if (error) return <ErrorMessage error={error} />;
  return (    <div className="user-card">      <img src={user.avatar} alt={user.name} />      <h3>{user.name}</h3>      <p>{user.bio}</p>      <div className="actions">        <button onClick={() => followUser(userId)}>Takip et</button>        <button onClick={() => startConversation(userId)}>Mesaj</button>      </div>    </div>  );}

Hook'ları mock'layarak test etmek yine basit:

typescript
// Test için hook'ları mock'lajest.mock('./hooks/useUser', () => ({  useUser: () => ({    user: mockUser,    loading: false,    error: null  })}));
// UserCard'ı doğrudan test etrender(<UserCard userId="123" />);expect(screen.getByText(mockUser.name)).toBeInTheDocument();

Storybook, hook'ları story seviyesinde mock'layarak çalışıyor:

typescript
// UserCard.stories.tsxexport const Default: Story = {  decorators: [    (Story) => {      // Storybook için hook'ları mock'la      jest.spyOn(require('./hooks/useUser'), 'useUser')        .mockReturnValue({          user: mockUser,          loading: false,          error: null        });
      return <Story />;    }  ]};

Container/Presenter Hala Ne Zaman Mantıklı

Katı ayrım şu durumlarda hala değerli:

  1. App'ler arasında paylaşılan presentation component'leri: Design system component'leri
  2. Server-side rendering: Platform başına farklı data fetching pattern'leri
  3. Karmaşık prop interface'leri: Data ve display arasında net contract
  4. Ekip sınırları: Farklı ekipler data'ya ve UI'a sahip

Çoğu uygulama kodu için, hook'larla co-location test edilebilirlikten ödün vermeden daha iyi developer experience sağlıyor.

Render Props: Gelişmiş Kontrol Pattern

Hook'lar Yeterli Olmadığında

Hook'lar çoğu senaryo için çalışıyor ama consumer'ların rendering kontrolüne ihtiyacı olduğunda başarısız oluyor:

typescript
// Hook yaklaşımı - consumer loading UI'ı kontrol edemiyorfunction useData<T>(url: string) {  const [data, setData] = useState<T | null>(null);  const [loading, setLoading] = useState(true);
  useEffect(() => {    fetch(url)      .then(r => r.json())      .then(setData)      .finally(() => setLoading(false));  }, [url]);
  if (loading) return <DefaultSpinner />;  // Sabit loading UI  return data;  // Consumer customize edemez}

Render Props Pattern

Rendering kontrolünü consumer'lara ver:

typescript
interface DataFetcherProps<T> {  url: string;  children: (state: {    data: T | null;    loading: boolean;    error: Error | null;    refetch: () => void;  }) => ReactNode;}
function DataFetcher<T>({ url, children }: DataFetcherProps<T>) {  const [data, setData] = useState<T | null>(null);  const [loading, setLoading] = useState(true);  const [error, setError] = useState<Error | null>(null);
  const fetchData = useCallback(() => {    setLoading(true);    setError(null);
    fetch(url)      .then(r => r.json())      .then(setData)      .catch(setError)      .finally(() => setLoading(false));  }, [url]);
  useEffect(() => {    fetchData();  }, [fetchData]);
  return <>{children({ data, loading, error, refetch: fetchData })}</>;}
// Esnek kullanım - consumer her şeyi kontrol ediyor<DataFetcher<User> url="/api/user">  {({ data, loading, error, refetch }) => (    <div>      {loading && <CustomSpinner message="Kullanıcı yükleniyor..." />}      {error && (        <div className="error">          <p>{error.message}</p>          <button onClick={refetch}>Tekrar dene</button>        </div>      )}      {data && (        <div>          <h1>{data.name}</h1>          <button onClick={refetch}>Yenile</button>        </div>      )}    </div>  )}</DataFetcher>
// Farklı consumer, farklı rendering<DataFetcher<Product[]> url="/api/products">  {({ data, loading }) => (    loading ? <SkeletonGrid /> : <ProductGrid products={data!} />  )}</DataFetcher>

Render Props vs Hook'lar

Hook'ları kullan:

  • Consumer'lar sadece data'ya ihtiyaç duyduğunda
  • Rendering kullanımlar arasında tutarlı olduğunda
  • Logic composition, rendering kontrolünden daha önemli olduğunda

Render props kullan:

  • Consumer'ların tam rendering kontrolüne ihtiyacı olduğunda
  • Loading/error state'leri önemli ölçüde değiştiğinde
  • Component karmaşık UI state yönetiyorsa (modal'lar, dropdown'lar)

React Router'dan gerçek örnek:

typescript
// Render prop router state'e erişim veriyor<Route path="/users/:id">  {({ match }) => (    match ? (      <UserProfile userId={match.params.id} />    ) : (      <UserList />    )  )}</Route>

Modern React Router v6, çoğu kullanım için hook'lara (useParams, useNavigate) geçti, ama render props gelişmiş senaryolar için kaldı.

Seri Özeti: Geçmişten Günümüze Pattern'ler

Pattern'leri 1994'ten 2025'e keşfeden dört yazıyı tamamladık:

Post 1: Creational Pattern'ler ES module'lerin, object spread'in ve TypeScript'in type system'inin çoğu creational pattern'i nasıl değiştirdiğini gösterdi. Singleton module export'u oldu, prototype spread operator'ü oldu, factory discriminated union oldu. Builder pattern karmaşık konfigürasyonlar için hala değerli.

Post 2: Structural Pattern'ler React'in composition model'inin structural pattern'leri nasıl içine aldığını inceledi. Decorator hook'lar ve HOC'lar oldu, facade karmaşık API'leri basitleştirdi, composite doğal olarak component tree'lere map'lendi, adapter harici bağımlılıkları izole etti.

Post 3: Behavioral Pattern'ler observer'ın reactive programming'e evrimini araştırdı. RxJS, Redux ve hook'lar daha iyi error handling ve cancellation ile modern observer implementasyonlarını temsil ediyor. Strategy function'lar oldu, command undo/redo'yu güçlendiriyor, state machine'ler invalid state'leri önlüyor.

Post 4: Modern Pattern'ler JavaScript/TypeScript ekosistemlerinden doğan pattern'leri katalogladı. Hook'lar wrapper cehennemini çözdü, compound component'ler esnek API'ler sağladı, repository data access'i soyutladı, module'ler doğal singleton'lar oldu.

Ne Değişti ve Neden

Gang of Four 1994'te C++ ve Smalltalk ile çalıştı. Bu dillerde vardı:

  • Statik class hierarchy'leri (inheritance birincil mekanizma)
  • Sınırlı type inference
  • First-class function yok
  • Module system yok
  • Asenkron primitive'ler yok
  • Component composition yok

Modern JavaScript ve TypeScript'te var:

  • First-class function'lar ve closure'lar
  • Sofistike type inference
  • Tree shaking'li ES module'ler
  • Async/await ve Promise'ler
  • Component composition (React/Vue/Svelte)
  • Reactive programming primitive'leri

Farklı kısıtlamalar farklı pattern'lere yol açıyor. Pattern'lerin çözdüğü problemler hala geçerli - implementasyonlar evrildi.

Pattern Seçim Framework'ü

Bir tasarım kararıyla karşılaştığında:

  1. Problemi tanımla: Hangi spesifik problem çözülmeli?
  2. Dil özelliklerini kontrol et: Dil bunu zaten çözüyor mu?
  3. Framework'leri değerlendir: React/Next.js/vb bir çözüm sağlıyor mu?
  4. Trade-off'ları değerlendir: Pattern değer mi katıyor yoksa sadece karmaşıklık mı?
  5. Test etmeyi düşün: Bu kod'u test etmeyi kolaylaştırıyor mu zorlaştırıyor mu?
  6. Ekip context'ini değerlendir: Ekip bunu anlayıp maintain edebilir mi?

Modern Pattern Checklist

Herhangi bir pattern implement etmeden önce sor:

  • TypeScript'in type system'i bunu çözüyor mu? (Discriminated union'lar genellikle factory pattern'i ortadan kaldırıyor)
  • Hook'lar bunu çözüyor mu? (Stateful logic paylaşımı için genellikle evet)
  • Composition bunu çözüyor mu? (React component'leri doğal olarak compose oluyor)
  • Module'ler bunu çözüyor mu? (ES module'ler birçok creational pattern'i değiştiriyor)
  • Bu test edilebilirliği iyileştiriyor mu? (Eğer değilse, yeniden düşün)
  • Bu takım arkadaşlarına net olacak mı? (Akıllı pattern'ler maintainability'ye zarar verebilir)

İleriye Bakış

Diller ve framework'ler değiştikçe pattern'ler evrilmeye devam edecek:

React Server Component'leri server/client composition için yeni pattern'ler getiriyor. Server ve client kodu arasındaki sınır yeni abstraction zorlukları yaratıyor. React 19 (Aralık 2024) ile RSC stabil ve production-ready hale geldi, server-first pattern'leri mainstream development'a getirdi.

Signal'ler ve fine-grained reactivity (SolidJS, Vue 3, Preact Signals) React'in component re-rendering model'inden farklı state yönetim pattern'lerini temsil ediyor. Bu yaklaşım, virtual DOM diffing'e alternatif olarak birden fazla framework'te benimseniyor.

TypeScript'te type-level programming daha önce runtime pattern gerektiren kısıtlamaları compile time'da encode etmeyi mümkün kılıyor.

Edge computing ve distributed system'ler network boundary'leri, caching ve eventual consistency ile başa çıkmak için pattern'ler getiriyor.

Bir sonraki nesil pattern'ler, henüz karşılaşmaya başladığımız problemleri çözecek. Klasik pattern'leri ve modern evrimlerini anlamak, yeni ortaya çıkan pattern'leri tanımaya ve trade-off'larını değerlendirmeye hazırlıyor.

Tüm Pattern'lerde Ortak Prensipler

Dönem veya dilden bağımsız olarak, iyi pattern'ler özellikleri paylaşıyor:

  1. Gerçek problemleri çöz: Pattern için pattern yapma
  2. Maintainability'yi iyileştir: Kod anlaşılması ve değiştirilmesi daha kolay olmalı
  3. Test etmeyi sağla: İyi pattern'ler kod'u daha test edilebilir yapıyor, daha az değil
  4. Coupling'i azalt: Component'ler implementasyonlara değil abstraction'lara bağımlı olmalı
  5. Net niyet: Pattern kullanımı tasarım kararlarını iletmeli
  6. Context'e uygun: Library'ler için işe yarayan uygulamalardan farklı
  7. Ekiple uyumlu: Pattern'ler ekibin becerileri ve codebase convention'larıyla eşleşmeli

Son Düşünceler

Pattern'lerle çalışmak bana bunların araç olduğunu öğretti, kural değil. Gang of Four pattern'leri spesifik context'lerde spesifik problemleri çözdü. Modern pattern'ler farklı context'lerde farklı problemleri çözüyor. İkisini de anlamak, her birinin ne zaman uygulanacağını tanımaya yardımcı oluyor.

Kitaplardan veya blog yazılarından pattern'leri cargo-cult yapma. Her pattern'in çözdüğü problemi anla, bu problemin codebase'inde olup olmadığını değerlendir ve en uygun çözümü seç - ister klasik pattern, ister modern pattern, isterse hiç pattern olmasın.

En iyi kod genellikle pattern'leri görünmez kullanıyor. Okuyucular açık pattern implementasyonu görmeden çözümü tanıyor. Hedef bu: Pattern'leri yardımcı olduklarında kullanarak ve olmadıklarında atlayarak problemleri net ve sürdürülebilir şekilde çöz.

Pattern'ler tasarımı tartışmak için bir kelime dağarcığı, onu implement etmek için bir reçete değil. Ekibini etkilemek için değil, onlarla iletişim kurmak için kullan.

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.

İlerleme4/4 yazı tamamlandı

İlgili Yazılar