Gang of Four'un Ötesinde Modern React ve TypeScript Tasarım Kalıpları
JavaScript ve TypeScript ekosistemlerinden doğan modern kalıplar - hooks, compound components, render props ve GoF'un görmediği problemleri çözen repository pattern'leri.
Gang of Four, 1994’te C++ ve Smalltalk için pattern’leri dokümante etti; asenkron programlama, component composition ve reactive data flow uygulama yazma biçimini henüz değiştirmemişti. JavaScript ve TypeScript kendi cevaplarını üretti: stateful logic paylaşımı için React Hooks, esnek API’ler için Compound Components, data access için Repository pattern ve kapsülleme için ES module’ler. Front-end’de ya da Node tarafında TypeScript yazıyorsan işin ağırlığını tek bir varsayılan taşıyor: önce dilin veya framework’ün hazır özelliğine bak, pattern’i ancak adını koyabildiğin bir problemi ortadan kaldırıyorsa ekle.
Bu varsayılan kısıtlardan çıkıyor. First-class function’lar, closure’lar, ES module’ler, async/await ve JSX klasik dolaylamanın çoğunu gereksiz kılıyor. React ve TypeScript codebase’lerinde ayakta kalan pattern’ler, 1994 katalogunun hiç karşılaşmadığı problemleri çözenler:
- Stateful logic’i HOC wrapper cehennemi olmadan paylaşmak
- Props patlaması olmadan esnek component API’leri kurmak
- Test edilebilirlik için data access’i soyutlamak
- Module seviyesindeki state’i kapsüllemek
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:
// HOC wrapper cehennemi
export 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:
// Data fetching için custom hook
function 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ı temiz
function 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:
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 et
function 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ım
function 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:
- Hook’ları sadece üst seviyede çağır: Conditional’larda, loop’larda veya nested function’larda hook yok
- Hook’ları sadece React function’lardan çağır: Functional component’ler veya diğer hook’lar
- Dependency’ler eksiksiz olmalı:
useEffectveuseCallbackdependency’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:
// 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ı pozisyon
const [age, setAge] = useState(0); // hooks[1] - aynı pozisyon
const [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:
// Props patlaması - extend etmesi zor
interface 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:
// Context paylaşılan state'i tutuyor
interface 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önetiyor
interface 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şiyor
function 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'le
Select.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.
Ekosistemden Örnekler#
Bu pattern React ekosisteminin her yerinde görülüyor:
Radix UI compound component’leri yaygın kullanıyor:
<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:
<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:
// UserService.ts
async function getUser(id: string) {
return prisma.user.findUnique({ where: { id } });
}
// OrderService.ts
async function getOrders(userId: string) {
return prisma.order.findMany({ where: { userId } });
}
// ProductService.ts
async function getProducts() {
return prisma.product.findMany();
}
Bu yaklaşımın problemleri var:
- ORM değiştirmek codebase’in her yerinde değişiklik gerektiriyor
- Test etmek Prisma’yı doğrudan mock’lamayı gerektiriyor
- Business logic ve data access arasında net ayrım yok
- Caching veya logging’i uniform şekilde implement etmek zor
Repository Pattern İmplementasyonu#
Repository pattern, data access’i interface’lerin arkasında merkezileştiriyor:
// 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 implementasyonu
class 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 implementasyon
class 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 kullan
const userRepository = new PrismaUserRepository(prisma);
const userService = new UserService(userRepository);
// Test - in-memory kullan
const testRepository = new InMemoryUserRepository();
const testService = new UserService(testRepository);
Faydalar ve Trade-off’lar#
Faydalar:
- Test edilebilirlik: ORM internal’larını mock’lamadan implementasyonları değiştir
- Esneklik: Interface’i implement ederek Prisma’dan TypeORM’e geç
- Merkezileşmiş logic: Query pattern’leri, caching, logging tek yerde
- Net sınırlar: Business logic database detaylarını bilmiyor
Trade-off’lar:
- Daha fazla kod: Her entity repository interface ve implementasyon gerektiriyor
- Öğrenme eğrisi: Takım repository pattern’i anlamalı
- Abstraction maliyeti: ORM-spesifik özelliklere kolay erişemiyorsun
- 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:
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:
// Type ile context oluştur
interface ThemeContextValue {
theme: 'light' | 'dark';
toggleTheme: () => void;
}
const ThemeContext = createContext<ThemeContextValue | null>(null);
// Context consume etmek için custom hook
function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme ThemeProvider içinde kullanılmalı');
}
return context;
}
// Provider component
function 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 yok
function 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:
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:
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ım
function App() {
return (
<AppProviders>
<Router />
</AppProviders>
);
}
Performans Maliyeti#
Context değer değiştiğinde tüm consumer’ları re-render ediyor. Memoization ile optimize et:
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:
// Hızlı ve yavaş değişen değerleri ayır
const 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 olmuyor
function 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:
// Eski IIFE pattern
const 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:
// logger.ts - varsayılan olarak private
let 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 dosyalar
import { log, setLogLevel } from './logger';
log('info', 'Uygulama başladı');
// logLevel veya shouldLog'a erişemezsiniz
IIFE’ye göre faydaları:
- Static analysis: TypeScript ve bundler’lar import’ları anlıyor
- Tree shaking: Kullanılmayan export’lar build’de eleniyor
- Daha iyi tooling: Auto-import, go-to-definition düzgün çalışıyor
- Type safety: TypeScript module boundary’lerini zorunlu kılıyor
- Boilerplate yok: Wrapping function gerekmiyor
Module-Scoped Singleton’lar#
ES module’ler doğal olarak singleton pattern implement ediyor:
// database.ts
class 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 et
export const db = new DatabaseConnection();
// Diğer dosyalar her zaman aynı instance'ı alıyor
import { 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:
// config.ts
interface 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 et
config = loadConfig();
export function getConfig(): Config {
if (!config) {
throw new Error('Config initialize edilmedi');
}
return config;
}
export function reloadConfig(): void {
config = loadConfig();
}
// İlk import initialization'ı tetikliyor
import { 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:
// Presenter - pure presentation
interface 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 logic
interface 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ı:
- Test edilebilirlik: UserCard mock prop’larla test etmesi kolay
- Yeniden kullanılabilirlik: UserCard herhangi bir user object’iyle çalışıyor
- 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:
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:
// Test için hook'ları mock'la
jest.mock('./hooks/useUser', () => ({
useUser: () => ({
user: mockUser,
loading: false,
error: null
})
}));
// UserCard'ı doğrudan test et
render(<UserCard userId="123" />);
expect(screen.getByText(mockUser.name)).toBeInTheDocument();
Storybook, hook’ları story seviyesinde mock’layarak çalışıyor:
// UserCard.stories.tsx
export 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: Uygun Kullanım Senaryoları#
Katı ayrım şu durumlarda hala değerli:
- App’ler arasında paylaşılan presentation component’leri: Design system component’leri
- Server-side rendering: Platform başına farklı data fetching pattern’leri
- Karmaşık prop interface’leri: Data ve display arasında net contract
- 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ın Sınırları#
Hook’lar çoğu senaryo için çalışıyor ama consumer’ların rendering kontrolüne ihtiyacı olduğunda başarısız oluyor:
// Hook yaklaşımı - consumer loading UI'ı kontrol edemiyor
function 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:
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:
// Render prop router state'e erişim veriyor
<Route path="/users/:id">
{({ match }) => (
match ? (
<UserProfile userId={match.params.id} />
) : (
<UserList />
)
)}
</Route>
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.
Değişimin Nedenleri#
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ıtlar farklı pattern’ler üretiyor; problemler yerinde kaldı, implementasyonlar değişti.
Pattern Seçim Framework’ü#
Bir pattern’e uzanmadan önce:
- Problemi adlandır: Tek cümleye sığmıyorsa, pattern süstür.
- Dile bak: Discriminated union’lar çoğu zaman factory ihtiyacını kaldırıyor, ES module’ler de creational pattern’lerin çoğunu karşılıyor.
- Framework’e bak: Stateful logic paylaşımını hook’lar, yapısal işin çoğunu composition karşılıyor.
- Dolaylamanın karşılığını tart: Yerini hak etmesi için test edilebilirlik ya da net bir sınır kazandırmalı.
- Ekibe bak: Ekipte kimsenin akıcı okuyamadığı pattern zamanla bakım maliyetine dönüşüyor.
Sırada Ne Var#
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.
Varsayılan Nerede Geçerli#
Önce dilin veya framework’ün özelliğine bakmak, uygulama kodunda işe yarıyor; orada state’i, composition’ı ve data flow’u zaten framework taşıyor. İki durum bunu tersine çeviriyor. Library ve design system kodunun açık bir contract’a ihtiyacı var; hook daha kısa olsa bile compound component ve render props oradaki dolaylamayı hak ediyor. Birden fazla data source’la yaşayan uzun ömürlü domain kodunda repository sınırı, ikinci kaynak gelmeden önce kurulmalı.
Pattern’ler tasarımı tartışmak için bir kelime dağarcığı, onu implement etmek için bir reçete değil. Buradan sonraki adım, sahip olduğun tek bir modülü gözden geçirmek olabilir: içindeki her soyutlama için o soyutlamanın kaldırdığı problemi adlandır. Adını koyamadığın her şey silinmeye aday.
Kaynaklar#
- The Catalog of Design Patterns - Refactoring.Guru (yeni sekmede açılır) - GoF ve ek pattern’lerin niyete göre gruplandırıldığı eksiksiz katalog
- Catalog of Patterns of Enterprise Application Architecture - Martin Fowler (yeni sekmede açılır) - PoEAA kitabındaki 40’tan fazla kurumsal pattern’in çevrimiçi katalogu
- Patterns of Enterprise Application Architecture - Martin Fowler (yeni sekmede açılır) - Kurumsal uygulama pattern’lerine (Repository, Unit of Work, Service Layer) dair referans kitap
- Software Architecture Guide - Martin Fowler (yeni sekmede açılır) - Fowler’ın mimari pattern’ler ve değiş tokuşlar üzerine derlediği makaleler
- Design Patterns in TypeScript - Refactoring.Guru (yeni sekmede açılır) - Modern alternatiflerle karşılaştırma için klasik GoF pattern’lerin TypeScript uygulamaları
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.
Bu serideki tüm yazılar
İlgili yazılar
SOLID prensiplerinin modern JavaScript'te uygulanışı: TypeScript, React hooks ve fonksiyonel pattern'lerle pratik örnekler, ayrıca ne zaman gereksiz.
typescript · javascript · react +4
Decorator, Adapter, Facade, Composite ve Proxy patternlerinin React ve TypeScript'te evrimi: HOC'lar ne zaman hook'lara yol verir, adapterler API'ları nasıl izole eder.
typescript · react · design-patterns +2
Mimari ağırlığını runtime'ın init-amortismanına göre seç: single-purpose Lambda'da yalın handler, Lambdalith'te orta, tam OOP/DI yalnızca uzun ömürlü runtime'da.
architecture · lambda · serverless +3
Effect'i adım adım öğrenmek ve AWS Lambda ile entegre etmek için pratik bir rehber: gerçek kod örnekleri, yaygın hatalar ve üretim desenleri.
typescript · functional-programming · lambda +4
Kural tabanlı chatbot'lardan otonom AI agent'larına mimari evrim: ReAct, Plan-and-Execute ve çoklu-agent desenleri TypeScript örnekleriyle anlatılıyor.
ai-agents · llm · architecture +2