Better Programming

Advice for programmers.

Follow publication

The Code Complexity Hockey Stick

Daniel Niland
Better Programming
Published in
10 min readOct 17, 2022
Hockey Stick with ‘Help Me!’ on the end.
Photo by Matthew Henry from Burst. Modified (poorly) by the author.

I make video players for the web. From the outside, such players seem like they’d be simple; stick an html <video> tag on your page point it at a media file and, with a little elbow grease, off you go. Give it a try. I dare you.

Don’t forget to splice in ads and handle custom subtitles and account for all the different browsers on all the different devices. Oh, and there’s tracking and seeking and fullscreen and implementing a good ui and… I hope you get the picture.

Video players turn out to be non-trivial because they contain a huge variety of features which interact in unpredictable ways. Nearly every video player I’ve ever worked with, including several I built myself, has eventually collapsed under its own weight. They have coupling issues. They’re not terribly extensible. Player teams get mired fixing obscure issues. New features become more and more complicated and brittle over time.

Applications like video players — with lots of interrelated states — can easily fall prey to the hockey stick graph of complexity:

Graph by Daniel Niland

How many of you are familiar with the process? It goes something like this:

You’re lucky enough to work on a greenfield project and everything’s going great for the first year or two. Development is quick. Features roll off the fingertips with little oversight. But then something happens — and it can happen suddenly. Issues roll in. Fixing one piece of code often breaks something else. After another year or two, you’ve become paranoid and meticulous. You approve PRs with the flair of my cat going outside for the night — peeking around every corner like a commando in enemy territory.

Ouch! You’ve been whacked by the business end of the hockey stick.

The Controller Manager Muddle

Over and over again, I’ve seen the same general pattern used for complex client-side applications (by which I don’t mean web applications, like most pages made in React or Angular — instead think video players). It’s a design pattern I’ve never seen spelled out in any book on software architecture, though I believe it’s an extension of the Model-View-Controller pattern. I’ll call it the Controller-Manager-Muddle pattern (CMM).

CMM happens when you try to apply the tenets of ‘good’ object-oriented design by expanding the Controller portion of MVC to handle complex business logic. You end up with a few god-class Controllers and bunch of supporting Managers and Factories and Engines and Items — a whole Mt. Olympus of lesser gods. Each of these classes, if built ‘well’, works hard to encapsulate its own state.

But are these classes cohesive? Not so much. There’s nothing cohesive about five thousand lines of code (yes, I’ve seen it with my own eyes) that does random things to a single bit of long-lived state.

Let’s call it what it is: a f**king disaster.

I’m not hating here. People are doing the best they can. Software Architecture books are obsessively focused on the back end and it’s nearly impossible to translate all the latest innovations into anything that makes sense for a large-scale frontend application. Google it. Most of the books and articles are either poorly written or focus on React. React and Redux are awesome, but they’re not built to handle our use case.

So how do we fill this void? First, we must understand why the Controller-Manager-Muddle paints us into a complexity corner.

Here’s a diagram of a typical CMM application.

An actual CMM diagram with the names changed a little.

It’s pretty clear how this application works… NOT! (Is this a safe space to revive Gen X memes?)

I’ve actually had the pleasure of working with this app, and it’s not poorly made. It does many things very well, but after a few years, it succumbed to the hockey stick. It became very difficult to maintain and extend.

Here’s the problem: Applications like this are composed of classes that encapsulate long-lived state. In order to get anything done, each controller and manager must somehow share its internal state with other parts of the application at key moments. All this is very object-oriented and seems to be the logical thing to do.

But it is a slow death. This design kills your application in two important ways.

Application Death pt. 1: Message Passing Becomes Inflexible

Look at the following code which is just a small part of a function that instantiates an ads manager (based on real code but changed to obfuscate and simplify):

...
this.adPlugin?.setAds(ads!);
if (ads?.importSys?.csai_type) {
if (popPlaybackData.adsConfig) {
popPlaybackData.adInsertConfig.isTlManRequired = true;
}
this.timelineAdsPlugin = new TimelineAdsPlugin(
new JsonABProvider(),
this.session,
initialPlaybackData.type,
this.pauseAdvsDispatcher!,
initialPlaybackData
);
this.timelineAdsPlugin.setAds(ads!);
}
if (popPlaybackData.adsFailoverReason) {
warn(`Error Fetching Ads data. Destroying Adverts Manager:`);
this.destroy();
return popPlaybackData;
}
const modPlaybackData = await this.modifyPlaybackData(popPlaybackData);
this.adPolicyMan?.init(this.session.onPbTimelineUpdated.bind(this.session));this.abProvider?.setPlaybackData(modPlaybackData);return modPlaybackData;

I’m not singling out this code as especially bad. This is all pretty typical for any class that contains references to other classes. What does it do? Some member classes are conditionally created. Some may or may not exist when this code is run, but we’ll try to initialize them if they’re there. Data is passed around. There’s even a bit of business logic — to destroy the manager if the ads failover.

The order of execution is clear… or is it? How many sub-modules are kicked off here? What will all this state be used for? To begin to understand the application data in this particular piece of code, you would first need to open up a whole ecosystem of managers and special-use classes. You must recursively follow each function call, figure out what it does, and understand the return value.

This is the flow of execution. It’s easy to lose the trail in an application like this. Often the cognitive load for understanding any particular feature of the program is just too much for mortal developers. (Maybe that’s why they call them god classes?)

But so what? Applications are complicated. Dependencies happen. Suck it up and learn the code.

And that attitude may be fine if we never need to modify the code again.

In the real world, however, applications evolve. Old features must be modified. New features must be added. This almost always involves modifying both the order and the format of the messages passed through your code.

Look back at the code above. Can the order of those calls be modified? Umm, maybe? Consider all those function calls within function calls and all that state that needs to be set just so. What if, rather than just being able to switch lines 150 and 151, we need to pull out a targeted piece of code that lies deep within the class that is instantiated on line 151 and put just that code before line 150, but the rest has to stay where it is. How would you begin to divide this stuff up? This is not an uncommon refactor, but the more baroque your flow of execution becomes, the more the code will be prone to unpredictable errors.

It’s the same for the format of the data. Say we add a feature to this existing codebase that demands that a whole new type of data needs to be added. How do you pass that new data around? What if it needs to be used in five random spots that are all in different unrelated parts of your application? Do you just add more parameters? Inject it?

Here’s the point. When you embed classes within other classes and call those functions directly, you can’t help but ossify your flow of execution. You’ll need to break a bone to bend an arm.

Application Death pt. 2: The Flow of Execution Becomes Dependent on the Structure of the State

Let’s follow the flow of execution through our application, sort of like being in that submarine in Fantastic Voyage. In a CMM pattern it might look something like this:

The flow of the application whips around the application like a whirlwind. It must make this torturous path because that’s how object-oriented design works, right? A proper class encapsulates its state so no one else has to deal with the details. Amen.

This is a ticking time bomb. If the application state as a whole becomes complicated enough (as with a web player), this design will collapse. Always.

But the reason for that collapse is subtle. Here’s how it works:

We tend to build our programs with an initial set of use cases in mind. Say the data in Manager A has to access Manager B to do some work asynchronously. As Manager B does its magic, it in turn calls Manager C which does something else. You hook all this up and it doesn’t really matter that you’re baking in the order of execution. A and C know nothing of each other. You’ve got encapsulation. You may even have cohesion. The world is good.

Now, a year has passed. Your product owner comes to you with a new feature that requires interacting in a completely different way with the data in Managers A and C. A and C don’t know anything about each other so now you need to figure out how to make this work.

There could be a few ways to handle this. Maybe you create a new controller D, that handles this feature by injecting both Manager A and Manager C. When Controller D needs to do its thing, it hits A then C, (or C then A, whatever) perhaps calling newly minted functions that let A and C modify their own internal state (encapsulation, baby).

Here’s the important thing, though: Now there’s code for a single feature across three different classes. Those classes are no longer cohesive (if they ever were). One class has many features. One feature has many classes. Coupling ensues.

You can get away with this type of distributed logic for a while. But, as you add features that cut across your stateful classes, the couplings grow combinatorially — even exponentially. The more coupling you have, the more likely it is that you will run into binds.

A bind is a flaw in logic created by an incompatible state. It is the unintentional consequence of coupling. These bind manifest as a practical zoo of bugs but the most difficult to fix and find are asynchronous issues where the state is changed unexpectedly out from under a process that needs it, or where you can’t predict the order in which a state is modified.

In a complex system, these binds become nearly impossible to untangle. But you can try…

Say you run into one of these binds and, through sheer force of stubborn will, you find its root cause. You stand before the team and say, “Remember that Controller D we put in there last year?” The meeting goes silent. How many of them actually worked on the project last year?

“Well,” you say, “it’s the strangest thing. The issue was caused by the function call to Manager A, combined with the New Feature Product Really Needs, but it only happens when the moon is full over the Philippines.”

No one seems impressed by your Sherlockian sleuthing. Damn them. You continue, “There’s no way we can refactor Manager D short of a complete re-write.” Everyone laughs nervously except the product manager. “But, no problem,” you say. “We’ll just put in a ‘isFullMoonPhillipines’ flag and we can move on.

Exhausted applause. What could go wrong?

These binds are a feature of object-oriented design, especially of the type seen in the Controller-Manager-Muddle pattern. No matter how hard we clack together our ruby slippers, we can’t wish them away. You can inject. You can try every design pattern in the book. You’re just delaying the inevitable. Eventually the complexity of the system will swamp every effort to save it — it’s a matter of runaway dependencies.

So, since you’re still reading, I hope you’ve been at least a little convinced that building an application with the Controller-Manager-Muddle pattern is like that last scene in Kill Bill, where Keith Carradine realizes Uma Thurman has done the five point palm exploding heart on him and he only has five steps left to live — and your project has already taken, what? Two steps? Three?

There should be one question burning in your mind.

How can we possibly move past this crap show of a design? How can we have a complex application that is truly extensible, scalable, cohesive, and decoupled? How?

Sorry, I have no idea.

JK! Hey, I’m the first to admit it isn’t easy building large-scale applications. In my next post, I’ll dive into the understanding coupling, and the relationship between architecture, business logic, and state.

Until then, happy coding.

Daniel Niland
Daniel Niland

Written by Daniel Niland

Studying the intricacies of life and video players.

Write a response

Lots of fancy schnizzle. Good luck with this. I'm neither impressed nor interested . . . so many Medium hooplas end up being a waste of time. We just want what's fair for a decent day's work.

--

Boy, this was a lot of work! Thanks for having our ackbizzles (is that right?)

--

Thanks, Robin! I always appreciate how much time and effort you put into your Medium information pieces. Hope you have a nice Wednesday❣️ A.J. (she, her)

--