Flyweight Trade-offs and Best Practices
When Flyweight Helps
Good Candidates
typescript1// ✅ Many instances with shared data2class ShippingLabel {3 constructor(4 private carrier: CarrierInfo, // Shared: ~10 carriers5 private serviceType: ServiceType, // Shared: ~20 service types6 private originZone: ZoneInfo, // Shared: ~8 zones7 private trackingNumber: string, // Unique: must store8 private weight: number // Unique: must store9 ) {}10}1112// ✅ Reference data that doesn't change13class CountryInfo {14 constructor(15 readonly code: string, // "US"16 readonly name: string, // "United States"17 readonly currency: string, // "USD"18 readonly phoneCode: string // "+1"19 ) {20 Object.freeze(this);21 }22}
Poor Candidates
typescript1// ❌ Few instances - overhead not worth it2class AdminUser {3 // Only 5-10 admins, no need for flyweight4}56// ❌ Unique data per instance7class TrackingEvent {8 timestamp: Date; // Unique9 location: string; // Unique (varies per event)10 description: string; // Unique11 // Nothing to share!12}1314// ❌ Mutable shared state15class MutableSettings {16 // Flyweights must be immutable17 settings: Map<string, any>; // ❌ Shared mutable state = bugs18}
Trade-offs
Memory vs CPU
typescript1// Without flyweight: More memory, faster access2class OrderWithFullData {3 carrierName: string; // Duplicated but fast4 carrierContact: string;5 carrierTrackingUrl: string;6}78// With flyweight: Less memory, lookup overhead9class OrderWithFlyweight {10 carrierId: string; // Reference to flyweight11 getCarrierName(): string {12 return this.factory.get(this.carrierId).name; // Lookup cost13 }14}
Immutability Requirement
typescript1// Flyweight MUST be immutable2class CarrierInfo {3 constructor(4 readonly id: string,5 readonly name: string,6 readonly trackingUrlTemplate: string7 ) {8 Object.freeze(this);9 }1011 // ❌ NEVER do this12 updateName(name: string): void {13 (this as any).name = name; // Affects ALL references!14 }1516 // ✅ Create new flyweight instead17 withName(name: string): CarrierInfo {18 return new CarrierInfo(this.id, name, this.trackingUrlTemplate);19 }20}
Extrinsic State Management
typescript1// Option 1: Pass extrinsic state to methods2class ZoneInfo {3 calculateRate(weight: number, dimensions: Dims): number {4 return this.baseRate + weight * this.perPound;5 }6}78// Option 2: Context object holds flyweight + extrinsic9class ShipmentContext {10 constructor(11 public readonly zone: ZoneInfo, // Flyweight12 public weight: number, // Extrinsic13 public dimensions: Dims // Extrinsic14 ) {}1516 getRate(): number {17 return this.zone.calculateRate(this.weight, this.dimensions);18 }19}2021// Option 3: Separate flyweight reference22interface Shipment {23 zoneId: number; // Reference to flyweight24 weight: number; // Extrinsic25 dimensions: Dims; // Extrinsic26}2728function getRate(shipment: Shipment, zoneFactory: ZoneFactory): number {29 const zone = zoneFactory.get(shipment.zoneId);30 return zone.calculateRate(shipment.weight, shipment.dimensions);31}
Cache Management
typescript1class FlyweightFactory<K, V> {2 private cache = new Map<K, V>();3 private accessOrder: K[] = [];4 private maxSize: number;56 constructor(maxSize: number = 1000) {7 this.maxSize = maxSize;8 }910 get(key: K, creator: () => V): V {11 if (this.cache.has(key)) {12 // Move to end (most recently used)13 this.accessOrder = this.accessOrder.filter(k => k !== key);14 this.accessOrder.push(key);15 return this.cache.get(key)!;16 }1718 // Evict LRU if at capacity19 if (this.cache.size >= this.maxSize) {20 const lru = this.accessOrder.shift()!;21 this.cache.delete(lru);22 }2324 const value = creator();25 this.cache.set(key, value);26 this.accessOrder.push(key);27 return value;28 }2930 clear(): void {31 this.cache.clear();32 this.accessOrder = [];33 }34}
Testing Flyweights
typescript1describe('CityInfoFactory', () => {2 let factory: CityInfoFactory;34 beforeEach(() => {5 factory = new CityInfoFactory();6 });78 it('returns same instance for same key', () => {9 const city1 = factory.get('New York', 'NY');10 const city2 = factory.get('New York', 'NY');1112 expect(city1).toBe(city2); // Same reference13 });1415 it('returns different instances for different keys', () => {16 const nyc = factory.get('New York', 'NY');17 const la = factory.get('Los Angeles', 'CA');1819 expect(nyc).not.toBe(la);20 });2122 it('flyweights are immutable', () => {23 const city = factory.get('New York', 'NY');2425 expect(() => {26 (city as any).city = 'Changed';27 }).toThrow(); // Object.freeze prevents mutation28 });2930 it('tracks cache statistics', () => {31 factory.get('New York', 'NY');32 factory.get('New York', 'NY');33 factory.get('Los Angeles', 'CA');3435 const stats = factory.getStats();36 expect(stats.hits).toBe(1);37 expect(stats.misses).toBe(2);38 expect(stats.size).toBe(2);39 });40});
Summary
| Aspect | Consideration |
|---|---|
| Memory | Significant savings with many objects |
| CPU | Lookup overhead for each access |
| Complexity | More complex than direct storage |
| Immutability | Required - affects design |
| Testing | Verify identity and immutability |
| Cache Size | May need LRU eviction |
Use Flyweight when memory savings outweigh complexity costs.