| Lesson 3 | How do Behavioral Patterns help programmers? |
| Objective | Test the Vehicle classes. |
Behavioral design patterns describe proven ways for objects and classes to communicate, share responsibilities, select algorithms, and respond to changes. They focus less on how objects are constructed and more on what happens after those objects begin working together. In a small program, direct method calls and conditional statements may be sufficient. As a system grows, however, those direct connections can create a network of dependencies that is difficult to understand, test, and modify.
The Gang of Four behavioral patterns give programmers a vocabulary for organizing these interactions. Instead of inventing a new communication mechanism for every application, a developer can start with an established arrangement of participants and responsibilities. Saying that a design uses Observer, Strategy, Command, State, or Mediator communicates both structure and intent. Other developers can then recognize which object initiates an action, which object performs it, and where variable behavior is expected.
Although each behavioral pattern addresses a different design problem, the patterns provide several related benefits. The following six benefits are especially important when a group of objects must cooperate without becoming tightly dependent on one another.
Modern Java provides language features and library interfaces that can make behavioral patterns more concise, but the underlying design decisions remain important. A lambda expression, for example, can implement a small Strategy or Command when the role contains one abstract method. The lambda removes ceremonial class syntax, but the program still benefits from separating a variable algorithm or action from the client that uses
it. Standard functional interfaces such as Function, Predicate, Consumer, and Supplier can represent common strategy and command shapes when their names communicate the responsibility clearly.
The java.util.concurrent.Flow interfaces provide a standard model for publishers, subscribers, subscriptions, and back pressure. This is a modern Java example of observer-style decoupling for asynchronous streams. It is more specialized than the basic GoF Observer pattern
because it defines how demand is signaled and how stream elements are delivered. A program should therefore use Flow when reactive stream semantics are required, not simply because two objects need a notification relationship.
Sealed classes and interfaces can define a closed family of valid states, while records can represent concise, immutable state or command data. Interface default methods can extend shared behavior at a stable pattern boundary. These features improve the representation, but they do not decide where behavior belongs. The designer must still give each participant a clear role and keep transition rules cohesive.
Behavioral design patterns and UML behavior diagrams describe related aspects of a system, but they are not the same thing. A behavioral pattern is a reusable arrangement of software responsibilities. A behavior diagram is a modeling view that shows how a system acts over time or how participants cooperate in a particular scenario. Diagrams can help programmers verify that an implementation follows the intended pattern.
UML provides several useful ways to examine dynamic behavior:
The objective of this lesson is to test the Vehicle classes created for the course project. Testing these classes involves more than confirming that individual accessor methods return stored values. The Vehicle objects participate in a behavioral system. They respond to traffic-light state, cooperate with queues and coordinating objects, and perform actions whose correctness depends on the order in which messages are delivered. A useful test must therefore examine both the state of one Vehicle and its interactions with the surrounding participants.
Begin by using the course UML class diagram to identify each Vehicle responsibility and each relationship that can affect behavior. The Vehicle hierarchy shows which operations are common to all vehicles and which operations are supplied by specialized subclasses. The test structure should then map each public behavior to a clear precondition, action, and expected result. This approach prevents the test from merely duplicating the implementation and helps it express the contract that every valid Vehicle must satisfy.
Each test should construct the smallest object network needed for the behavior under examination. Create the Vehicle with known values, supply any required observer or mediator collaborator, and place the traffic light or queue in a known state. Avoid sharing mutable fixtures between tests unless the fixture is reset reliably. A controlled initial state makes failures reproducible and distinguishes a defect in the Vehicle from leftover state created by an earlier test.
Test subclass behavior through the common Vehicle interface when the contract is intended to be polymorphic. This verifies that client code can use different Vehicle implementations without depending on their concrete types. Add subclass-specific tests only for behavior that genuinely extends the shared contract. The result should demonstrate substitutability rather than simply increasing the number of assertions.
In the Vehicle scenario, Observer-style communication allows interested objects to react when relevant traffic or Vehicle state changes. A test should register a controlled observer, trigger one specific change, and verify that the observer receives the expected notification. The test should also verify important boundary conditions: an unchanged value should not produce a duplicate event if the contract forbids it, a removed observer should no longer receive events, and multiple observers should each receive the appropriate update.
A test observer or recording observer is often clearer than a production collaborator. It can store the number of notifications, their order, and the values delivered with each event. Assertions can then describe the externally visible protocol without inspecting private collections inside the Vehicle. This preserves encapsulation and keeps the test valid if the internal observer storage changes.
Mediator-style coordination keeps Vehicle queues and the traffic light from communicating through a large set of direct dependencies. To test this design, replace the real mediator with a controlled test implementation when practical. Trigger a Vehicle action and verify that the request is sent to the mediator with the correct Vehicle and state information. A separate mediator test can then verify how that request affects queues, signals, or other Vehicles.
Dividing the tests in this way localizes failures. If a Vehicle sends the correct request but the system produces the wrong queue transition, the mediator test identifies the coordination problem. If the mediator is never contacted, the Vehicle test identifies the missing interaction. Testing through responsibilities also protects the architecture by discouraging new direct links between colleagues that should remain decoupled.
Vehicles may behave differently when a signal is red, yellow, or green, or when their position in a queue changes. Construct a test for each valid transition and for any event that should leave the state unchanged. Verify both the resulting state and the observable action. If the implementation uses separate State objects, the test should focus on behavior rather than asserting a private concrete state class unless that type is part of the public contract.
If movement, routing, or queue selection is represented by a Strategy, supply deterministic strategies to the Vehicle during testing. One strategy can return a known choice, while another returns a different choice. The test can then prove that the Vehicle delegates the decision and honors the result without embedding the algorithm itself. This confirms the reason for using Strategy: the algorithm can vary independently of the Vehicle client.
Where Vehicle operations are represented as commands or routed through handlers, verify the request contents and execution order. Tests should cover an accepted request, a request that cannot yet be handled, and any retry, queue, or cancellation behavior defined by the contract. If undo is supported, confirm that executing and then undoing a command restores the relevant prior state without altering unrelated objects.
Well-structured tests provide feedback about the design as well as the implementation. If testing one Vehicle requires constructing most of the application, the Vehicle probably depends on too many concrete collaborators. If a test must inspect private fields to determine whether an action succeeded, the public contract may not express the behavior clearly. If every new Vehicle type requires edits to the same large conditional block, the intended behavioral variation may not be properly encapsulated.
The most valuable tests describe observable responsibilities in course terminology. A reader should be able to see that a Vehicle reacts to a traffic-light change, notifies registered observers, delegates coordination to the mediator, and selects variable behavior through the intended strategy or state role. These tests document how the pattern participants cooperate and give future programmers confidence that one participant can be replaced without silently breaking the protocol.
Behavioral patterns help programmers solve problems because they identify where interaction rules belong. The Vehicle exercise applies that idea directly. Instead of testing a collection of isolated methods, it tests a small collaboration whose responsibilities have been deliberately separated. When those responsibilities can be verified independently and then as a coordinated scenario, the pattern has achieved its practical purpose: clearer intent, safer variation, and maintainable object interaction.
In this exercise, you will run tests on the Vehicle classes you have already created. Use the diagrams and the behavioral responsibilities described in this lesson to identify the expected state changes, notifications, and mediated interactions.
Testing Interface - Exercise