Chapter 5: Behavioral Patterns
Welcome to Last Minute Lecture.
This free chapter overview is designed to help students review and understand key concepts.
These summaries supplement, not replace the original textbook and may not be redistributed or resold.
For complete coverage, always consult the official text.
Welcome to the Deep Dive.
Today we're taking a shortcut right to the architectural heart of well -designed software systems, behavioral patterns.
We're plunging deep into that dynamic, almost invisible logic that dictates how objects communicate and share responsibilities and, you know, how they avoid collapsing into just monolithic code.
That's exactly right.
You know, when we look at behavioral patterns, we are making a really fundamental shift.
We're moving away from just defining what an object is.
Right, like with creational or structural patterns.
Exactly, and we're focusing entirely on how it acts all about the algorithms, the assignment of responsibilities between objects.
We're really just characterizing complex control flow that often happens at runtime.
Okay, so let's unpack this territory because the chapter provides a crucial map.
We see two distinct core families here.
We do.
First, you have the behavioral class patterns.
These distribute behavior, but they rely really heavily on inheritance, I think template method and interpreter.
Right.
And then you have the much larger, and I'd argue more flexible group, behavioral object patterns, which use composition.
Yes, they rely on composition.
So we assemble objects to define relationships and we can change behavior at runtime just by swapping a pointer.
That is the critical difference.
That's immediate.
Immediate, composition lets us adapt, whereas inheritance kind of locks you into a fixed behavior hierarchy.
The second group, that's where the real flexibility in a system comes from.
That runtime adaptability is so key.
Our mission today is really to distill the core value of all these essential behavioral patterns.
You know, the ones that achieve loose coupling and make your systems grow
without constant agonizing refactoring.
Yeah.
Let's start with a set of patterns that are all about managing communication implicitly, decoupling the sender from the receiver.
Okay, so first up is the chain of responsibility or core.
Its goal is actually elegantly simple.
It's to avoid coupling the sender of a request to its specific receiver.
You do that by allowing multiple objects a chance to handle it.
The classic motivation here is that context -sensitive help in a GUI.
Great example.
Imagine you've built this dense application interface.
You hit the help button on a specific widget,
maybe an input field.
Okay.
That input field initiates the help request that have no idea where the help info is stored.
Is it on the field itself?
Is it on the dialog box containing it or maybe the main application window?
The receiver is not known in advance.
It's implicitly found.
And that is the core mechanism.
The request object gets passed along a chain from one object to the next until someone in that chain decides to handle it.
The main consequence is just incredible reduced coupling.
Because each object only knows its successor.
It only maintains a single reference to its successor.
It simplifies everything.
And this is a crucial warning for anyone implementing this.
Since the receiver is implicit, there is absolutely no guarantee of receipt.
It can just fall off the end.
The request can just fall right off the end of the chain if nobody handles it.
Gone.
So if chain of responsibility is about spreading that communication load out linearly,
the next pattern, mediator, does the complete opposite.
It centralizes it.
It really does.
Mediator defines a separate dedicated object.
Its whole job is to encapsulate how a group of colleague objects interact.
And without it, they'd all be talking to each other.
Oh, you'd get a spider web of dependencies.
Just classic spaghetti code.
The mediator steps in as the central hub of all communication.
So think about a complex font selection dialog box.
Okay, yeah.
You've got a list box of fonts, an entry field for typing the size, okay, and cancel buttons.
If selecting a font has to update the size field,
and changing the size field might enable or disable the okay button,
all those widgets now need references to each other.
It gets complex fast.
The mediator, let's call it the font dialog director.
It solves this.
The colleagues, the widgets, they only know the director.
So they just report up?
They report up.
When a colleague changes, it just notifies the director.
The director then routes that information to whoever else needs to know.
It successfully decouples the colleagues.
But wait a second.
There's always a but.
Isn't that just replacing spaghetti code with one massive, unmaintainable object?
That's a great challenge.
I mean, if the interaction logic gets too complex, the mediator object itself can easily become the monolith you were trying to avoid.
You've traded dozens of small dependencies for one huge centralized dependency that might be way harder to test and maintain.
It's a fundamental trade -off.
Which leads us nicely to the third pattern in this group, the very famous observer pattern.
Yes, publish -subscribe.
Often known as publish -subscribe.
Observer defines a really reliable one -to -many dependency.
The whole idea is that when one object, the subject, changes its state, all of its dependence, the observers are automatically notified.
The canonical example here has to be the model -view relationship in MVC.
Say you have a subject that holds data, some complex financial model.
That subject needs to simultaneously notify two different views.
Maybe a graphical chart and a raw text table whenever the underlying numbers change.
And the mechanism is pretty straightforward.
The subject just keeps a list of its observers and when its state changes, it just iterates through that list and calls an update method on each one.
The crucial technical detail, though, is about data retrieval.
So the subject notifies the observers.
One now.
And then the observers often have to query the subject to get the new state.
This is called the pull model.
As opposed to the push model.
The alternative, the push model, forces the subject to send all the data it thinks the observers might need.
We usually favor the pull model because it achieves this wonderful abstract coupling.
The subject only knows it has generic observers, not what specific data they need.
They're more reusable.
But the liability.
The liability is the potential for unexpected updates.
You can get these cascading changes, especially if observers are heavily dependent on each other.
One update triggers another, which triggers another.
Okay, so we spent time on how objects talk to each other.
Now let's look at the patterns that treat behavior itself like.
Like a transferable asset.
Encapsulating actions and algorithms and states into objects we can pass around.
Starting with the command pattern.
The intent here is, well, it's revolutionary.
It's to encapsulate a request as an object.
And what's a request as an object?
You can treat it like any piece of data.
You can pass it as a parameter, store it, queue it up for later, anything.
So if I'm building a massive UI system with, I don't know, dozens of menus,
what's the immediate headache the command solves for me?
Well, the toolkit needs to issue requests, paste, open, save.
But the menu system itself shouldn't have to know the exact operation or who the ultimate receiver is.
Could be a document object, could be a selection object.
Right, command decouples the invoker, the menu item from the receiver, the object that's actually doing the work.
So the concrete command is like the binding agent.
It holds a reference to the receiver and it packages up the specific action through a simple execute interface.
Exactly, and the value here is just profound.
First, it is indispensable for under -ado support.
Yes.
Because the command object is stored in a history list, it can hold all the state it needs to support an un -execute operation.
Second, you can combine these commands into complex sequences, a macro command.
You can define transactional operations.
Very powerful.
Next, we turn to strategy, which is just the absolute goal standard for swapping algorithms.
It really is.
Strategy defines a whole family of algorithms and capsulates each one into its own class and makes sure they are fully interchangeable.
This lets the algorithm vary completely independently from the client or context that uses it.
A classic example would be a composition class, right?
Maybe it's laying out text.
It might need different line -breaking algorithms.
A basic, fast, simple compositor, a complex text compositor that optimizes the whole page, or an array compositor for tables.
And instead of cramming all three algorithms inside the composition class, it just delegates the task to a strategy object.
This is why strategy is such a powerful alternative to subclassing.
It is, and the primary consequence is the elimination of those massive, ugly, complex conditional statements.
The giant switch statement.
You are replacing a huge switch block, maybe dozens of lines long, with a single line of delegation to whatever strategy is currently configured.
So we're replacing all that logic with one delegation call.
That's the real win.
It simplifies the context and keeps the algorithms clean and separate.
The only minor drawback is that the client has to be aware of the different strategies available to pick the right one.
The final pattern in this group is state.
This one lets an object fundamentally change its behavior when its internal state changes, so much so that it appears to change its class entirely.
Think about a TCP connection object.
If it gets an open request, the result is completely different depending on whether the connection is currently closed, listening, or already established.
And if we did that with simple conditionals, the TCP connection methods would be just riddled with nested if state closed checks.
It would be so brittle.
The state pattern fixes this.
It delegates the state -specific requests to a dedicated concrete state subclass, like TCP closed or TCP established.
This structure localizes all the state -specific behavior and, more importantly, it makes the state transitions explicit.
The logic for moving between states is now decentralized into the state objects themselves.
That's clean.
It's very clean.
Plus, if those state objects don't hold any instance variables, they can be shared, which gives us a nice little link to the flyweight pattern.
You know, if I'm honest, command, strategy, and state all share this powerful theme.
They treat actions, algorithms, and complex states as transferable objects.
We've turned behavior into a first -class currency.
That's a perfect summary.
Now let's move into two patterns that are more focused on defining structure, specifically for traversal and for interpretation.
First up is the iterator pattern.
The intent is to provide a standardized way to access elements of some aggregate object, a list, a tree, whatever, without exposing the underlying data structure.
The motivation is so clear.
You want to traverse that list in multiple ways, forward, backward, maybe filtered, but you definitely don't want to bloat the list's own public interface with 10 different traversal methods.
Right.
So the iterator defines a common interface.
You have first, next is done, current item.
By abstracting this, we get what's called polymorphic iteration.
Yes.
The client just requests an iterator from an abstract list using a factory method.
This completely decouples the client from the specific aggregate implementation, and we typically use an external iterator where the client controls when to call next.
Because it gives the client more control.
Way more flexible control over the traversal logic.
Okay, the final structural pattern we'll discuss is the interpreter, which is all about formalizing a simple language.
The interpreter pattern is designed to represent the grammar rules of a simple language think regular expressions or say complex search queries using a class hierarchy.
And then it provides an interpreter that uses that representation to interpret sentences.
So if a problem occurs frequently enough, like pattern matching in text, it's actually worth defining the problem itself as a sentence in a simple language.
Exactly.
The implementation builds an abstract syntax tree, an AST, using classes like abstract expression, terminal expression, and so on.
The interpret operation then just recursively traverses this tree.
It's elegant because it's easy to change the grammar by just adding new classes.
But there's a massive caveat.
A massive one.
This only scales for simple grammars.
If the language is complex, you need a specialized tool, a parser generator.
Otherwise that class hierarchy becomes completely unmanageable.
Right.
That brings us to our last pattern, which is focused on state preservation,
the slightly magical Memento.
Memento's intent is to capture and externalize an object's internal state without violating its encapsulation, allowing the object to be restored later.
It's critical for any kind of checkpoint or undo mechanism.
This solves a really deep problem, right?
Sometimes to restore an object accurately, you need to know its deep internal state, but you can't just make all that public.
Exactly.
A complex constraint solver in a drawing editor, for instance, it needs to save way more than just coordinates to accurately undo a move.
So how does Memento manage that?
It defines three roles.
The originator, which is the object whose state is saved, the caretaker, the undo mechanism,
and the Memento itself, the snapshot.
Okay.
The unique defining feature here is that the Memento has two distinct interfaces,
a narrow opaque interface for the caretaker, so it can just hold this token safely, and a wide interface that is accessible only by the originator.
That way, the complex internal state remains perfectly encapsulated.
That wraps up the individual patterns.
So now let's synthesize this.
What do all these behavioral patterns really tell us about system design?
Well, they highlight the value of objectifying variation.
I mean, notice how strategy, state, and iterator all take some variable piece of functionality, an algorithm, a behavior, a traversal method, and they encapsulate it into its own dedicated interchangeable object.
And the idea of treating objects as tokens or arguments is so powerful.
Absolutely.
Look at command and Memento.
The objects are just magic tokens representing requests or safe states that you pass around.
Even the visitor pattern, where the operation itself is passed as an object to the accept call.
The client handles the token, not the complex internal logic.
Let's quickly circle back to that rivalry.
Mediator versus observer.
They're both aiming for loose coupling, but the results are so different.
They're competitive solutions for sure.
Observer distributes communication.
This makes your components highly reusable, but tracking the flow of communication can get complicated, especially with multiple interdependencies.
Whereas mediator centralizes it, mediator centralizes control.
This makes the flow really easy to analyze and debug, but you run that risk of creating that monolithic director object we talked about.
Your choice really depends on what you prioritize,
component reusability or highly traceable communication.
And finally, we have to mention the concept inherent in template method, which really defines control flow, the Hollywood principle.
Don't call us, we'll call you.
The parent class defines the invariant, the skeleton of the algorithm.
It dictates the entire flow, and it calls abstract operations that the subclasses are responsible for filling in.
It completely flips the control structure.
It guarantees the overall structure is enforced while still allowing for specialization.
So the ultimate goal of mastering these behavioral patterns is pretty clear.
It's achieving high flexibility, loose coupling.
By turning these complex states or operations or protocols into manageable reusable objects.
And we've seen how they provide all these unique mechanisms for decoupling the sender from the receiver.
And I wanna leave you with a final thought on the interpreter pattern.
We often think of it as something you only need when you're designing a formal language.
But what if we apply that concept to any complex hierarchy, not just code, but maybe a defined business process.
If we define the rules for traversing and operating on that structure, we might be building an interpreter without even naming it that.
You mean that the power of the pattern isn't just about the complexity of the code, but whether we decide the structure we're operating on counts as a grammar in the first place.
Precisely.
It just goes to show that sometimes the application of a pattern is less about the technical implementation and more about the perspective you take on the underlying problem.
A powerful concept to carry forward into your next design decision.
We hope this deep dive encourages you to not just recognize these patterns,
but to intentionally apply them to build more resilient and adaptable software.
Thank you for joining us.
ⓘ This audio and summary are simplified educational interpretations and are not a substitute for the original text.
Using this chapter to study? Last Minute Lecture is free and student-run. If it helped, consider supporting the project.
Support LML ♥Related Chapters
- IntroductionDesign Patterns: Elements of Reusable Object-Oriented Software
- Structural PatternsDesign Patterns: Elements of Reusable Object-Oriented Software
- A Case Study: Designing a Document EditorDesign Patterns: Elements of Reusable Object-Oriented Software
- About Crystal Structures and Diffraction PatternsStructure of Materials: An Introduction to Crystallography, Diffraction and Symmetry
- Behavioral Genetics: From Variance to DNAThe Cambridge Handbook of Personality Psychology
- Behavioral Neuroscience Scope & OutlookBehavioral Neuroscience