Chapter 2: A Case Study: Designing a Document Editor

0:00 / 0:00
Report an issue

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 to the Deep Dive.

Today, we're really getting to the guts of high -quality software development.

We are.

We're looking at a fantastic case study on object -oriented design centered on building a document editor called Lexi.

And Lexi is basically a model for something like, what, Word or Google Docs?

What you see is what you get, editor.

Exactly.

It distills all that complexity down to its core design challenges.

Our mission today is to look at seven of those big problems.

And not just the problems, but how they were solved using eight really foundational design patterns.

We're going to learn by seeing them in action.

This is where you see why these patterns exist.

They're all about adding flexibility.

But we have to start with a really crucial warning from the source material.

Design patterns are powerful, but they almost always add a layer of indirection.

You're adding objects, adding complexity to solve for future flexibility.

And that can have a cost, right?

Performance, maybe?

Or just making the design harder to understand at first?

Absolutely.

So the rule of thumb is simple.

Only use a pattern when you actually need the flexibility it provides.

Don't over -engineer.

Okay, with that in mind, let's get into the first big challenge.

How do you even represent the document itself?

So Lexi has to manage this incredibly complex internal structure.

You've got characters, images, tables, columns.

Everything.

And it all has to be represented internally, then drawn on the screen.

And here's the tricky part.

If a user clicks somewhere,

Lexi has to map that screen position right back to the specific element.

The real problem seems to be treating simple things and complex things in the same way.

Like, how is a single letter A similar to a giant table?

That's the core dilemma.

If you treat them differently, your code becomes a mess of if -then statements for every single operation.

So the solution here is something called a recursive composition.

That's it.

Think of it like a set of Russian dolls.

You build complex things out of simpler things, and those simpler things are built out of even simpler ones.

All the way down.

And the technical term for these building blocks is the glyph.

The glyph is an abstract base class.

It's the contract for everything in the document.

A character is a glyph.

A row of characters is a glyph.

A whole diagram is a glyph.

So what does that glyph contract or interface actually require?

It's mostly about three things.

First, how it looks.

Every glyph has to know how to draw itself and report its bounds, the space it takes up.

Second, its structure.

It needs methods to handle children and its parents, so things like insert, remove, child.

But a single character doesn't have children.

Right, so the character glyph just has empty implementations for those methods.

But a row glyph uses them to manage its list of characters.

I see.

So the client code, like a spell checker, just talks to the glyph interface.

It never has to ask, hey, are you a single thing or a group of things?

You've got it.

And that structure where you treat individual objects and compositions uniformly, that's the composite pattern.

It just simplifies everything.

All right, so we have the structure.

Now we have to actually lay it out on the page.

Formatting.

Line breaking.

Yeah, and this is a huge problem.

Formatting algorithms are really complex, and there are trade -offs.

You might want a super fast, simple one for when you're typing.

But for printing, you want the high quality one that, you know, optimizes whitespace and looks professional.

Exactly.

And that algorithm is a big self -contained chunk of logic.

You can't just bury it inside the glyph classes.

Because then you'd have to change your core structure just to swap out the formatting logic.

That sounds like a nightmare.

It would be.

So the key is to separate the content, the glyphs, from the how, which is the formatting algorithm.

You encapsulate the algorithm?

Precisely.

We pull it out into its own object hierarchy.

We call it the compositor.

So you have an abstract compositor class with one main method,

compose.

And then you could have a simple compositor or maybe a really fancy text compositor for that high -end layout.

Right.

And the composition, which is the special glyph that holds all the document content, it just holds a reference to whichever compositor is currently active.

So when I resize the window, the composition just turns to its compositor and says, hey, compose.

That's the interaction.

The compositor then iterates through all the wrong glyphs and inserts new structural glyphs, like row objects, to create the final layout.

That's incredibly clean.

It is.

And that's the strategy pattern.

You define a family of algorithms, you encapsulate each one, and you make them completely interchangeable.

Okay.

Next up is the UI itself.

The things around the main document.

Borders, scroll bars, maybe a drop shadow.

Things you want to be able to add or remove easily, maybe even at run day.

My first instinct would be to use inheritance, make a bordered composition class.

And that's where you hit the class explosion problem.

What if you want to scroll bar two?

Now you need scrollable composition, then border scrollable composition.

And if you add one more option, the number of classes just blows up.

It becomes totally unmanageable.

The goal has to be adding these responsibilities dynamically without this mess of subclasses.

So the solution is described as a transparent enclosure.

It sounds fancy, but the idea is simple.

Think of it like wrapping an object.

You have your core object and you wrap it in another object that adds some functionality.

Like putting a sleeve on a coffee cup.

Perfect analogy.

The sleeve adds the ability to hold it without getting burned.

But to you, it's still just the cup.

The wrapper is transparent.

And since everything in Lexi has to be a glyph.

The wrappers themselves must also be glyphs.

So we create a special abstract glyph subclass called monoglyph.

Its whole job is to hold a reference to one other component, the thing it's wrapping.

And by default, it just passes all the requests straight through.

Exactly.

Monoglyph just delegates draw to its component, making it transparent.

But then you create a concrete subclass like border.

Its draw method first tells its component to draw itself, and then it draws the border around it.

So you could have a border object that contains a scroller object that contains the main composition, a chain of wrappers.

You've got it.

And the client just talks to the outermost wrapper.

That's the decorator pattern.

You're adding responsibilities dynamically by wrapping.

Now let's talk about portability.

Lexi has to run on different operating systems and, crucially, look like it belongs there.

A button on a Mac looks different from a button in Motif.

Right.

You absolutely cannot have code that says,

new Motif scroll bar scattered everywhere.

Because porting it would mean finding and replacing every single one of those.

It would be impossible.

What we need is a way to create a whole family of widgets, the scroll bar, the button, the menu, and guarantee they all belong to the same look and feel without ever naming a concrete class in your application code.

So we need to abstract the creation of the objects.

Yes.

And that brings us to the abstract factory pattern.

We start by defining an abstract class, say GUFactory.

And that would have methods like create scroll bar and create button.

Correct.

It just defines the interface.

Then we create concrete factories for each look and feel.

A Motif factory, a PM factory, and so on.

They implement those methods to return the actual specific widgets.

So at startup, Lexi just figures out what system it's on and creates one single global instance of the right factory.

That's the idea.

And from then on, whenever any part of the application needs a button, it just calls the factory .create button.

It doesn't know or care if it's getting a motif button or a PM button.

The application code is completely decoupled from the specific widget classes.

Exactly.

Perfect for managing families of related products.

Okay.

But wait, we just solved the look and feel problem.

But what about the underlying window systems themselves?

The source material says X, PM, Mac APIs.

They're all incompatible at a much deeper level.

This is a really important distinction.

Abstract factory is great for creating product families.

But this problem is different.

How so?

We need to separate the high level concept of a window, what our application thinks a window is,

from the low level platform specific implementation of how you draw a rectangle on the screen.

These two things need to evolve independently.

So you're saying abstract factory isn't enough here.

It isn't.

This is a job for the bridge pattern.

The whole point of bridge is to decouple an abstraction from its implementation.

So we split the code into two completely separate class hierarchies.

We do.

On one side, you have the window abstraction.

This is what the application uses.

It has subclasses like application window or dialogue window.

It thinks in high level terms.

And on the other side.

You have the window imp or window implementation.

This is another abstract class.

But its interface is all about low level system specific drawing operations like device rect.

And it has concrete subclasses like X window imp or PM window imp.

And the bridge is the connection between them.

Yes.

Every window object holds a pointer to a window imp object.

So when your application tells its dialogue window to draw a rectangle.

The window object doesn't do the drawing itself.

It delegates.

It delegates across the bridge to its window imp, which then makes the actual native API calls for that specific platform.

So the application side is totally insulated from the platform specifics.

You could port the whole thing to a new window system just by writing a new window.

That's the power of the bridge pattern.

Total decoupling of abstraction and implementation.

All right.

Let's talk about what the user actually does.

Cutting, pasting, saving, changing a font.

These are triggered from all over the place.

Menus, buttons.

And most importantly, they have to be undoable.

Multi -level undo and redo.

Which means you can't just call a simple function because you need to remember how to reverse it.

That's the key insight.

The request itself needs to become an object.

And that is the command pattern.

Okay.

So we create an abstract command class.

What does its interface need?

Well, obviously an execute method.

But to support our goal, it also needs an execute.

And probably a way to ask is reversible.

And then a paste command would store what?

The text that was pasted and where it went.

Exactly.

It has to hold all the state it needs to both perform the action and crucially to reverse it completely.

And how do the UI elements like buttons use this?

Super simple.

A menu item doesn't know anything about pasting.

It just holds a pointer to a command object.

When you click it, it just calls execute.

Okay.

But how does the multi -level under redo work?

That's the magic part.

It's handled by a command history.

It's just a list of all the command objects that have been executed.

You keep a pointer, a present marker to the last action taken.

So undo just means?

You find the command to the left of the marker, call its un -execute method, and move the marker one step to the left.

And redo is the opposite.

Go right, call execute,

move the marker right.

You're just traveling back and forth in time along this history of commands.

It's an incredibly elegant way to handle user operations.

We're at our last major challenge, which is actually a two -part problem requiring two different patterns.

We need to analyze the document spell checking, hyphenation, that kind of thing.

Right.

And the document is scattered across this very complex glyph structure we built earlier with the composite pattern.

So part A of the problem.

How do we even walk through that structure to find all the characters without messing up the glyph classes by exposing their internal lists or arrays?

Yeah, we can't just add a bunch of traversal methods to the glyph interface.

It would get huge and inflexible because different analyses need to walk the tree in different ways.

The traversal logic needs to live outside the structure itself.

And for that, we use the iterator pattern.

We define an abstract iterator class with a standard interface.

First, next is done, current item.

The basics of walking through a collection.

And how do the glyphs use it?

The glyph class gets one new method, create iterator.

A row glyph, which might use a list internally,

returns a list iterator.

A column might return an array iterator.

So the client code just asks for an iterator and uses that standard interface to walk the document completely oblivious to how the data is actually stored.

Exactly.

The iterator solves the access problem.

Which brings us to part B.

We can access all the elements now.

How do we actually perform the analysis, the spell check, without adding a check spelling method to every single one of our glyph classes?

That would be a maintenance nightmare.

Every time you wanted a new kind of analysis, you'd have to modify hundreds of classes.

We need to avoid that.

And we do with a visitor pattern.

It's an amazing pattern that uses a technique called double dispatch.

Okay, double dispatch.

Let's walk through that slowly.

First, we put all the analysis logic into a separate visitor object hierarchy.

The abstract visitor class declares a visit method for every single concrete glyph subclass.

So visit character, visit row, visit image, and so on.

That's a lot of methods.

It is, but they're all in one place.

Now, the glyph abstract class gets just one new method.

Accept visitor.

Okay.

So when our iterator gives us, say, a character glyph, our analysis code calls my character, except my spelling visitor.

Right.

Inside the character class's implementation of accept, it immediately calls back to the visitor, but it calls the specific method, my spelling visitor.

Visit character this.

Oh, I see.

The first dispatch is the generic accept call.

The second dispatch is the glyph telling the visitor exactly what type it is by calling the correct visit method.

That's double dispatch.

It identifies the type of the glyph to the operation without any ugly casting or type checking.

So if we want to add hyphenation analysis, we just create a new hyphenation visitor class.

Right.

The hundreds of glyph classes don't change at all.

Not one bit.

You've completely separated the operations from the object structure they operate on.

It's the visitor pattern.

Wow.

We have covered a huge amount of ground here.

Let's recap.

We saw composite for the document structure, strategy for swappable formatting.

Decorator for adding UI features like borders,

abstract factory for handling different look and feel widget families.

Then bridge to decouple the application from the underlying window system.

Command for undoable user actions, and finally, iterator and visitor to perform complex analysis on the structure.

The common thread through almost all of these, it seems, is encapsulating the thing that changes.

That's the big takeaway.

Anytime you identify a part of your system that needs to vary, the algorithm, the platform, the look and feel, you pull it out, you give it its own object, its own hierarchy, and you make the rest of your system stable.

And these problems are universal.

This isn't just about document editors.

Any big application is going to face these challenges.

Absolutely.

Which brings us back to that first warning about trade -offs.

We saw that to get this flexibility, we often had to add an extra component like the compositor or the window imp.

A layer of indirection.

Right.

So a final thought for you to take away from this deep dive.

Think about a piece of complex software you use every day.

Maybe a video editor or a mapping application.

What's a piece of flexibility it gives you?

Maybe supporting new file formats or adding new kinds of filters.

And then think about what that flexibility might have cost the developers.

A little bit of performance, a bit more complexity in the design.

And was that trade -off worth it for the value you get as a user?

Something to think about until our next deep dive.

β“˜ This audio and summary are simplified educational interpretations and are not a substitute for the original text.

Chapter SummaryWhat this audio overview covers
Lexi, a WYSIWYG document editor, serves as an integrated case study demonstrating how design patterns solve interconnected architectural problems in complex software systems. The design begins with representing documents as hierarchical structures where characters, graphics, and other content must be treated uniformly despite their different complexities. Recursive composition and the Composite Pattern enable this by defining all document elements as abstract Glyph objects, allowing the editor to manipulate simple and complex components identically. Text formatting then emerges as a critical challenge, since different layout algorithms must work interchangeably depending on user preferences and constraints. The Strategy Pattern addresses this by encapsulating formatting logic within separate Compositor objects, making it trivial to swap one layout approach for another without restructuring the core document model. User interface enhancement presents another problem: adding embellishments like scroll bars and borders should not force rigid inheritance hierarchies or create tangled dependencies. The Decorator Pattern solves this through transparent enclosure, letting visual enhancements wrap components cleanly while preserving their original behavior. Supporting multiple platform aesthetics, such as Motif and Presentation Manager, requires abstracting widget creation processes so the application avoids hard-coded platform dependencies. The Abstract Factory Pattern provides this by defining families of related UI components that conform to different platform standards. Similarly, running on diverse window systems necessitates separating the logical Window concept from platform-specific implementations, which the Bridge Pattern accomplishes by decoupling abstraction from implementation. User interactions triggered through menus and buttons, particularly the sophisticated undo and redo mechanism, are managed by encapsulating requests as polymorphic Command objects that queue and reverse operations. Finally, supporting analytical features like spell checking and hyphenation requires traversing the scattered hierarchical document structure and applying operations uniformly across it. The Iterator Pattern abstracts traversal mechanisms, while the Visitor Pattern encapsulates analysis logic itself, enabling new analytical capabilities to be added without modifying existing Glyph classes.

Using this chapter to study? Last Minute Lecture is free and student-run. If it helped, consider supporting the project.

Support LML β™₯