15 minlesson

Flyweight Trade-offs and Best Practices

Flyweight Trade-offs and Best Practices

When Flyweight Helps

Good Candidates

typescript
1// ✅ Many instances with shared data
2class ShippingLabel {
3 constructor(
4 private carrier: CarrierInfo, // Shared: ~10 carriers
5 private serviceType: ServiceType, // Shared: ~20 service types
6 private originZone: ZoneInfo, // Shared: ~8 zones
7 private trackingNumber: string, // Unique: must store
8 private weight: number // Unique: must store
9 ) {}
10}
11
12// ✅ Reference data that doesn't change
13class 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

typescript
1// ❌ Few instances - overhead not worth it
2class AdminUser {
3 // Only 5-10 admins, no need for flyweight
4}
5
6// ❌ Unique data per instance
7class TrackingEvent {
8 timestamp: Date; // Unique
9 location: string; // Unique (varies per event)
10 description: string; // Unique
11 // Nothing to share!
12}
13
14// ❌ Mutable shared state
15class MutableSettings {
16 // Flyweights must be immutable
17 settings: Map<string, any>; // ❌ Shared mutable state = bugs
18}

Trade-offs

Memory vs CPU

typescript
1// Without flyweight: More memory, faster access
2class OrderWithFullData {
3 carrierName: string; // Duplicated but fast
4 carrierContact: string;
5 carrierTrackingUrl: string;
6}
7
8// With flyweight: Less memory, lookup overhead
9class OrderWithFlyweight {
10 carrierId: string; // Reference to flyweight
11 getCarrierName(): string {
12 return this.factory.get(this.carrierId).name; // Lookup cost
13 }
14}

Immutability Requirement

typescript
1// Flyweight MUST be immutable
2class CarrierInfo {
3 constructor(
4 readonly id: string,
5 readonly name: string,
6 readonly trackingUrlTemplate: string
7 ) {
8 Object.freeze(this);
9 }
10
11 // ❌ NEVER do this
12 updateName(name: string): void {
13 (this as any).name = name; // Affects ALL references!
14 }
15
16 // ✅ Create new flyweight instead
17 withName(name: string): CarrierInfo {
18 return new CarrierInfo(this.id, name, this.trackingUrlTemplate);
19 }
20}

Extrinsic State Management

typescript
1// Option 1: Pass extrinsic state to methods
2class ZoneInfo {
3 calculateRate(weight: number, dimensions: Dims): number {
4 return this.baseRate + weight * this.perPound;
5 }
6}
7
8// Option 2: Context object holds flyweight + extrinsic
9class ShipmentContext {
10 constructor(
11 public readonly zone: ZoneInfo, // Flyweight
12 public weight: number, // Extrinsic
13 public dimensions: Dims // Extrinsic
14 ) {}
15
16 getRate(): number {
17 return this.zone.calculateRate(this.weight, this.dimensions);
18 }
19}
20
21// Option 3: Separate flyweight reference
22interface Shipment {
23 zoneId: number; // Reference to flyweight
24 weight: number; // Extrinsic
25 dimensions: Dims; // Extrinsic
26}
27
28function getRate(shipment: Shipment, zoneFactory: ZoneFactory): number {
29 const zone = zoneFactory.get(shipment.zoneId);
30 return zone.calculateRate(shipment.weight, shipment.dimensions);
31}

Cache Management

typescript
1class FlyweightFactory<K, V> {
2 private cache = new Map<K, V>();
3 private accessOrder: K[] = [];
4 private maxSize: number;
5
6 constructor(maxSize: number = 1000) {
7 this.maxSize = maxSize;
8 }
9
10 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 }
17
18 // Evict LRU if at capacity
19 if (this.cache.size >= this.maxSize) {
20 const lru = this.accessOrder.shift()!;
21 this.cache.delete(lru);
22 }
23
24 const value = creator();
25 this.cache.set(key, value);
26 this.accessOrder.push(key);
27 return value;
28 }
29
30 clear(): void {
31 this.cache.clear();
32 this.accessOrder = [];
33 }
34}

Testing Flyweights

typescript
1describe('CityInfoFactory', () => {
2 let factory: CityInfoFactory;
3
4 beforeEach(() => {
5 factory = new CityInfoFactory();
6 });
7
8 it('returns same instance for same key', () => {
9 const city1 = factory.get('New York', 'NY');
10 const city2 = factory.get('New York', 'NY');
11
12 expect(city1).toBe(city2); // Same reference
13 });
14
15 it('returns different instances for different keys', () => {
16 const nyc = factory.get('New York', 'NY');
17 const la = factory.get('Los Angeles', 'CA');
18
19 expect(nyc).not.toBe(la);
20 });
21
22 it('flyweights are immutable', () => {
23 const city = factory.get('New York', 'NY');
24
25 expect(() => {
26 (city as any).city = 'Changed';
27 }).toThrow(); // Object.freeze prevents mutation
28 });
29
30 it('tracks cache statistics', () => {
31 factory.get('New York', 'NY');
32 factory.get('New York', 'NY');
33 factory.get('Los Angeles', 'CA');
34
35 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

AspectConsideration
MemorySignificant savings with many objects
CPULookup overhead for each access
ComplexityMore complex than direct storage
ImmutabilityRequired - affects design
TestingVerify identity and immutability
Cache SizeMay need LRU eviction

Use Flyweight when memory savings outweigh complexity costs.