Chapter 4: Structural Patterns
Welcome to Last Minute Lecture.
This free chapter overview is designed to help students review and understand key concepts.
These summaries supplement not replaced the original textbook and may not be redistributed or resold.
For complete coverage, always consult the official text.
Welcome back to the Deep Dive.
Today we are moving past the initial stages of object creation and we're really focusing on architecture.
Our mission is a deep dive into the structural foundations of object -oriented design, specifically the blueprints that govern how we put objects and classes together to form, well, large cohesive and maintainable systems.
That's exactly right.
If creational patterns were all about making the building blocks,
structural patterns are about the engineering discipline of assembling them.
Right.
We're looking for efficient ways to define relationships,
handle complexity, and just ensure flexibility without inheriting these really rigid structures.
And the sources, they immediately point out a critical distinction in how that structure is actually achieved.
It defines two core groups.
It does.
First, we have structural class patterns.
There are very few of these really.
The class adapter is the most famous example.
And these patterns, they rely on inheritance.
So static composition.
Static composition, exactly.
Meaning the relationship is defined and just fixed at compile time.
And then there's the other category, which is far more flexible and much more common, the structural object patterns.
Right.
And these rely entirely on object composition or dynamic composition.
By composing objects, so one object holding a reference to another, the relationships aren't fixed.
You can change the implementation.
You can swap components or add features while the program's actually running.
That runtime flexibility is really the fundamental power boost that composition gives you over static inheritance.
Flexibility is the key word here.
So we're going to unpack seven essential patterns that provide this adapter, ridge, composite, decorator, facade, flyweight, and proxy.
Let's start with the one designed purely to deal with incompatibility.
That would have to be the adapter pattern.
It's often just called the wrapper.
Its whole intent is to convert the interface of an existing class, which we call the adaptee, into an interface that the client already expects.
It's like an architectural universal plug adapter.
That's the perfect analogy.
The motivation here is usually pretty pragmatic, right?
You have two perfectly good systems that just were never designed to speak to each other.
For example, you might have a really high -performance TextView class, maybe from some third -party toolkit, but your application, say a drawing editor, demands all components conform to your application's shape interface.
And you can't just rewrite the toolkit.
You can't.
And you can't redesign your application's core interfaces.
So what do you do?
You introduce an adapter, maybe called a text shape.
When a client calls a method on that text shape, the adapter just translates and delegates that call to the right method on the internal TextView object.
And the source material highlights that there are two distinct ways to actually implement this translation.
Exactly.
The first one is the class adapter.
This one needs multiple inheritance, which allows the adapter class to inherit the interface you want, like shape, and also inherit the implementation of the adaptee, the TextView.
But the consequence there is commitment, right?
You're locked into that one specific implementation.
You are.
And this approach, it generally doesn't work if you need to adapt a class and all its subclasses at the same time.
It's just not flexible.
That sounds really limiting, especially in modern languages like Java or C Sharp that don't even support multiple implementation inheritance.
It is, which brings us to the much preferred structure, the object adapter.
This just uses composition.
The adapter class simply holds a reference to an instance of the TextView.
And because it works with it through an object reference, it's just inherently more flexible.
A single adapter can work with the adaptee and all of its subclasses.
So composition for the win, again, on flexibility.
And the adapter isn't just a translator, is it?
It can.
It can also be an enhancer.
Absolutely.
Beyond just simple translation, the adapter can fulfill methods that the target interface requires.
But the original class just didn't support.
So back to our example, the client might expect to create manipulator operation for dragging the shape around.
Which TextView definitely doesn't have.
Right.
So the TextShape adapter just implements that operation itself.
It adds the functionality to bridge the gap completely.
That takes us neatly from fixing unforeseen incompatibility to managing anticipated variation, which I think is the domain of our next pattern, Bridge.
Yeah.
If adapter is reactive, Bridge is definitely proactive.
Its intent is to decouple an abstraction from its implementation so that both can vary totally independently.
This is the handle body idiom, and it's a fundamental strategy for achieving stability in really large systems.
The classic motivation for Bridge, it always seems to involve avoiding that class proliferation problem, right?
When you're dealing with cross -platform stuff.
Let's use the classic window abstraction example.
Sure.
It needs to run on, say, the X window system and presentation manager.
Okay.
Now, if you try to solve that with simple inheritance, you fall into a class hierarchy pit.
Fast.
You get X window and PM window.
Fine.
But the second you introduce a new type of window, let's say an icon window.
Oh, now you need icon window and PM icon window.
And then you add a third operating system, and you've just tripled your class count.
Again,
every new abstraction multiplies the number of concrete classes you need.
That sounds like a maintenance nightmare.
Just a dependency web that's impossible to compile and manage.
It is.
The Bridge pattern solves this by creating two distinct parallel hierarchies.
The first is the abstraction hierarchy, so it has your high -level objects like window and icon window.
The second is the implementer hierarchy, rooted in something like window imp, with platform -specific classes like X window imp and PM window imp.
And the connection between these two worlds, how do they talk?
It's composition, again.
The abstraction object holds a reference to an implementer object.
So when a client calls a method on the icon window, the icon window doesn't draw itself.
It just forwards the request to its internal implementer object.
I see.
And crucially, the implementer interface only declares primitive operations.
This separation kills the compile -time dependencies and lets you swap implementations at runtime.
Just change the object the abstraction is holding.
So the architectural insight here is that Bridge forces you to define this minimal, stable interface, the implementer.
While the abstraction handles all the high -level logic, it's a huge upfront investment for, I guess,
massive long -term flexibility.
Precisely.
Moving on to structure number three, let's look at organizing objects into groups, the composite pattern.
The intent here is to compose objects into tree structures to represent part -whole hierarchies.
And this lets clients treat individual objects and compositions uniformly.
It's critical any time you have containers and content that have to be handled in the exact same way.
So back in our drawing editor.
Right.
A client needs to draw a single primitive, like a line.
But that same client also needs to be able to draw a picture, which might contain hundreds of lines, rectangles, and maybe even other pictures.
And if the client has to write special code for every type, you know, if it's a line, call draw.
If it's a picture, loop over the children and call draw.
That complexity just explodes.
Composite solves this by defining a uniform interface, the component.
So for example, graphic primitives, which we call leaf objects like line, they just implement that interface directly.
And the containers.
The containers, which we call composite objects like picture, they also implement the interface.
But they store and manage references to their children, and they forward requests down the hierarchy.
So whether the client has a reference to a single line or a complex picture with thousands of objects, they just call the same draw method.
That's the transparency benefit.
It is.
And this transparency leads directly to the pattern's main trade -off, safety versus transparency.
To keep things transparent, the component interface has to include child management operations like add and remove.
But wait a minute.
If I define add on the root component interface, then a simple leaf object like a line inherits add.
So calling line .add a new rectangle, that makes no sense.
If a client does that, it's probably a bug.
That's the safety compromise.
If you prioritize safety, you'd only put add and remove on the composite class.
But then you force clients to check the type before they do anything.
Which complicates the client code.
It does.
And the source material generally leans toward transparency.
It often suggests that the leaf classes should just implement these meaningless methods to do nothing or maybe throw an exception.
It's a choice between simple client logic and runtime safety checks.
That makes a lot of sense.
So once we have our component structures sorted with composite, the next logical step is adding features to them dynamically, which brings us to the decorator pattern.
Yeah, decorator's intent is to dynamically attach additional responsibilities to an object.
It's a flexible alternative to static subclassing.
It's often used to avoid what we call the explosion of subclasses.
That explosion happens when?
It happens when you try to use inheritance to manage features.
If you have a window and you want to add a scroll bar, a title bar, and a thick border,
you end up having to create subclasses for every single permutation.
Scrollable window, border scroll window.
Titled bordered window and so on.
It gets brittle.
If I want to add a drop shadow feature, I'd suddenly need to create what?
Eight new classes just to cover all the combinations.
Decorator solves this by making the extra responsibility an object that wraps the original component.
The decorator object conforms to the component's interface.
It holds a reference to it and then it forwards requests, performing its own function either before or after forwarding.
So if I have a basic text view, I can wrap it in a scroll decorator.
When draw gets called on the decorator, the decorator handles the scrolling logic and then calls draw on the text view inside it.
And because that scroll decorator is a component, you can wrap that in a border decorator and so on.
You're building up functionality at runtime by nesting these wrappers.
It avoids those huge feature -laden classes high up in the hierarchy.
The liability though that the source material mentions is the proliferation of lots of small similar objects.
If I add three features, I now have four objects working together or I used to have one.
That sounds like a debugging headache.
It can be.
While your code base avoids the subclass explosion, your runtime object graph can become pretty complex.
It's a trade -off.
Code flexibility for runtime complexity.
And we should also distinguish this from the strategy pattern.
Decorator is about changing an object's external appearance or responsibility, sort of its skin.
Strategy is about changing the object's core interchangeable algorithm.
It's guts.
Sten versus guts.
I like that.
Excellent distinction.
Now, moving from structural enhancement to structural simplification, let's talk about facade.
Facade is all about managing complexity.
Its intent is to provide a unified high -level interface to a complex subsystem, just to make that subsystem easier for clients to use.
But why not just design a simpler subsystem to begin with?
Isn't facade just kind of lazy programming that hides a bad underlying design?
That's a common critique.
But the real value of facade isn't in hiding poor design.
It's in managing necessary complexity.
Think about a compiler subsystem.
It has to have separate components for a scanner, a parser, a code generator, an optimizer.
These are inherently complex.
Right.
But 99 % of clients just want to call compile source file.
They don't care about managing AST nodes or token streams.
Exactly.
The compiler class acts as the facade.
It knows which subsystem classes are responsible for each phase and it delegates the client's single compile request across all of them internally.
And the crucial architectural benefit here is the weak coupling it enforces.
Clients only depend on the facade, not on the dozens of internal volatile subsystem classes.
So if you change the internal optimizer class, only the facade needs to be recompiled, not every single client that uses the compiler.
This is vital for managing dependencies in massive systems.
That weak coupling benefit is the real key there.
OK.
Let's pivot to efficiency with the flyweight pattern.
The intent here is using sharing to support huge numbers of fine -grained objects efficiently, focusing on space efficiency.
This pattern is completely motivated by scale.
If you're building a document editor where every single character, hundreds of thousands of them, is logically a separate object, you're facing a memory disaster,
instantiating that many objects is just prohibitive.
So flyweight basically says, if these objects are similar enough, let's just share them.
But how do you share objects when their position, or their color, or their font is constantly changing?
This is where the critical distinction comes in.
Separating state into two types.
First, you have intrinsic state.
This is the data that is independent of the flyweight to context.
It's stored inside the flyweight, and that's what makes it shareable.
For a character object, that would be the character code itself, like the ask value for a.
Exactly.
And the other half is the extrinsic state.
Right.
That's the context -dependent stuff.
It varies with the object's use.
The position of the A on the page, the font size, the color.
This state must be supplied by the client when it makes a request to the flyweight.
The flyweight itself never stores it.
So when I call draw on a flyweight for the letter A, I have to pass in the coordinates in the font and the color as arguments.
The flyweight only uses its internal shared knowledge of A and the external context I supply to do the drawing.
Precisely.
And because sharing is mandatory, clients can't just instantiate flyweights on their own.
They have to get them from a central flyweight factory.
The factory manages a pool and makes sure that if a client asks for the flyweight for A and one already exists, it just returns that same shared instance.
Brilliant complexity management through strict state segregation.
OK, finally, let's discuss the proxy pattern.
The intent here is to provide a surrogate or a placeholder for another object to control access to it.
Proxy is all about control and direction.
And the simplest motivation is performance.
Let's go back to our document editor.
Loading a massive image takes time and memory.
You don't want to load it until the user actually scrolls to that page.
In that scenario, we use a virtual proxy.
The proxy object stands in for the real image.
It implements the same interface, but internally it only caches minimal information like the image dimensions.
So the document formatter can still lay out the page instantly.
Right.
The key is that the proxy only creates and loads the expensive real image object when the client calls an operation that absolutely needs it, like the draw operation.
It's delayed or deferred instantiation saves a ton of time and memory.
And the sources outline three other crucial types of proxies based on what kind of access they control.
Right.
There's the remote proxy, which handles spatial boundaries.
It's a local representative for an object on a different machine.
Then you have the protection proxy, which manages permission boundaries, controlling access based on client credentials.
And the last one.
The smart reference.
It just replaces a bare pointer to manage housekeeping tasks like reference counting or making sure dependent objects are loaded from a disk when you first access them.
But regardless of the type, virtual remote protection smart, the key requirement is that the proxy has to expose an interface that is identical to the object it's standing in for.
Absolutely.
The client has to be able to swap the proxy for the real subject seamlessly.
No questions asked.
Now that we have covered all seven, let's try to tie them together.
I mean, a lot of these patterns look very similar on the surface because they all rely on composition and indirection.
They do.
So let's revisit some of those comparisons.
Adapter versus Bridge.
Again, what's the core difference in architectural purpose?
It comes down to timing.
Adapter solves unforeseen integration problems between existing components.
You use it when you realize two things just won't talk to each other.
It's reactive.
It's reactive.
Bridge is used proactively to manage anticipated future variation.
You build a bridge when you know your system is going to need multiple implementations and you want to separate those concerns from the very beginning.
And what about composite versus decorator?
Both rely on that recursive nesting of components that share an interface.
Their intent is the discriminator.
Composite is focused entirely on representation building a hierarchy so you can treat one object and many objects in the same way.
Uniformity.
Decorator is focused on embellishment or augmentation.
It's about dynamically adding responsibility or features without changing the core object.
They actually work together brilliantly, but their jobs are fundamentally different.
Finally, decorator versus proxy.
They both wrap an object.
They both implement the same interface.
The intent, again, defines the boundary.
A proxy's job is always about controlling access, controlling when, where, or by whom the object is used.
It manages boundaries in time, space, or security.
A decorator's job is about enhancing functionality or adding responsibilities within those boundaries.
So decorator adds features.
Proxy manages the relationship between the client and the subject.
That provides a huge architectural insight.
Proxy deals with system boundaries.
Decorator deals with feature layering.
That's the takeaway.
These structural patterns are your toolkit for managing the sheer scale of modern software.
They're all strategies for organizational integrity.
If you're struggling with a rigid implementation bound to an interface, you probably need a bridge to separate those concerns.
And if you're facing that subclass explosion we talked about, you need a decorator to layer features dynamically.
Understanding the intent of the seven architectural blueprints is really the shortcut to designing far more flexible, resilient, and reusable systems.
They give you a common vocabulary for diagnosing and solving the most persistent structural problems in software design.
These are not just academic concepts.
They are the battle -tested foundational language of engineering reusable object -oriented software.
Absolutely.
Thank you for joining us on this deep dive into structural patterns.
We encourage you to start looking for these patterns in the software you use every day.
We'll see you next time.
ⓘ 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
- Behavioral PatternsDesign Patterns: Elements of Reusable Object-Oriented Software
- About Crystal Structures and Diffraction PatternsStructure of Materials: An Introduction to Crystallography, Diffraction and Symmetry
- Broad Patterns of EvolutionCampbell Biology in Focus
- Chromosome Number & Structural VariationPrinciples of Genetics
- ConclusionDesign Patterns: Elements of Reusable Object-Oriented Software
- Creational PatternsDesign Patterns: Elements of Reusable Object-Oriented Software