Structural Patterns «Prev Next»

Lesson 10

Structural Pattern Conclusion

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.

Structure Begins With a Client Contract

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.

Composition Makes Variation Explicit

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.

The Seven Patterns Solve Different Pressures

  • Adapter converts an incompatible interface while preserving meaning.
  • Bridge separates two dimensions that need to evolve independently.
  • Composite lets clients treat a leaf and a part-whole hierarchy uniformly.
  • Decorator composes optional responsibilities without subclass combinations.
  • Facade gives common clients a focused subsystem entry point.
  • Flyweight shares repeated immutable intrinsic state at large scale.
  • Proxy represents another object and controls how it is reached or used.

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 Requires a Measured Motivation

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 and Extrinsic State Define Correctness

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.

Factory Design Determines Whether Sharing Works

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.

Consequences Must Be Evaluated Together

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 Intersection Project Preserves Those Boundaries

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.

Verification Turns a Pattern Into Engineering

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.

Module Outcome

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.

Modern Architecture Extends the Classic Context

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.

A Final Structural Decision Checklist

  1. State the client problem and the behavior that must remain stable.
  2. Identify the dependency, responsibility, hierarchy, or repeated state that creates pressure.
  3. Compare the named pattern with direct composition, a function, a compact value, or a data-oriented alternative.
  4. Define contracts, ownership, lifecycle, equality, errors, and concurrency before implementation.
  5. Build the smallest collaboration that contains the identified change.
  6. Test substitution and boundary-specific behavior, including failure and cleanup.
  7. Measure performance claims with a representative workload and baseline.
  8. Add operational signals that reveal latency, cardinality, cache behavior, or resource growth.
  9. Record the decision and the condition under which it should be revisited or removed.

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.