React ve TypeScript'te Yapısal Design Pattern'ler
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.
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 beşini de framework konvansiyonlarına, hook’lara ve type-safe wrapper’lara dönüştürdü.
Bir React kod tabanında asıl soru, wrapper’ın nereye ait olduğu. Kendi yazdığın kodda varsayılan composition: cross-cutting davranış için hook’lar, hiyerarşi için düz component’ler. Wrap etmek ise kontrol etmediğin sınırlarda yerini buluyor; third-party API’lar, SDK kurulumu ve erişim kuralları gibi.
TypeScript’te Üç Farklı Decorator Anlamı#
“Decorator” terimi TypeScript ekosisteminde üç farklı şey ifade ediyor:
- Gang of Four Decorator Pattern: Objelere dinamik olarak davranış ekleme
- React Higher-Order Components (HOCs): Componentlere ek fonksiyonalite kazandırma
- TypeScript Decorator Syntax: Class/method decorator’lar için Stage 3 proposal
Her biri farklı bir problemi çözüyor ve birbirinin yerine geçmiyor.
React HOC’larla Decorator#
Higher-order componentler, componentleri ek fonksiyonalite ile wrap ederek güçlendirir:
// Authentication HOC
function 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ım
const Dashboard = ({ data }: DashboardProps) => {
return <div>Dashboard'a hoş geldin</div>;
};
export default withAuth(Dashboard);
Wrapper’ın kendi maliyetleri var:
Wrapper Hell: Birden fazla HOC stack’lemek çok derin component tree’leri oluşturuyor; bu da hem debugging’i hem performance monitoring’i zorlaştırıyor:
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’da fonksiyon component’leri ref’i normal bir prop olarak alabiliyor, bu da işi basitleştiriyor. forwardRef hâlâ çalışıyor ama kaldırılma yolunda.
Belirsiz Data Flow: HOC’lar tarafından inject edilen prop’lar component signature’ında görünmüyor.
Custom Hook’lar Çoğu HOC Kullanımının Yerine Geçiyor#
Hook’lar aynı fonksiyonaliteyi daha temiz composition ile sağlıyor: wrapper hell ortadan kalkıyor, data flow explicit kalıyor ve her hook’un ne yaptığı return değerinden anlaşılıyor.
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>;
}
Aynı denge, birden fazla component’e analytics eklerken de karşımıza çıkıyor. HOC yaklaşımı hızla verbose hale geliyor:
const DashboardWithTracking = withTracking(Dashboard, 'dashboard_view');
const ProfileWithTracking = withTracking(Profile, 'profile_view');
const SettingsWithTracking = withTracking(Settings, 'settings_view');
Bir hook ise dokunduğu component’e yakın kalıyor:
function Dashboard() {
usePageTracking('dashboard_view');
// component logic
}
function usePageTracking(pageName: string) {
useEffect(() => {
analytics.track('page_view', { page: pageName });
return () => {
// Cleanup gerekirse
};
}, [pageName]);
}
HOC’ların Hâlâ İşe Yaradığı Yerler#
Hook’lar her durumu kapsamıyor. Library kodunda bazen component’i internal’larına dokunmadan wrap etmek gerekiyor, class component’lerden çıkış yolunda da HOC’lar hook’lara köprü oluyor. Geriye kalan en net durum ise sadece markup ekleyen bir wrapper:
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>
);
}
TypeScript Decorator Syntax#
TypeScript decorator’ları (Stage 3 proposal, TypeScript 5.0+ desteği) declarative metadata ve behavior modification sağlıyor:
// Method decorator for logging
function 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();
}
}
Uygun oldukları yer, methodlar boyunca tekrar eden cross-cutting işler: logging ve monitoring, validation ve authorization, caching ve memoization, performance tracking.
Sağlayıcı Sınırında Adapter’lar#
Adapter’lar bir interface’i diğerine çeviriyor. TypeScript’te bu izolasyon testi kolaylaştırıyor, gelecekteki migration’ları da basitleştiriyor.
StripeCustomer’dan Customer’a#
External kütüphaneler genellikle domain modelinle eşleşmeyen interface’lere sahip:
// 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 modelin
interface Customer {
customerId: string;
email: string;
organizationId: string;
createdAt: Date;
notes?: string;
}
Stripe’ın API’si onların backend’i için optimize, senin domain modelin ise business logic’in için. Adapter ikisi arasında çeviri yapıyor:
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ım
class 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);
}
}
Mapped Type’larla Compile-Time Adapter#
TypeScript’in type system’ı mapped types kullanarak compile-time adapter’ları sağlıyor:
// API response wrapper'ları için generic adapter
type ApiResponse<T> = {
data: T;
status: number;
message: string;
metadata: {
timestamp: number;
requestId: string;
};
};
// API response type'ını unwrap et
type UnwrapApiResponse<T> = T extends ApiResponse<infer U> ? U : T;
// Data type'ını otomatik extract et
type UserData = UnwrapApiResponse<
ApiResponse<{ id: string; name: string }>
>;
// Sonuç: { id: string; name: string }
// Runtime adapter fonksiyonu
function unwrapApiResponse<T>(response: ApiResponse<T>): T {
if (response.status >= 400) {
throw new Error(`API Error: ${response.message}`);
}
return response.data;
}
Button Kütüphanesini Değiştirmek#
Adapter’lar third-party UI kütüphanelerini design system’ına entegre etmeye yardımcı oluyor; böylece bir kütüphaneden diğerine geçiş sadece adapter component’i etkiliyor:
// Internal design system button interface
interface InternalButtonProps {
label: string;
variant: 'primary' | 'secondary' | 'danger';
onClick: () => void;
disabled?: boolean;
}
// Material-UI için adapter component
function 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}
/>
Adapter, değiştirme ihtimalin olan bir third-party servisin (payment processor, cloud provider) etrafında yerini buluyor. TypeScript desteği zayıf bir API’de, karmaşık domain dönüşümü isteyen bir serviste ve sık breaking change çıkaran bir kütüphanede de kendini amorti ediyor. Stabil ve iyi type’lanmış kütüphanelerde (lodash, date-fns), kontrolün altındaki internal utility’lerde ve düz bir fonksiyonun hallettiği bire bir mapping’lerde adapter’a gerek yok.
Facade Neyi Gizler#
Facade’lar karmaşık bir subsystem’a basitleştirilmiş bir interface sağlıyor; initialization karmaşıklığını gizliyor, birden fazla servisi koordine ediyor ve implementation detaylarına coupling’i azaltıyor.
Tek Class Arkasında AWS SDK Çağrıları#
AWS SDK v3 her servis için özel configuration, command objeler ve dikkatli error handling gerektiriyor:
// Facade olmadan - dağınık karmaşıklık
import { 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ı gerektiriyor
await 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' }),
}));
Facade bu kurulumu toplayıp yerine domain operasyonları sunuyor:
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ım
const 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' });
Kayıt Formu Facade’ı#
Formlar genellikle validation, submission, error handling ve analytics içeriyor; bir facade bunların hepsini tek bir method call’un arkasında koordine edebiliyor:
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ım
function 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 Olarak Barrel Export’lar#
Barrel export’lar (index.ts dosyaları) moduller için public facade’lar oluşturuyor:
// lib/index.ts - public API facade
export { 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üyor
import { User, createClient, authenticate } from 'my-lib';
Bu internal yapıyı gizliyor, tüketicileri bozmadan internal’ları refactor etmeni sağlıyor.
Ancak: kod tabanı büyüdükçe barrel export’lar build süresine mal oluyor. Atlassian, barrel dosyalarını kaldırdıktan sonra Jira frontend’inde harcanan build dakikalarında %75 azalma bildirdi. Daha küçük bir kod tabanında Dominik Dorfmeister, barrel’lar üzerinden 11k’dan fazla modül yükleyen bir Next.js sayfasını internal barrel’ları silerek yaklaşık 3.5k’ya (%68) indirdi.
Class Hiyerarşisi Olmadan Composite#
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.
Dosya Ağacı Örneği#
Ders kitabı örneği dosya sistemleri gibi hiyerarşik yapıları içeriyor:
// Component interface
interface FileSystemNode {
name: string;
size: number;
render(): JSX.Element;
}
// Leaf - Dosya
class 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ör
class 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 interface
const 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'),
]);
Pattern sağlam (bireysel item’ları ve collection’ları uniform şekilde ele almak), ancak class seremonisi React’in doğal composition’ını kullanmıyor. Aynı logic idiomatic React ile:
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ım
const 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} />
Compound Components Pattern#
Radix UI, Headless UI ve Reach UI gibi kütüphanelerin kullandığı compound component pattern, implicit state sharing ile esnek API’lar sağlıyor:
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 namespace
Select.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>
Çağıranın Görmediği Proxy’ler#
Proxy’ler objelere erişimi kontrol ediyor, lazy loading, caching, validation veya access control gibi davranışlar ekliyor; TypeScript’te bunu class-based proxy’lerle ya da runtime interception sağlayan JavaScript’in kendi Proxy API’siyle yapabiliyorsun.
React.lazy ve Suspense#
React’in built-in lazy loading’i bir proxy. Talep üzerine kod yüklemek için component render’ını intercept ediyor, Suspense boundary da kod gelene kadar loading state gösteriyor:
// Code-splitting için proxy
const 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>
);
}
Aynı Interface’i Koruyan Cache’li Client#
Proxy’ler calling code’u değiştirmeden caching katmanları ekliyor:
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ı interface
const realClient = new RealApiClient();
const cachedClient = new CachedApiClient(realClient);
// Kullanım değişmedi
const user = await cachedClient.fetchUser('123');
const posts = await cachedClient.fetchPosts('123');
Proxy Trap’leriyle Runtime Validation#
JavaScript’in Proxy API’si runtime interception sağlıyor; her değişiklikte invariant’ları enforce etmesi gereken configuration objeleri veya domain entity’ler için kullanışlı:
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ım
const user = createValidatedProxy(
{ name: '', email: '', age: 0 },
userSchema
);
user.name = 'Ahmet'; // OK
user.email = 'ahmet@example.com'; // OK
user.age = 30; // OK
// user.age = -5; // Validation error fırlatır
// user.email = 'gecersiz'; // Validation error fırlatır
React Query’nin İçindeki Proxy#
React Query, data fetching için proxy görevi görüyor, caching, loading state’leri ve refetching’i yönetiyor:
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 fetch’i intercept ediyor. Üstüne configurable TTL ile otomatik caching, loading ve error state’leri, background refetching, cache invalidation ve request deduplication geliyor.
Admin İşlemlerinin Etrafında Yetkilendirme#
Proxy’ler, real implementation’a dokunmadan authorization ve logging enforce edebilir:
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ım
const adminActions = new RealAdminActions();
const authorizedProxy = new AuthorizedAdminProxy(adminActions, currentUser);
// Proxy her action'dan önce authorization kontrol ediyor
await authorizedProxy.deleteUser('user-123');
Bu Pattern’lerin Kırıldığı Noktalar#
Leaky Facade’lar#
Implementation detaylarını expose eden facade’lar kendi amacını yitiriyor.
Sızdıran:
class CloudStorage {
// Leaky - S3-specific detayları expose ediyor
async upload(command: PutObjectCommand): Promise<void> {
await this.s3.send(command);
}
}
Kapalı:
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);
}
}
Adapter Proliferation#
Her external dependency için adapter oluşturmak, net bir fayda getirmeden maintenance yükü yaratıyor. Adapter, dependency’nin değişme ihtimali yüksekse ya da şekli domain modelinle çakışıyorsa kendini amorti ediyor; diğer her yerde iki hareketli interface arasında senkron tutulacak fazladan bir dosya oluyor.
Composite Overengineering#
Basit component hiyerarşileri için full composite pattern implement etmek, işin gerektirdiğinden fazla makine kurmak demek:
interface Component {
render(): JSX.Element;
getSize(): number;
add(child: Component): void;
remove(child: Component): void;
}
Idiomatic React versiyonu bu seremoniyi atlıyor:
// React'in doğal olarak composition'ı handle etmesine izin ver
function List({ items }: { items: Item[] }) {
return (
<ul>
{items.map(item => <ListItem key={item.id} item={item} />)}
</ul>
);
}
Barrel Export Performance#
Internal moduller için barrel export’lar (index.ts dosyaları) kullanmak build performance’ına zarar veriyor: her barrel import’u arkasındaki modül grafiğini içeri çekiyor, bundler, type checker ve test runner kodun hiç kullanmadığı dosyaları yüklüyor.
Barrel export’ları sadece public library API’leri için kullan. Internal moduller için direkt import et:
// 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';
Wrapper’ı Ne Zaman Tutmalı#
Third-party bir yapı domain modeline sızıyorsa ya da sağlayıcıyı değiştirmeyi bekliyorsan adapter’ı koru. Tek bir çağrının birkaç servisi koordine etmesi gerektiğinde facade yerini buluyor; caching, lazy loading veya yetki kontrolü çağıran taraftan gizli kalmalıysa proxy yerini buluyor. Hook’lar class component’lere ya da düzenleyemediğin library internal’larına ulaşamıyor, orada wrapper doğru cevap olmayı sürdürüyor; wrapping tamamen görselse de öyle. Bir wrapper argümanlarını yalnızca tek bir methoda iletiyorsa, onu sil ve kütüphaneyi doğrudan çağır.
Kaynaklar#
- Structural Design Patterns - Refactoring.Guru (yeni sekmede açılır) - Adapter, Facade, Composite, Proxy ve Decorator dahil GoF yapısal pattern’leri niyet ve yapılarıyla
- Design Patterns in TypeScript - Refactoring.Guru (yeni sekmede açılır) - Her yapısal pattern için TypeScript kod örnekleri
- Composition vs Inheritance - React Documentation (yeni sekmede açılır) - React’in children prop ve özelleştirme pattern’leriyle composition’ı tercih etme önerisi
- TypeScript Handbook - Creating Types from Types (yeni sekmede açılır) - Yapısal pattern’leri güçlendiren mapped type’lar, conditional type’lar ve şablon literal’lar
- The Catalog of Design Patterns - Refactoring.Guru (yeni sekmede açılır) - Yapısal pattern’leri davranışsal karşılıklarıyla çapraz referanslamak için tam pattern katalogu
- How We Achieved 75% Faster Builds by Removing Barrel Files - Atlassian (yeni sekmede açılır) - Jira frontend’inden barrel dosyalarının kaldırılması ve sonrasındaki build dakikası tasarrufunun Atlassian tarafından anlatımı
- Please Stop Using Barrel Files - TkDodo (yeni sekmede açılır) - Bir Next.js kod tabanında internal barrel dosyaları kaldırılmadan önce ve sonra ölçülen modül sayıları
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
Singleton, Factory, Builder ve Prototype pattern'lerinin TypeScript'te evrimi: ES modülleri singleton'ı ne zaman, factory function'lar class'ı ne zaman geçer.
typescript · design-patterns · architecture +1
Domain-Driven Design'a kapsamlı giriş: temel kavramlar, yapı taşları, stratejik desenler ve DDD'yi ne zaman ve nasıl uygulayacağına dair rehber.
domain-driven-design · architecture · design-patterns +2
CDK stack düzeni için yaşam döngüsü testi: bir kaynağın ömrü tek bir dağıtımdan uzunsa kendi uzun ömürlü stack'ine koyun, ona bilinen bir adla erişin.
aws-cdk · infrastructure-as-code · typescript +3
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