Structural patterns make relationships between software components explicit. This module began with a broad question: how can classes and objects form a larger unit while keeping change contained? It answered that question through the seven Gang of Four structural patterns, then examined Flyweight in depth and applied its state model to the traffic-intersection course project. The main outcome is not a collection of class diagrams. It is a repeatable way to identify a structural pressure, choose a narrow collaboration, and verify its consequences.
A structural design is useful when a client can depend on a focused contract instead of a vendor API, object graph, storage engine, or subsystem sequence. That dependency direction contains change. Adapter translates an existing interface into the contract a client needs. Facade presents cohesive subsystem operations. Proxy preserves an interface while controlling access, location, creation, or caching. Decorator preserves a component contract while layering one independent responsibility.
The contract includes more than method signatures. Ordering, exceptions, thread safety, identity, ownership, cleanup, transactions, and remote failure semantics may all be observable. Contract tests should run against each substitutable implementation. A wrapper that compiles but changes those promises is not structurally correct.
The module distinguished is-a inheritance from has-a composition. Inheritance is appropriate for a genuine stable type relationship. Composition and delegation are preferable when a responsibility, policy, or implementation must be replaceable. A containing object accepts a request, performs boundary-specific work, and delegates the core operation to a collaborator supplied explicitly.
Generics, records, lambdas, dependency injection, C++ templates, and smart pointers reduce implementation ceremony. They do not choose the correct boundary. Programmers must still decide who owns creation, what can vary, which state may be shared, and which failures cross the collaboration. A simple function or direct composition remains better when a named pattern would add indirection without containing meaningful change.
Several patterns contain forwarding objects, so intent is the reliable selection guide. Adapter changes an interface; Decorator adds responsibility; Proxy controls access; Facade represents a subsystem. Patterns may collaborate, but each boundary should have its own reason, tests, and lifecycle. The goal is clarity, not the largest possible pattern count.
Flyweight applies when a very large number of logical objects duplicate a much smaller set of values, and profiling shows that duplication creates a material resource cost. The pattern divides state into immutable intrinsic data and context-specific extrinsic data. A canonical factory returns one shared flyweight for each normalized key, while clients retain the state that varies per occurrence.
The cost model must include object headers, alignment, references, map entries, key construction, lookup, synchronization, and client context. Measure retained heap, allocation rate, garbage collection, throughput, and latency under representative key distributions. If object count is modest, most state is unique, or a compact value object already meets the constraint, Flyweight is unnecessary.
Intrinsic state is independent of one use and safe for simultaneous sharing. Extrinsic state belongs to a particular occurrence and is supplied when behavior runs. In the course project, a vehicle description can contain type, dimensions, and stable configuration. A vehicle event contains identity, direction, lane, arrival time, queue position, and current movement.
Deep immutability protects the shared boundary. Java final fields must not expose mutable collections; C++ interfaces need const correctness and explicit ownership. If one client can change a flyweight and affect another, the state split is wrong. If nearly every operation requires a massive context object, too much state may have been externalized. The separation should improve both memory use and the clarity of the domain.
The factory normalizes a value key, checks the catalog, and creates an immutable flyweight only on a miss. A key that omits a meaningful property returns the wrong value. A key that includes occurrence-specific data creates one entry per logical object and defeats sharing. Equality and hashing therefore deserve direct tests.
Factory scope establishes ownership and cleanup. A catalog owned by one simulation run is isolated and deterministic. A process-wide static catalog retains objects longer and can mix tests or tenants. Unbounded input requires an eviction or rejection policy. Metrics for entries, hits, misses, creation latency, and retained memory reveal whether production behavior matches the original cardinality assumptions.
Sharing can reduce memory, allocation, and repeated preprocessing. It can also add lookup cost, contention, map overhead, lifecycle rules, and weaker encapsulation. Canonical identity is not domain occurrence identity: two queued cars may share one vehicle type and still represent different events. An evicting factory may later create an equal value at a different address. Domain code should use explicit identifiers and value equality rather than accidental reference comparisons.
Concurrency has two boundaries. Immutable flyweights support parallel reads, but the factory still needs safe creation and catalog management. Mutable extrinsic queues remain the client's responsibility. Remote caches and cross-process sharing introduce consistency, latency, authentication, and failure concerns that exceed the classic in-memory pattern.
The revised intersection model keeps one directional queue of
VehicleEvent values and refers to shared
VehicleType descriptions. Enums constrain directions, records
make event data explicit, and EnumMap with
ArrayDeque communicates the queue structure. Releasing an event
does not mutate its shared vehicle type.
Scheduling belongs to a separate policy. Simply serving the largest queue can starve a quiet direction, so a realistic controller considers waiting time, minimum green duration, pedestrian phases, clearance intervals, emergency priority, and fairness. A deterministic simulation clock and preserved event stream allow policies to be compared under identical workloads.
Tests should prove canonical reuse for equal keys, distinct values for unequal keys, independent outcomes for different extrinsic contexts, and predictable failure for invalid input. Intersection tests verify first-in, first-out release, directional isolation, fairness, event uniqueness, and repeatable results. Performance tests compare the Flyweight version with a straightforward baseline at production-relevant scale.
Document the design pressure, rejected alternatives, ownership, expected cardinality, and threshold for success. Revisit the decision after runtime upgrades or workload changes. If the measured benefit disappears, remove the optimization. A pattern should remain reversible because the domain contract, not the catalog map, owns the application's meaning.
You should now be able to define structural patterns, distinguish the intent of all seven Gang of Four patterns, choose composition or inheritance deliberately, and evaluate Flyweight with quantitative evidence. You should also be able to divide intrinsic and extrinsic state, design a canonical factory, preserve domain identity, and integrate shared vehicle descriptions without coupling the traffic controller to an optimization.
The lasting lesson is that good structure makes change, ownership, and behavior visible. Pattern vocabulary starts the conversation; precise contracts, measurements, tests, and operational signals complete the design.
Classic patterns describe collaborations inside an object system, while today's applications also cross process, container, and cloud boundaries. An API gateway may resemble a Facade, a generated service client may resemble a Proxy, and an integration component may act as an Adapter. The resemblance is useful vocabulary, but it does not erase distributed-systems behavior. Remote operations can time out, be retried, complete more than once, or succeed after the caller has stopped waiting.
For every remote structural boundary, document latency budgets, authentication, authorization, idempotency, rate limits, tracing, and failure translation. A retrying Decorator must know whether the operation is safe to repeat. A caching Proxy must define freshness and tenant isolation. A Facade that coordinates several services must address partial completion. These operational promises join the object contract rather than replacing it.
Functional programming and data-oriented design also offer alternatives. Higher-order functions can express a small decorator without a new class. Immutable values and transformation pipelines may be clearer than a mutable object graph. Column-oriented arrays may outperform Flyweight for a large simulation. Pattern literacy includes recognizing when modern language or data features solve the pressure more directly.
This checklist keeps pattern use evidence-based. It prevents a familiar name from substituting for design reasoning and gives future maintainers a clear standard for preserving or simplifying the structure.