| Lesson 8 | Creational Classes - Course Project |
| Objective | Write Creational Classes for the Course Project |
Write Creational Classes for the Course Project
This lesson represents the culmination of Module 4's exploration of creational design patterns. You've studied the theory behind Factory Method, Abstract Factory, Builder, Prototype, and Singleton patterns. Now it's time to apply that knowledge by implementing a concrete creational class for your traffic simulation project.
The goal isn't to force patterns into every corner of your codebase, but rather to recognize where creational patterns solve real problems: decoupling client code from concrete implementations, centralizing object construction logic, improving testability, and creating a foundation that can evolve without cascading changes throughout the system.
The Current State of Your Project
By this point in the module, your traffic simulation should have a well-structured creational layer in place:
- An abstract base class
Vehicle that defines the interface and shared behavior for all vehicles—including properties like length, speed, position, and methods for bounds checking and movement.
- Concrete vehicle subclasses such as
Car, Bus, Bicycle, and Pedestrian, each with appropriate constructor parameters that define their specific characteristics (e.g., a bus is longer and slower than a car).
- An abstract factory class
VehicleFactory that stores probability weights for each vehicle type and declares the abstract method createVehicle() that subclasses must implement.
What remains is the core implementation work: creating one or more
concrete factory classes that implement the
createVehicle() method to actually instantiate and return vehicle objects based on the configured probabilities.
Implementing the Concrete Factory Class
Your primary task is to write a concrete implementation of the abstract
VehicleFactory class. This concrete factory will serve as the single point of vehicle creation for your simulation. Here's what the implementation needs to accomplish:
Core Responsibilities
- Probability-based selection: Use the factory's probability fields (
chanceCar, chanceBus, chanceBicycle, chancePedestrian) to determine which vehicle type to create.
- Random number generation: Generate a random value and map it to a vehicle type based on the cumulative probability distribution.
- Object instantiation: Construct the appropriate concrete
Vehicle subclass with valid initialization parameters.
- Type abstraction: Return the result as the abstract type
Vehicle so that client code remains decoupled from concrete implementations.
Example Usage Pattern
From the client's perspective, vehicle creation becomes straightforward and uniform:
VehicleFactory factory = new TrafficSimulationFactory();
Vehicle v = factory.createVehicle();
// Client code never needs to know if v is a Car, Bus, or Bicycle
v.move();
v.checkBounds();
This separation of concerns is the essence of the Factory Method pattern—clients depend on abstractions, not concrete classes.
Why This Qualifies as a Creational Pattern
You might think, "Isn't this just random selection with extra steps?" Not quite. This is a creational pattern because it fundamentally restructures how objects come into existence in your system.
Key Characteristics of Creational Design
- Centralized construction logic: All the rules about how vehicles are created—probability distributions, valid parameter ranges, initialization sequences—live in one dedicated component rather than being scattered throughout client code.
- Dependency inversion: Client code depends on the
Vehicle abstraction and the VehicleFactory interface, not on concrete classes like Car or Bus. This inverts the typical dependency structure.
- Controlled instantiation: The factory enforces invariants and ensures that all vehicles are created in a valid state. Clients can't accidentally create a vehicle with invalid parameters.
- Extensibility point: When you need to add a new vehicle type (like
Motorcycle or Truck), most changes happen within the factory hierarchy rather than rippling through client code.
Trade-offs to Consider
Like all patterns, the Factory Method comes with trade-offs:
- Indirection: There's an extra layer between client code and object creation, which adds some cognitive overhead when reading the code.
- Centralized complexity: As the factory grows to handle more vehicle types and more complex creation rules, it can become a maintenance bottleneck if not carefully managed.
- Potential over-engineering: For very simple systems that don't need to evolve, direct instantiation with
new might be simpler and more appropriate.
The key is recognizing when the benefits outweigh the costs—and for a simulation that will grow in complexity, the factory pattern pays dividends.
Implementation Considerations and Best Practices
While the Gang of Four patterns originated in the 1990s, modern software development has refined how we implement them. Keep these contemporary practices in mind:
Dependency Injection and Testability
Design your system so that components receive their factory instance through constructor injection or method parameters rather than creating it themselves:
public class TrafficSimulation {
private final VehicleFactory factory;
public TrafficSimulation(VehicleFactory factory) {
this.factory = factory; // Injected dependency
}
public void spawnVehicle() {
Vehicle v = factory.createVehicle();
// ...
}
}
This makes testing trivial—you can inject a mock factory that returns predetermined vehicles, eliminating randomness from your tests.
Configuration Over Hard-Coding
Rather than hard-coding probability values in your factory implementation, consider loading them from external configuration:
- Configuration files (JSON, YAML, properties files)
- Environment variables for deployment-specific tuning
- Database records for runtime adjustability
- Builder pattern to construct factories with custom probabilities
This separation allows you to tune simulation behavior without recompiling code.
Memory Management and Object Lifecycle
Different languages handle object lifecycle differently:
- Java/Python: Garbage collection handles cleanup automatically. Focus on not holding unnecessary references.
- C++: Prefer returning smart pointers (
std::unique_ptr<Vehicle> or std::shared_ptr<Vehicle>) rather than raw pointers to avoid memory leaks.
- Rust: Ownership semantics enforce memory safety at compile time. Return owned values or use appropriate smart pointer types.
Error Handling and Validation
Your factory should handle edge cases gracefully:
- What happens if probability values don't sum to 1.0?
- How do you handle negative or invalid probabilities?
- Should the factory throw exceptions, return null, or use an Optional/Result type?
Document these decisions and implement consistent error handling across your factory hierarchy.
Connecting to Other Design Patterns
The course project deliberately creates touch points with other patterns you'll study in later modules. Understanding these connections helps you see the bigger architectural picture.
Structural Patterns: Flyweight
As your simulation scales, you might create thousands of vehicles. The
Flyweight pattern addresses this by sharing common state among many objects. In Module 5, you'll create a
FlyweightVehicleFactory that reuses vehicle instances or shares immutable data (like sprites or textures) across multiple vehicles. The factory becomes responsible not just for creation, but for managing a pool of reusable objects.
Behavioral Patterns: Strategy
The probability-based selection logic in your factory could become quite complex—different rules for different times of day, weather conditions, or traffic patterns. The
Strategy pattern lets you extract this selection logic into interchangeable strategy objects:
interface VehicleSelectionStrategy {
VehicleType selectVehicleType(Random rng, double[] probabilities);
}
class WeightedRandomStrategy implements VehicleSelectionStrategy { /* ... */ }
class RushHourStrategy implements VehicleSelectionStrategy { /* ... */ }
Your factory can then accept a strategy and delegate the selection decision to it, making the system more flexible.
Behavioral Patterns: Observer
You might want to observe and log what your factory creates—for debugging, analytics, or performance monitoring. The
Observer pattern would let interested parties subscribe to creation events without the factory needing to know who's listening.
Creational Patterns: Object Pool
For high-performance simulations, constantly creating and destroying vehicle objects creates pressure on the garbage collector. An
Object Pool works with your factory to recycle objects. Instead of creating a new vehicle each time, the factory checks out an available instance from the pool, resets it, and returns it. When the simulation is done with the vehicle, it goes back to the pool instead of being discarded.
Architectural Patterns: Abstract Factory
Your current factory creates individual vehicles. What if you need to create entire "families" of related objects—vehicles, traffic lights, road segments—that must be stylistically consistent? The
Abstract Factory pattern provides a higher-level interface for creating families of related objects. You might have a
SimulationComponentFactory that creates both vehicles and infrastructure elements that match a particular theme (realistic vs. arcade-style).
Testing Your Creational Classes
Creational patterns shine when it comes to testing. Here's how to verify your factory implementation:
Unit Tests for the Factory
- Probability distribution tests: Create thousands of vehicles and verify that the distribution matches expected probabilities within statistical tolerance.
- Type verification: Ensure that
createVehicle() always returns valid Vehicle instances of the expected concrete types.
- Initialization verification: Check that created vehicles have valid property values (positive speeds, reasonable lengths, etc.).
- Edge case handling: Test with invalid probability configurations to ensure proper error handling.
Integration Tests for Client Code
- Decoupling verification: Write client code that uses only the
Vehicle interface and verify it works with all concrete types.
- Factory substitution: Replace the production factory with a test factory that returns predetermined sequences of vehicles, eliminating randomness from higher-level tests.
- Configuration testing: If using external configuration, test that different configurations produce different vehicle distributions.
Quick Reference: Creational Patterns Summary
As you implement your factory, it helps to remember where Factory Method fits among the creational patterns:
- Factory Method: Define an interface for creating objects, but let subclasses decide which class to instantiate. Clients depend on abstractions and override creation in subclasses. (Your current focus)
- Abstract Factory: Provide an interface for creating families of related or dependent objects without specifying their concrete classes. Ensures created objects work together.
- Builder: Separate the construction of a complex object from its representation. Build objects step-by-step, often with fluent interfaces, handling optional parameters elegantly.
- Prototype: Specify the kinds of objects to create using a prototypical instance, and create new objects by copying this prototype. Useful when instantiation is expensive or complex.
- Singleton: Ensure a class has only one instance and provide a global point of access to it. Use sparingly—modern practice favors dependency injection for managing shared instances.
Each pattern addresses different creation concerns. Factory Method excels at decoupling clients from concrete types while allowing for polymorphic creation behavior.
Looking Ahead
After completing this lesson, you'll have a working factory that creates vehicles for your simulation. In the next module, you'll revisit this factory when studying structural patterns, particularly Flyweight, to optimize memory usage as your simulation scales. You'll see how the factory can coordinate object reuse and caching strategies.
The patterns aren't isolated techniques—they form a toolkit where patterns complement and enhance each other. Your factory becomes the foundation for more sophisticated object management strategies as your project evolves.
Course Project - Exercise
