Loading learning content...
Design patterns aren't decorative—they're prescriptions for recurring problems. In our parking lot system, two patterns emerge naturally from our requirements:
In this page, we'll implement both patterns with production-quality code, exploring variations, trade-offs, and the precise situations where each shines.
By the end of this page, you will understand how to apply Factory patterns for flexible object creation, implement Strategy patterns for interchangeable algorithms, combine patterns effectively, and articulate why you chose specific patterns in an interview context.
The Factory pattern encapsulates object creation logic, allowing clients to request objects without knowing the concrete classes involved. It comes in several flavors:
Factory Method — A method that creates objects. The method can be overridden by subclasses to change the type of object created.
Simple Factory — A dedicated class with static methods for creating different types of objects.
Abstract Factory — A factory interface with multiple factory methods, allowing families of related objects to be created together.
For our parking lot, we'll use Simple Factory for Vehicle and ParkingSpot creation, as it provides the right balance of simplicity and flexibility.
| Factory Type | Use When | Complexity | Flexibility |
|---|---|---|---|
| Factory Method | Subclasses need to determine what to create | Medium | High for subclass variation |
| Simple Factory | Centralized creation logic, multiple types | Low | Medium - add types by modifying factory |
| Abstract Factory | Families of related objects must be created together | High | Very High - swap entire families |
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788
// ============================================// SIMPLE FACTORY: Vehicle Creation// ============================================ // Requirement: FR-1 - Support multiple vehicle types// Requirement: NFR-4 - Adding new types should be easy class VehicleFactory { /** * Create a vehicle from type string * Useful for API/user input where type comes as string */ static fromType(licensePlate: string, typeString: string): IVehicle { const type = this.parseVehicleType(typeString); return new Vehicle(licensePlate, type); } /** * Create a vehicle from VehicleType enum * Type-safe when caller knows the type */ static create(licensePlate: string, type: VehicleType): IVehicle { this.validateLicensePlate(licensePlate); return new Vehicle(licensePlate, type); } // Convenience methods for common types static createMotorcycle(licensePlate: string): IVehicle { return this.create(licensePlate, VehicleType.MOTORCYCLE); } static createCar(licensePlate: string): IVehicle { return this.create(licensePlate, VehicleType.CAR); } static createBus(licensePlate: string): IVehicle { return this.create(licensePlate, VehicleType.BUS); } // Parse string to VehicleType with validation private static parseVehicleType(typeString: string): VehicleType { const normalized = typeString.toUpperCase().trim(); if (!Object.values(VehicleType).includes(normalized as VehicleType)) { throw new InvalidVehicleTypeError( `Unknown vehicle type: ${typeString}. \Valid types: ${Object.values(VehicleType).join(', ')}` ); } return normalized as VehicleType; } // Validate license plate format private static validateLicensePlate(plate: string): void { if (!plate || plate.trim().length === 0) { throw new ValidationError('License plate cannot be empty'); } // Example format validation (customize per region) const plateRegex = /^[A-Z0-9]{2,10}$/i; if (!plateRegex.test(plate.trim())) { throw new ValidationError( `Invalid license plate format: ${plate}` ); } }} // Custom error types for factory failuresclass InvalidVehicleTypeError extends Error { constructor(message: string) { super(message); this.name = 'InvalidVehicleTypeError'; }} class ValidationError extends Error { constructor(message: string) { super(message); this.name = 'ValidationError'; }} // Usage examplesconst car = VehicleFactory.createCar('ABC123');const bus = VehicleFactory.fromType('XYZ789', 'bus');const motorcycle = VehicleFactory.create('M00001', VehicleType.MOTORCYCLE);Notice how the factory centralizes: 1) Validation logic (license plate format), 2) Type parsing (string to enum), 3) Error handling (custom exceptions), 4) Object construction. If we later need to change how vehicles are created (e.g., add logging, caching, or switch to subclasses), we change one class.
Creating parking spots is more complex than vehicles because spots exist in a spatial context—they have floors, rows, and positions. The factory needs to support both individual creation and bulk initialization.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
// ============================================// FACTORY: ParkingSpot Creation// ============================================ // Configuration for spot layout on a floorinterface FloorConfiguration { floorNumber: number; layout: SpotLayoutConfig[];} interface SpotLayoutConfig { spotType: SpotType; count: number; rowStart?: number; // Optional, auto-assign if not specified} class ParkingSpotFactory { /** * Create a single spot */ static create( floor: number, row: number, spotNumber: number, type: SpotType ): IParkingSpot { return new ParkingSpot(floor, row, spotNumber, type); } /** * Create a range of spots of the same type * Useful for initializing sections */ static createRange( floor: number, row: number, startNumber: number, count: number, type: SpotType ): IParkingSpot[] { const spots: IParkingSpot[] = []; for (let i = 0; i < count; i++) { spots.push(new ParkingSpot(floor, row, startNumber + i, type)); } return spots; } /** * Create all spots for a floor based on configuration * This is the main method used during lot initialization */ static createFloor(config: FloorConfiguration): IParkingSpot[] { const spots: IParkingSpot[] = []; let currentRow = 1; let spotNumber = 1; for (const layoutConfig of config.layout) { const row = layoutConfig.rowStart ?? currentRow; for (let i = 0; i < layoutConfig.count; i++) { spots.push(new ParkingSpot( config.floorNumber, row, spotNumber++, layoutConfig.spotType )); } currentRow = row + 1; } return spots; } /** * Create entire parking lot spot inventory * Master method for lot initialization */ static createLotInventory( floors: number, spotsPerFloorByType: Map<SpotType, number> ): Map<string, IParkingSpot> { const allSpots = new Map<string, IParkingSpot>(); for (let floor = 1; floor <= floors; floor++) { let spotNumber = 1; for (const [spotType, count] of spotsPerFloorByType) { const floorSpots = this.createRange( floor, 1, // Single row per floor for simplicity spotNumber, count, spotType ); for (const spot of floorSpots) { allSpots.set(spot.id, spot); } spotNumber += count; } } return allSpots; } /** * Create from JSON configuration * Useful for loading lot configurations from files/database */ static fromJSON(json: string): IParkingSpot[] { const configs: FloorConfiguration[] = JSON.parse(json); const spots: IParkingSpot[] = []; for (const config of configs) { spots.push(...this.createFloor(config)); } return spots; }} // Example usageconst lotInventory = ParkingSpotFactory.createLotInventory( 3, // 3 floors new Map([ [SpotType.MOTORCYCLE, 20], // 20 motorcycle spots per floor [SpotType.COMPACT, 50], // 50 compact spots per floor [SpotType.LARGE, 10], // 10 large spots per floor ])); console.log(`Created ${lotInventory.size} spots`); // 240 spots totalNotice how the factory supports both programmatic and configuration-based creation. In production, lot layouts often come from JSON files or database records. The fromJSON method bridges external configuration to our domain objects, keeping the domain model clean from serialization concerns.
The Strategy pattern encapsulates algorithms in separate classes, making them interchangeable. The client (in our case, ParkingLot) delegates to a strategy interface without knowing which concrete algorithm is in use.
The Pattern Structure:
In our parking lot, we apply Strategy to:
Why Strategy Instead of Conditionals?
Without Strategy, the ParkingLot would be littered with conditionals:
// BAD: Pricing logic embedded in ParkingLot
calculateFee(ticket: Ticket): number {
if (this.pricingMode === 'hourly') {
return ticket.hours * this.hourlyRate;
} else if (this.pricingMode === 'daily') {
return Math.min(ticket.hours * this.hourlyRate, this.dailyMax);
} else if (this.pricingMode === 'tiered') {
// Complex tiered logic...
}
// Every new mode requires modifying this method
}
This violates Open/Closed Principle: adding new pricing modes requires changing ParkingLot. Strategy fixes this by extracting each mode into its own class.
Let's implement a comprehensive pricing strategy system that supports hourly rates, daily maximums, vehicle-type variations, and time-of-day adjustments.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
// ============================================// STRATEGY: Pricing Calculation// ============================================ // Strategy interface - the contract all pricing strategies must fulfillinterface IPricingStrategy { calculateFee(ticket: ITicket): Money; getDisplayName(): string; getDescription(): string;} // Rate configuration per vehicle typeinterface VehicleRates { [VehicleType.MOTORCYCLE]: number; [VehicleType.CAR]: number; [VehicleType.BUS]: number;} // ============================================// CONCRETE STRATEGY 1: Simple Hourly Pricing// ============================================class HourlyPricingStrategy implements IPricingStrategy { private readonly ratesPerHour: VehicleRates; constructor(ratesPerHour: VehicleRates) { this.ratesPerHour = ratesPerHour; } calculateFee(ticket: ITicket): Money { const hours = ticket.getDurationHours(); const rate = this.ratesPerHour[ticket.vehicleType]; if (rate === undefined) { throw new Error(`No rate defined for vehicle type: ${ticket.vehicleType}`); } return new Money(hours * rate); } getDisplayName(): string { return 'Hourly Rate'; } getDescription(): string { return 'Flat hourly rate based on vehicle type, charged per hour or partial hour'; }} // ============================================// CONCRETE STRATEGY 2: Daily Maximum Pricing// ============================================// Charges hourly but caps at a daily maximumclass DailyMaxPricingStrategy implements IPricingStrategy { private readonly hourlyRates: VehicleRates; private readonly dailyMaxRates: VehicleRates; constructor(hourlyRates: VehicleRates, dailyMaxRates: VehicleRates) { this.hourlyRates = hourlyRates; this.dailyMaxRates = dailyMaxRates; } calculateFee(ticket: ITicket): Money { const hours = ticket.getDurationHours(); const hourlyRate = this.hourlyRates[ticket.vehicleType]; const dailyMax = this.dailyMaxRates[ticket.vehicleType]; const days = Math.floor(hours / 24); const remainingHours = hours % 24; // Full days charged at daily max const dayCharge = days * dailyMax; // Remaining hours charged hourly, capped at daily max const hourCharge = Math.min(remainingHours * hourlyRate, dailyMax); return new Money(dayCharge + hourCharge); } getDisplayName(): string { return 'Hourly with Daily Max'; } getDescription(): string { return 'Hourly charging capped at daily maximum, making multi-day stays economical'; }} // ============================================// CONCRETE STRATEGY 3: Tiered Pricing// ============================================// Different rates for different time blocksinterface PricingTier { upToHours: number; // This tier applies for hours 0 to upToHours ratePerHour: number;} class TieredPricingStrategy implements IPricingStrategy { private readonly tiersByVehicleType: Map<VehicleType, PricingTier[]>; constructor(tiersByVehicleType: Map<VehicleType, PricingTier[]>) { this.tiersByVehicleType = tiersByVehicleType; } calculateFee(ticket: ITicket): Money { const hours = ticket.getDurationHours(); const tiers = this.tiersByVehicleType.get(ticket.vehicleType); if (!tiers || tiers.length === 0) { throw new Error(`No pricing tiers for: ${ticket.vehicleType}`); } let totalFee = 0; let hoursRemaining = hours; let lastTierEnd = 0; for (const tier of tiers) { if (hoursRemaining <= 0) break; const tierHours = tier.upToHours - lastTierEnd; const hoursInThisTier = Math.min(hoursRemaining, tierHours); totalFee += hoursInThisTier * tier.ratePerHour; hoursRemaining -= hoursInThisTier; lastTierEnd = tier.upToHours; } // Any hours beyond last tier use last tier's rate if (hoursRemaining > 0) { const lastTier = tiers[tiers.length - 1]; totalFee += hoursRemaining * lastTier.ratePerHour; } return new Money(totalFee); } getDisplayName(): string { return 'Tiered Pricing'; } getDescription(): string { return 'Different rates for different duration ranges (e.g., first 2 hours at $3/hr, then $2/hr)'; }} // ============================================// CONCRETE STRATEGY 4: Time-of-Day Pricing// ============================================interface TimeSlot { startHour: number; // 0-23 endHour: number; // 0-23 (exclusive) multiplier: number; // e.g., 1.5 for peak hours} class TimeOfDayPricingStrategy implements IPricingStrategy { private readonly baseStrategy: IPricingStrategy; private readonly timeSlots: TimeSlot[]; constructor(baseStrategy: IPricingStrategy, timeSlots: TimeSlot[]) { this.baseStrategy = baseStrategy; this.timeSlots = timeSlots; } calculateFee(ticket: ITicket): Money { // Start with base calculation const baseFee = this.baseStrategy.calculateFee(ticket); // Calculate average multiplier based on hours parked const avgMultiplier = this.calculateAverageMultiplier( ticket.entryTime, ticket.getDurationHours() ); return baseFee.multiply(avgMultiplier); } private calculateAverageMultiplier(entryTime: Date, hours: number): number { // Simplified: use entry hour's multiplier // Production would calculate hour-by-hour const entryHour = entryTime.getHours(); for (const slot of this.timeSlots) { if (entryHour >= slot.startHour && entryHour < slot.endHour) { return slot.multiplier; } } return 1.0; // Default: no adjustment } getDisplayName(): string { return 'Dynamic Time-of-Day Pricing'; } getDescription(): string { return 'Base pricing adjusted by time-of-day multipliers (peak/off-peak)'; }}Notice how TimeOfDayPricingStrategy wraps another strategy (baseStrategy). This is the Decorator pattern combined with Strategy—you can layer pricing adjustments: 'Take the tiered pricing, add time-of-day multipliers, then add holiday surcharges.' Each decorator adds behavior without modifying existing strategies.
Spot allocation is about finding the best available spot for a vehicle. Different strategies optimize for different goals: minimizing search time, reducing walking distance, or balancing wear across the lot.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
// ============================================// STRATEGY: Spot Allocation// ============================================ interface IAllocationStrategy { findSpot( vehicle: IVehicle, spotsByType: Map<SpotType, IParkingSpot[]> ): IParkingSpot | null; getStrategyName(): string;} // ============================================// CONCRETE STRATEGY 1: First Available// ============================================// Simple: return the first available spot that fits// Optimizes for: Allocation speed (O(n) where n = spots of preferred type)class FirstAvailableStrategy implements IAllocationStrategy { findSpot( vehicle: IVehicle, spotsByType: Map<SpotType, IParkingSpot[]> ): IParkingSpot | null { const compatibleTypes = vehicle.getCompatibleSpotTypes(); // Try each compatible type in preference order for (const spotType of compatibleTypes) { const spots = spotsByType.get(spotType) || []; for (const spot of spots) { if (spot.isAvailable() && spot.canFit(vehicle)) { return spot; } } } return null; // No spot found } getStrategyName(): string { return 'First Available'; }} // ============================================// CONCRETE STRATEGY 2: Nearest Exit// ============================================// Find the available spot closest to the exit// Optimizes for: Customer convenience (walking distance)class NearestExitStrategy implements IAllocationStrategy { private readonly exitLocation: SpotLocation; constructor(exitFloor: number, exitRow: number, exitSpot: number) { this.exitLocation = { floor: exitFloor, row: exitRow, spot: exitSpot }; } findSpot( vehicle: IVehicle, spotsByType: Map<SpotType, IParkingSpot[]> ): IParkingSpot | null { const compatibleTypes = vehicle.getCompatibleSpotTypes(); let nearestSpot: IParkingSpot | null = null; let minDistance = Infinity; for (const spotType of compatibleTypes) { const spots = spotsByType.get(spotType) || []; for (const spot of spots) { if (spot.isAvailable() && spot.canFit(vehicle)) { const distance = this.calculateDistance(spot); if (distance < minDistance) { minDistance = distance; nearestSpot = spot; } } } } return nearestSpot; } private calculateDistance(spot: IParkingSpot): number { // Manhattan distance considering floor differences are costly const floorPenalty = 10; // Moving between floors is expensive const floorDiff = Math.abs(spot.floor - this.exitLocation.floor); const rowDiff = Math.abs(spot.row - this.exitLocation.row); const spotDiff = Math.abs(spot.spotNumber - this.exitLocation.spot); return (floorDiff * floorPenalty) + rowDiff + spotDiff; } getStrategyName(): string { return 'Nearest to Exit'; }} // ============================================// CONCRETE STRATEGY 3: Distributed Allocation// ============================================// Spread vehicles across floors evenly// Optimizes for: Load balancing, even wear distributionclass DistributedAllocationStrategy implements IAllocationStrategy { findSpot( vehicle: IVehicle, spotsByType: Map<SpotType, IParkingSpot[]> ): IParkingSpot | null { const compatibleTypes = vehicle.getCompatibleSpotTypes(); // Group available spots by floor const availableByFloor = new Map<number, IParkingSpot[]>(); for (const spotType of compatibleTypes) { const spots = spotsByType.get(spotType) || []; for (const spot of spots) { if (spot.isAvailable() && spot.canFit(vehicle)) { if (!availableByFloor.has(spot.floor)) { availableByFloor.set(spot.floor, []); } availableByFloor.get(spot.floor)!.push(spot); } } } if (availableByFloor.size === 0) return null; // Find the floor with the most available spots (least utilized) let targetFloor = 1; let maxAvailable = 0; for (const [floor, spots] of availableByFloor) { if (spots.length > maxAvailable) { maxAvailable = spots.length; targetFloor = floor; } } // Return first available on that floor return availableByFloor.get(targetFloor)?.[0] || null; } getStrategyName(): string { return 'Distributed (Load Balanced)'; }} // ============================================// CONCRETE STRATEGY 4: Compact-First// ============================================// Always use the smallest suitable spot// Optimizes for: Capacity efficiency (saves large spots for large vehicles)class CompactFirstStrategy implements IAllocationStrategy { findSpot( vehicle: IVehicle, spotsByType: Map<SpotType, IParkingSpot[]> ): IParkingSpot | null { const compatibleTypes = vehicle.getCompatibleSpotTypes(); // compatibleTypes is already ordered by preference (smallest first) // So we just return the first available in preference order for (const spotType of compatibleTypes) { const spots = spotsByType.get(spotType) || []; for (const spot of spots) { if (spot.isAvailable()) { return spot; } } } return null; } getStrategyName(): string { return 'Compact First (Space Efficient)'; }}| Strategy | Time Complexity | Best For | Trade-off |
|---|---|---|---|
| First Available | O(n) worst case, O(1) average | Maximum throughput | May cluster near entrance |
| Nearest Exit | O(n) always (must check all) | Customer convenience | Slower allocation |
| Distributed | O(n) to group, O(1) to select | Even wear, aesthetics | Inefficient for short stays |
| Compact First | O(n) worst case | Capacity efficiency | May fragment availability |
In production systems, strategies might change based on configuration, time of day, or special events. Let's implement a strategy selector that can switch strategies dynamically.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
// ============================================// STRATEGY REGISTRY AND FACTORY// ============================================ // Registry of available pricing strategiesclass PricingStrategyRegistry { private static readonly strategies = new Map<string, () => IPricingStrategy>(); static { // Register default strategies this.register('hourly', () => new HourlyPricingStrategy({ [VehicleType.MOTORCYCLE]: 2, [VehicleType.CAR]: 4, [VehicleType.BUS]: 8, })); this.register('daily-max', () => new DailyMaxPricingStrategy( { [VehicleType.MOTORCYCLE]: 2, [VehicleType.CAR]: 4, [VehicleType.BUS]: 8 }, { [VehicleType.MOTORCYCLE]: 15, [VehicleType.CAR]: 30, [VehicleType.BUS]: 50 } )); } static register(name: string, factory: () => IPricingStrategy): void { this.strategies.set(name.toLowerCase(), factory); } static get(name: string): IPricingStrategy { const factory = this.strategies.get(name.toLowerCase()); if (!factory) { throw new Error(`Unknown pricing strategy: ${name}`); } return factory(); } static getAvailable(): string[] { return Array.from(this.strategies.keys()); }} // Context-aware strategy selectorinterface PricingContext { dayOfWeek: number; // 0 = Sunday hour: number; // 0-23 isHoliday: boolean; lotOccupancy: number; // 0.0 to 1.0} class ContextualPricingStrategy implements IPricingStrategy { private readonly weekdayStrategy: IPricingStrategy; private readonly weekendStrategy: IPricingStrategy; private readonly holidayStrategy: IPricingStrategy; private readonly highDemandStrategy: IPricingStrategy; private readonly demandThreshold: number; constructor(config: { weekday: IPricingStrategy; weekend: IPricingStrategy; holiday: IPricingStrategy; highDemand: IPricingStrategy; demandThreshold?: number; }) { this.weekdayStrategy = config.weekday; this.weekendStrategy = config.weekend; this.holidayStrategy = config.holiday; this.highDemandStrategy = config.highDemand; this.demandThreshold = config.demandThreshold ?? 0.85; } calculateFee(ticket: ITicket): Money { const strategy = this.selectStrategy(ticket); return strategy.calculateFee(ticket); } private selectStrategy(ticket: ITicket): IPricingStrategy { const context = this.buildContext(ticket); // Priority order: Holiday > High Demand > Weekend > Weekday if (context.isHoliday) { return this.holidayStrategy; } if (context.lotOccupancy > this.demandThreshold) { return this.highDemandStrategy; } if (context.dayOfWeek === 0 || context.dayOfWeek === 6) { return this.weekendStrategy; } return this.weekdayStrategy; } private buildContext(ticket: ITicket): PricingContext { const entryDate = ticket.entryTime; return { dayOfWeek: entryDate.getDay(), hour: entryDate.getHours(), isHoliday: this.checkHoliday(entryDate), lotOccupancy: this.getCurrentOccupancy(), }; } private checkHoliday(date: Date): boolean { // In production, check against a holiday calendar service return false; } private getCurrentOccupancy(): number { // In production, query from lot manager return 0.5; } getDisplayName(): string { return 'Contextual Dynamic Pricing'; } getDescription(): string { return 'Automatically selects pricing strategy based on day, time, and demand'; }} // Usage: Wire into ParkingLotconst dynamicPricing = new ContextualPricingStrategy({ weekday: PricingStrategyRegistry.get('hourly'), weekend: PricingStrategyRegistry.get('daily-max'), holiday: new HourlyPricingStrategy({ [VehicleType.MOTORCYCLE]: 3, [VehicleType.CAR]: 6, [VehicleType.BUS]: 12, }), highDemand: new HourlyPricingStrategy({ [VehicleType.MOTORCYCLE]: 4, [VehicleType.CAR]: 8, [VehicleType.BUS]: 15, }), demandThreshold: 0.80,}); const parkingLot = new ParkingLot(config, dynamicPricing, allocationStrategy);ContextualPricingStrategy is itself a strategy that delegates to other strategies. This is the power of good abstraction: strategies can be composed, chained, and layered. The ParkingLot doesn't care—it just calls calculateFee() on whatever strategy it has.
Let's see how Factory and Strategy patterns integrate into the complete ParkingLot implementation.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
// ============================================// COMPLETE PARKINGLOT WITH FACTORY AND STRATEGY// ============================================ interface ParkingLotConfig { name: string; address: string; floors: number; spotsPerFloorByType: Map<SpotType, number>;} class ParkingLot implements IEntryGate, IExitGate, IDisplayBoard { readonly name: string; readonly address: string; // Spot storage - created via Factory private readonly spotsByType: Map<SpotType, IParkingSpot[]>; private readonly allSpots: Map<string, IParkingSpot>; // Ticket storage private readonly activeTickets: Map<string, Ticket> = new Map(); // Strategies - injected via constructor private readonly pricingStrategy: IPricingStrategy; private readonly allocationStrategy: IAllocationStrategy; constructor( config: ParkingLotConfig, pricingStrategy: IPricingStrategy, allocationStrategy: IAllocationStrategy ) { this.name = config.name; this.address = config.address; this.pricingStrategy = pricingStrategy; this.allocationStrategy = allocationStrategy; // Use Factory to create all spots this.allSpots = ParkingSpotFactory.createLotInventory( config.floors, config.spotsPerFloorByType ); // Organize spots by type for strategy use this.spotsByType = this.organizeSpotsByType(); } private organizeSpotsByType(): Map<SpotType, IParkingSpot[]> { const byType = new Map<SpotType, IParkingSpot[]>(); for (const type of Object.values(SpotType)) { byType.set(type as SpotType, []); } for (const spot of this.allSpots.values()) { byType.get(spot.type)!.push(spot); } return byType; } // ============================================ // IEntryGate Implementation // ============================================ hasAvailableSpot(vehicleType: VehicleType): boolean { const tempVehicle = VehicleFactory.create('CHECK', vehicleType); const spot = this.allocationStrategy.findSpot( tempVehicle, this.spotsByType ); return spot !== null; } parkVehicle(vehicle: IVehicle): Ticket { // Strategy finds the best spot const spot = this.allocationStrategy.findSpot( vehicle, this.spotsByType ); if (!spot) { throw new NoAvailableSpotError( `No available spot for vehicle type: ${vehicle.type}` ); } // Park in the allocated spot spot.park(vehicle); // Create ticket (could be via Factory too) const ticket = new Ticket( TicketIdGenerator.generate(), vehicle, spot, new Date() ); this.activeTickets.set(ticket.ticketId, ticket); return ticket; } // ============================================ // IExitGate Implementation // ============================================ validateTicket(ticketId: string): boolean { return this.activeTickets.has(ticketId); } exitVehicle(ticket: Ticket): Money { // Validate ticket is active if (!this.activeTickets.has(ticket.ticketId)) { throw new InvalidTicketError( `Ticket ${ticket.ticketId} is not valid or already used` ); } // Find and vacate the spot const spot = this.allSpots.get(ticket.spotId); if (spot) { spot.vacate(); } // Strategy calculates the fee const fee = this.pricingStrategy.calculateFee(ticket); // Mark ticket as exited ticket.markExited(new Date(), fee.amount); // Remove from active tickets this.activeTickets.delete(ticket.ticketId); return fee; } // ============================================ // IDisplayBoard Implementation // ============================================ getTotalCapacity(): number { return this.allSpots.size; } getCurrentOccupancy(): number { let occupied = 0; for (const spot of this.allSpots.values()) { if (!spot.isAvailable()) occupied++; } return occupied; } getAvailableSpotsByType(): Map<SpotType, number> { const result = new Map<SpotType, number>(); for (const [type, spots] of this.spotsByType) { result.set(type, spots.filter(s => s.isAvailable()).length); } return result; } isFull(): boolean { return this.getCurrentOccupancy() >= this.getTotalCapacity(); }} // Custom errorsclass NoAvailableSpotError extends Error { constructor(message: string) { super(message); this.name = 'NoAvailableSpotError'; }} class InvalidTicketError extends Error { constructor(message: string) { super(message); this.name = 'InvalidTicketError'; }} // ============================================// USAGE EXAMPLE// ============================================ // Configurationconst lotConfig: ParkingLotConfig = { name: 'Downtown Parking Garage', address: '123 Main Street', floors: 3, spotsPerFloorByType: new Map([ [SpotType.MOTORCYCLE, 20], [SpotType.COMPACT, 50], [SpotType.LARGE, 10], ]),}; // Create strategiesconst pricing = new DailyMaxPricingStrategy( { [VehicleType.MOTORCYCLE]: 2, [VehicleType.CAR]: 4, [VehicleType.BUS]: 8 }, { [VehicleType.MOTORCYCLE]: 15, [VehicleType.CAR]: 30, [VehicleType.BUS]: 50 }); const allocation = new NearestExitStrategy(1, 1, 1); // Create the parking lotconst lot = new ParkingLot(lotConfig, pricing, allocation); // Use itconst myCar = VehicleFactory.createCar('ABC123');const myTicket = lot.parkVehicle(myCar); console.log(`Parked at: ${myTicket.spotId}`);console.log(`Available spots: ${JSON.stringify([...lot.getAvailableSpotsByType()])}`); // Later...const fee = lot.exitVehicle(myTicket);console.log(`Fee: ${fee.toString()}`);What's next:
With factories creating our objects and strategies handling our algorithms, we need to consider what happens at the edges—the exceptional cases, the unusual scenarios, and the extensions that transform a classroom exercise into a production system. The next page covers edge cases and extensions.
You now understand how to apply Factory and Strategy patterns effectively to the parking lot domain. These patterns trace directly to our requirements and provide the flexibility needed for a production system. Next, we explore edge cases and system extensions.