The complexity that lives in the GUI

The user interfaces are a weird thing. There are all sorts of libraries and frameworks that are supposed to help you on your journey of writing a GUI, but despite all the best practices and frameworks forcing you to eat your vegetables, the GUI always ends up being a ridiculous mess. After pondering some more about this topic, I’ve finally realized what is the cause of this problem.

Suppose you start working on a greenfield project for managing a warehouse (gasp). This GUI has everything that a warehouse owner desires, including the irrelevant oversized side bar on the left that scores upvotes on the design related social networks. The user avatar with the details of logged in user are on the left, inventory table is in the middle and all kinds of buttons are everywhere else.

If I asked you to architect such application from the given user interface concept, you would probably start to mentally compose parts of the GUI into boxes with labels such as: UserSection, InventoryTable, BottomActionButtons and so on. These days such labeled boxes are commonly put into separate classes. Classes are a great building blocks for the GUI, because they let you divide the complexity into smaller parts each containing just the internal state of one little component. When you are programming an inventory table in the middle you don’t want to care about the complexity surrounding the user avatar section. In other words, if you are organizing the stuff in box A you don’t care about the mess that resides in box B.

For a while this strategy of “mess in other boxes is not my problem” works, but soon enough you hit the next challenge. Suppose the user section of the warehouse GUI has a little light called “Working”. If the user is editing the inventory table that light should be displayed in green and when the user stops editing, the light should be turned back to gray.

Oops, the “mess in other boxes” is your problem now as you have to connect the editing state of the inventory table with the state of the light in the user avatar section. You find yourself at the crossroads of important design decisions.

  • Connect the boxes: create the user avatar component and pass its instance to the inventory table component. Whenever the edit state of the inventory table changes, the business logic in the inventory table should also trigger a state change in the user avatar component with the help of the user avatar’s public API.

  • Lift the state up: move the internal state of the user avatar component and the state of the inventory table into a separate box/class. The logic of the user avatar and inventory table component will still be neatly separated in their own boxes, but they will be able to communicate without inventory table needing the direct access to the user avatar.

  • Introduce a message bus: connect the inventory table and the user avatar component to the shared pipe that is used for distributing events in the application. The user avatar component subscribes to the message bus and every time it receives a table edit event, it executes an appropriate action (e.g turn the light on).

It goes without saying, that none of the presented options are without problems.

Connect the boxes

If you’ve decided to chicken out of introducing another layer for holding the common state, you may solve this cross box communication problem by injecting the user avatar component directly into the inventory table component. The programming theorists and other purists will tell you that this is a bad idea and you should never even think about it. Think about the children, err, all the tests that you won’t be able to write.

I am definitely guilty of such crime against the Holy Church of Unit Testers. When the project is still in its infancy and I have yet to figure out what am I even building, I like to jam the components together without giving it much thought. Sometimes you cut the corners, and sometimes the corners cut you. How bad could it really be?

It turns out, for small components this strategy works quite well. As usual, it’s often the wrong thing to do when your project grows large and contains hundreds of such inter class communication paths. Not to mention how injecting hundreds of components is tedious, error prone and ugly to look at.

In hard times like these, the developers like to reach out for one of the fancy pants dependency injection frameworks, which supposedly allow you to clean up the component injection mess. In reality they trade compile time safety for some convenience and runtime crashes [1]. Now, you have two problems:

  1. You are still injecting hundreds of components.
  2. The dependency injection framework randomly breaks and nobody knows why.

The main reason why I prefer not to use dependency injection frameworks in large projects is because they tend to make the whole project more convoluted and harder to understand. It’s very easy to add just another component injection and not put any effort towards refactoring the code. This leads to the proliferation of small classes or services, because the injection framework allows you to do with no upfront investment. You pay for this crime later on once you have hundreds of injections all over the place and nobody is able to follow and debug this mesh of small components (see also On navigating a large codebase).

Lift the state up

A preferred way, where preferred means some kind of handwavy generalization of what the majority of developers might do, of handling this accidental complexity is by lifting the state up and storing the state of your component into another box that is usually called the model. Model View Controller (MVC) gang rejoice. This pattern allows you to separate the presentation of your data (view) from the actual data (model), with the use of controller that connects the two together [2].

Instead of injecting hundreds of components into a god like component that controls all its children, you put the shared state of the relevant GUI components into a god like model that controls the state of all those children. The end result is similar, but separating the data from the view layer might earn you some positive reviews at the end of the year from the unit test groupies. Even though you improved the situation a little bit, you still have a huge model that is full of weird edge cases. The GUIs are inherently a giant state machines and they often get into a weird state that is only discovered when you are actually running the GUI.

Maintaining a large and messy model is hard, so you decide to saw the model into smaller models that group the state of components which conceptually fit together. At some point during this “saw the model” process you realize that certain things between the two different models should be kept in sync. The more modern GUI frameworks usually arm you with some kind of data binding abstraction, which allows to easily propagate data changes from one model to another via the so called one way, two way data binding.

Soon enough, you realize that it would be really useful if you could attach a change listener that would trigger and perform an action on every change of the state object. Say we would like to change the background color of the user avatar component every time the working light turns on. The code describing this situation might look something like:

this.lightTurnedOn.bind(this.editingInventoryTable);

this.lightTurnedOn.addListener((oldState, lightTurnedOn) -> {
    if (lightTurnedOn) {
        changeBackgroundToRed();
    } else {
        changeBackgroundToIbmGray();
    }
});

This is usually the point at which you start losing control of your GUI. Clicking on buttons will start triggering events which will modify the state in the model that will in turn start triggering event listeners causing your GUI to flash like a christmas tree. The problem of data bindings and change listeners is that they make it really easy to introduce a hidden circular event listeners that will trigger one another multiple times (event A changes state B and change of state B triggers event A). Such problems are rarely discovered during development, because developers are normally using powerful machines in comparison to the mortals that are still sticking to their ancient computers. These problems are also rarely discovered through code reviews, as the state changes are well hidden within one of those one line state bindings.

I still don’t know what the proper solution to this problem would be. Keep your state manipulations as simple as possible and try not to share any data between different models. Every time I went forward with some fancy listener-binding mechanisms, I’ve ended up causing subtle circular listener recalculations that were extremely hard to debug.

Message bus

Congratulations, a large amount of your effort will go towards resolving weird message bus problems as opposed to writing the business logic of your app. In the beginning, the message bus usually sounds like a good idea because it simplifies a lot of communication problems between the different UI components. You can send some events through the pipe and sure enough other parts of your application will react to them.

You might be thinking: “Message bus for communicating between components in the GUI? That’s crazy!” Well, it turns out you can have a message bus that is not a huge ram gobbling process. In fact you are probably already using it, as the GUI frameworks usually have some sort of an event queue built in that is used for propagating the events in the system.

As your application grows you might realize that just spamming messages back and forth causes a lot of performance problems. In case you have hundreds of components that are generating events, your little user avatar component might be sifting through hundreds of messages per second while it is only interested in one. Have no fear, message bus got your back. Any message bus worth their salt allows you to define different channels that introduce rough message type filtering. Why filter out all the irrelevant messages and waste CPU cycles, when you can subscribe to a specific channel that receives only the messages on the topic you are interested in?

But, it’s so easy to throw yet another message into the void.

GUIs are complex

It seems to me that most of the GUI complexity stems from the problematic cross component communication. What seems like a simple button change on the screen could be a lot more convoluted change due to problematic component wiring that happens in the background (see also It’s just a button). Just because the components are displayed close together on the screen doesn’t mean that wiring them together will be easy.

Either class based component modelling is not as appropriate as we would like to think, or we simply haven’t figured out the right abstractions that would allow us to easily connect different chunks of the application together. I’d love to hear what the functional programming camp has to say about this problem, but I guess they are too busy with inventing yet another $20 term for a 5 cent concept.

There is also another way of making GUIs called Immediate Mode that is commonly used for drawing user interfaces in games. In this mode the GUI components are no longer subscribing and waiting for events to come, but are instead a part of the main loop that runs at 60 fps and re-render themselves based on the current “global” state. One notable example of such immediate mode UI framework would be Dear ImGui and according to the ImGui gallery it seems to be a perfectly fine way of doing non trivial and performant GUIs. Immediate Mode GUIs somehow never reached the critical mass and you are probably not going to find it outside of the games industry.

As far as the common saying goes, don’t complain to your boss about the problem without providing at least one solution. I don’t have a boss and I don’t have a solution.

Notes

[1] Yeah, yeah not everyone is using type safe language. Some are also trying to outsmart compilers with unit tests - see also Unit testing is not enough. You need static typing too article.

[2] Talking about MVC usually spawns a heated debate where the business logic of your component should be present, but let’s not go into that because in reality it doesn’t really matter. The messy complexity of this world has to live somewhere regardless of how you call that component.