Are you sure you need that 'else'
A few years ago I worked for a company with a large codebase.
We’d been going for a few years and, as it has a habit of doing, the existing code had spawned more code until finally, we’d reached a point where managing and maintaining the various applications was a bit of a slog.
Tell me if this sounds familiar:
- A feature request comes in for a “small tweak” to some existing logic
- You head in to the code to figure out where to make the change
- Several hours later, the full enormity of the task hits you, you realise you’re no closer to finding the relevant code than you were two hours ago
- You keep jumping from one method to another, trying to remember the convoluted path you took along the way
- Finally, a few hours (or even days) later you find the promised land, the hallowed
if
statement you’ve been looking for - There’s already 15 branches to this thing, some of them nested a few layers deep, but you can finally see where the “small tweak” could be made
- You take a momentary pause to think “this could probably do with a refactor” then throw another
else
condition in and deny you were ever here in the first place!
This is true for backend apps, but it applies to UI/front end to.
It doesn’t take much for a “simple” component to descend into chaos.
It starts off simple, a nice small component to show a panel (for example).
Panel
There’s nothing really to this, just a little bit of UI and parameters to set the title and contents…
But it doesn’t take much for the complexity to start ramping up.
Let’s say we’re asked to make panels dismissible (so they can be hidden after the user has seen them).
Oh, and we also need Important panels, which have a red background for the title. Important panels should not be dismissible.
Here’s one attempt at handling these new requirements:
Panel (Advanced Edition!)
(this is using Boostrap CSS utility classes to control the panel’s appearance).
Now we’ve embedded the idea of different panel types:
- Important
- Inactive
- Default
With possibly more to come.
With this we’ve met the requirement (for non-dismissible important panels) but, even at this early stage the complexity is creeping up.
We’ve ended up with conditional logic, checking the panel type, in multiple places, in the markup:
And the razor component’s code:
Conditional logic tends to attract more conditional logic over time…
It seems clear this code is increasing in complexity, and likely to continue to grow organically as the feature requests pile up.
NOTE
It’s amazing how quickly something simple like a Panel can ramp up in complexity. Take these example requirements:
- Time-limited panels (got a black friday sale going? Only show panels until the promotion is over)
- Panels for logged-in users only
- Panels which can be dismissed after 5 seconds (the user needs to have had a chance to read them first)
- Panels which can only be dismissed after the user has done something (taken an important action)
An ex-colleague of mine used to refer to these as “If it’s Tuesday, and it’s raining” requests.
Implement one or two of these requirements and our small, simple Panel
probably won’t be so small, or simple after all!
Design your component’s API#
So let’s take a step back and ask a key question:
What do our panels need to be able to do.
It looks like we need to give them options to be:
- Dismissible
- Customisable (specifically the
TitleClass
we’re using for the panel heading)
At the moment we’ve put all the logic for controlling the appearance and behaviour of different types of panel in the Panel
component itself.
One option would be to “dumb down” the panel component, to have less knowledge about our important panels etc.
We could make Panel
instances more configurable, at a granular level, by expanding the public API of the Panel
component:
Here we’ve exposed parameters for Dismissible
and TitleClass
, which removes some of the complexity from the underlying Panel
component:
But we’ve lost something here:
We can render Panel
instances wherever we we like, and arbitrarily set the Dismissible
and TitleClass
parameters, but we’ve lost some all important domain knowledge in the process.
The business came to us and talked about Important panels, which always behave a certain way.
If we simplify that down down to a few primitive parameters (Dismissible
and TitleClass
) we’ve lost that context in our code.
What started as a clear business requirement for ‘important panels’ is now implemented as a generic Panel
component, which just happens to have a certain combination of values (not collapsible, red).
With this approach we can’t easily locate, let alone change all important panels as new requirements come in, because we haven’t used the terminology ‘important’ anywhere in our code. Effectively we just have a number of panels with seemingly arbitrary combinations of parameter values.
NOTE
Ubiquitous Language
It can be really useful to embed the terminology used by the business in your components.
Domain-Driven Design talks about having a ubiquitous language, shared by developers and the business, and embedded in the code for an application.
In this case, if someone in the business keeps using the phrase ‘important panels’ then it’s going to be easier to work on, and maintain that functionality if we use the phrase ‘important panel’ in the code.
On the other hand, the Panel
component is looking quite simple and maintainable at this point, so what should we do?
Well, we can refactor that knowledge, and logic about Important Panels, into a separate component which, in turn, delegates to the generic Panel
component.
ImportantPanel.razor
Now we’ve encapsulated what an ImportantPanel
is, in terms of appearance and behaviour. We can use this panel as many times as we like:
This small change brings numerous benefits:
- If we ever get a requirement to tweak the colour, or behaviour of important panels, we can do it in one place
- There’s less code to reason about, because it delegates to the existing
Panel
“black box” for the core Panel functionality - The ‘API’ for
ImportantPanel
is consistent, with no options to setDismissible
, because Important Panels can’t be dismissed - It should prove easy to find the code that relates to important panels because it’s in a single, well-named component
- This
ImportantPanel
component doesn’t have any concerns about other panel types, so there’s no risk of behaviour leaking into other component types by mistake
As more requirements come in, we can create more, specific, panel variations.
‘Thinking in components’#
This ability to compose components together is a big part of building ‘modern’ web applications with Blazor.
I’ve been using component-based frameworks since Angular JS first burst on to the scene and I remember thinking back then, how powerful components were, but also how I wasn’t quite “getting it”.
My apps would start off simple, with one or two components, but then slowly grow in complexity, until they were, frankly, a bit of a mess of interconnected components.
Since then, with every new requirement, and a lot of trial and error, I’ve built up mental models, and a repeatable, consistent approach that makes it much faster, easier, and more enjoyable to build web apps using components.
The approach we’ve seen here is just one way you can really leverage Blazor’s component model to build better web apps.
But there’s more, lots more! 📚
Over the last few months or so I’ve been working on a new self-paced, interactive workshop that makes Blazor development easier and faster by retraining your brain to “think in components”.
Find out more about the Blazor Jumpstart below.
Are you getting the most out of Blazor?
Make more of Blazor's component model, and build modern, reliable web applications, faster with .NET.
Speed up your development