15 minlesson

Proxy vs Decorator

Proxy vs Decorator

Proxy and Decorator have similar structures but different intents. Understanding when to use each is important.

Structural Similarity

Both wrap an object and delegate to it:

typescript
1// Decorator structure
2class LoggingDecorator implements ShippingService {
3 constructor(private wrapped: ShippingService) {}
4
5 async getRates(request: RateRequest): Promise<Rate[]> {
6 console.log('Getting rates...');
7 return this.wrapped.getRates(request);
8 }
9}
10
11// Proxy structure
12class LoggingProxy implements ShippingService {
13 constructor(private real: ShippingService) {}
14
15 async getRates(request: RateRequest): Promise<Rate[]> {
16 console.log('Getting rates...');
17 return this.real.getRates(request);
18 }
19}
20
21// Look identical!

Key Differences

Intent

AspectDecoratorProxy
PurposeAdd behaviorControl access
FocusEnhancementManagement
Client awarenessKnows about decorationTransparent

Lifecycle Control

typescript
1// Decorator: Client creates both
2const service = new LoggingDecorator(new CarrierAPI());
3
4// Proxy: Often controls creation of real object
5class LazyProxy implements ShippingService {
6 private real: CarrierAPI | null = null; // Proxy decides when to create
7
8 async getRates(request: RateRequest): Promise<Rate[]> {
9 if (!this.real) {
10 this.real = new CarrierAPI(); // Created on first use
11 }
12 return this.real.getRates(request);
13 }
14}

Access Control

typescript
1// Proxy: Can prevent access entirely
2class AuthProxy implements ShippingService {
3 constructor(private real: ShippingService) {}
4
5 async getRates(request: RateRequest): Promise<Rate[]> {
6 if (!this.isAuthorized()) {
7 throw new UnauthorizedError(); // Blocks access
8 }
9 return this.real.getRates(request);
10 }
11}
12
13// Decorator: Adds behavior but doesn't prevent access
14class AuditDecorator implements ShippingService {
15 async getRates(request: RateRequest): Promise<Rate[]> {
16 this.audit('getRates', request); // Adds logging
17 return this.wrapped.getRates(request); // Always proceeds
18 }
19}

Decision Guide

Use Decorator When:

typescript
1// Adding optional features
2class InsuredShipment extends ShipmentDecorator {
3 getCost(): number {
4 return this.wrapped.getCost() + this.insurancePremium;
5 }
6}
7
8// Stacking multiple enhancements
9let shipment: Shipment = new BasicShipment();
10shipment = new InsuredShipment(shipment);
11shipment = new ExpressShipment(shipment);
12shipment = new FragileShipment(shipment);
13
14// Key: Each decorator adds value

Use Proxy When:

typescript
1// Controlling access
2class CachingProxy {
3 async getRates(request: RateRequest): Promise<Rate[]> {
4 const cached = this.cache.get(key);
5 if (cached) return cached; // May not call real at all
6 return this.real.getRates(request);
7 }
8}
9
10// Managing lifecycle
11class PooledConnectionProxy {
12 private connection: Connection | null = null;
13
14 async query(sql: string): Promise<any> {
15 if (!this.connection) {
16 this.connection = await this.pool.acquire();
17 }
18 return this.connection.query(sql);
19 }
20
21 release(): void {
22 if (this.connection) {
23 this.pool.release(this.connection);
24 this.connection = null;
25 }
26 }
27}
28
29// Key: Proxy controls when/if real object is used

Combined Example

Sometimes you need both:

typescript
1// Proxy for access control
2class SecuredCarrierProxy implements ShippingService {
3 constructor(
4 private real: ShippingService,
5 private auth: AuthService
6 ) {}
7
8 async getRates(request: RateRequest): Promise<Rate[]> {
9 await this.auth.checkPermission('shipping:read');
10 return this.real.getRates(request);
11 }
12}
13
14// Decorator for adding features
15class InsuranceDecorator implements ShippingService {
16 async getRates(request: RateRequest): Promise<Rate[]> {
17 const rates = await this.wrapped.getRates(request);
18 return rates.map(rate => ({
19 ...rate,
20 price: rate.price + this.calculateInsurance(rate)
21 }));
22 }
23}
24
25// Combine them
26const api = new CarrierAPI();
27const secured = new SecuredCarrierProxy(api, authService);
28const withInsurance = new InsuranceDecorator(secured);

Naming Conventions

typescript
1// Decorators often describe what they add
2class LoggingShipmentDecorator { }
3class InsuredShipmentDecorator { }
4class FragileHandlingDecorator { }
5
6// Proxies often describe their control purpose
7class CachingProxy { }
8class LazyInitializationProxy { }
9class AccessControlProxy { }
10class RateLimitingProxy { }

Summary

QuestionDecoratorProxy
Does it add features?YesNo (controls access)
Does it control creation?NoOften yes
Can it prevent operation?RarelyYes
Is stacking common?YesSometimes
Client aware?UsuallyIdeally not

Think:

  • Decorator: "Add more to what it does"
  • Proxy: "Control how/when/if it does it"