Are you sure you need that 'else'

Published on

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:

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

<div class="border border-2">
<div class="p-4 bg-primary text-white">
<h4 class="mb-0">@Title</h4>
</div>
<div class="p-4 fs-6">
@ChildContent
</div>
</div>
@code {
[Parameter]
public string Title { get; set; }
[Parameter]
public RenderFragment ChildContent { get; set; }
}

There’s nothing really to this, just a little bit of UI and parameters to set the title and contents…

<Panel Title="Sales (this month)">
<p>This could be a chart or anything really...</p>
</Panel>

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!)

<div class="border border-2 @visibility">
<div class="p-4 d-flex justify-content-between @TitleClass() text-white">
<h4 class="mb-0">@Title</h4>
@if (Type != PanelType.Important)
{
<button class="btn-close" @onclick="()=>dismissed = true"></button>
}
</div>
<div class="p-4 fs-6">
@ChildContent
</div>
</div>
@code {
[Parameter]
public string Title { get; set; }
[Parameter]
public RenderFragment ChildContent { get; set; }
[Parameter]
public PanelType Type { get; set; }
private bool dismissed = false;
private string visibility => dismissed ? "d-none" : "d-block";
string TitleClass()
{
switch (Type)
{
case PanelType.Default:
return "bg-primary";
case PanelType.Important:
return "bg-danger";
case PanelType.Inactive:
return "bg-secondary";
}
return "bg-primary";
}
public enum PanelType
{
Important = 1,
Inactive = 2,
Default = 3
}
private void Dismiss()
{
if (Type != PanelType.Important)
{
dismissed = true;
}
}
}

(this is using Boostrap CSS utility classes to control the panel’s appearance).

Now we’ve embedded the idea of different panel types:

With possibly more to come.

Two panels

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:

...
@if (Type != PanelType.Important)
{
<button class="btn-close" @onclick="()=>dismissed = true"></button>
}
...

And the razor component’s code:

...
string TitleClass()
{
switch (Type)
{
case PanelType.Default:
return "bg-primary";
case PanelType.Important:
return "bg-danger";
case PanelType.Inactive:
return "bg-secondary";
}
return "bg-primary";
}
private void Dismiss()
{
if (Type != PanelType.Important)
{
dismissed = true;
}
}
...

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:

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:

<Panel Dismissible="false" TitleClass="bg-danger">
Important Panel!
</Panel>
<Panel Dismissible="true" TitleClass="bg-primary">
Some other Panel!
</Panel>

Here we’ve exposed parameters for Dismissible and TitleClass, which removes some of the complexity from the underlying Panel component:

<div class="border border-2 @visibility">
<div class="p-4 d-flex justify-content-between @TitleClass text-white">
<h4 class="mb-0">@Title</h4>
@if (Dismissible)
{
<button class="btn-close" @onclick="()=>dismissed = true"></button>
}
</div>
<div class="p-4 fs-6">
@ChildContent
</div>
</div>
@code {
[Parameter]
public string Title { get; set; }
[Parameter]
public RenderFragment ChildContent { get; set; }
[Parameter]
public string TitleClass { get; set; } = "bg-primary";
[Parameter]
public bool Dismissible { get; set; } = true;
private bool dismissed = false;
private string visibility => dismissed ? "d-none" : "d-block";
private void Dismiss()
{
if (Dismissible)
{
dismissed = true;
}
}
}

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

<Panel Title="@Title"
ChildContent="ChildContent"
TitleClass="bg-danger"
Dismissible="false"/>
@code {
[Parameter]
public string Title { get; set; }
[Parameter]
public RenderFragment ChildContent { get; set; }
}

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:

<ImportantPanel Title="Another One?">
<p>I mean how many panels are enough?</p>
</PanelImportant>

This small change brings numerous benefits:

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
Next Up
  1. Blazor Changes in .NET 8 - Solving Blazor's biggest challenges?
  2. Use a recursive Blazor component to render a TreeView
  3. Inject content into your Blazor components with typed Render Fragments