Deliver reusable components without making them reusable

Tjeerd in 't Veen

Tjeerd in 't Veen

— 7 min read

Reusability and duplication are tough to balance. What if I told you that, sometimes, you don’t have to?

In this article, we are going to get philosphical about the purpose of components. This allows you to deliver features that are more reusable with little risk.

Online you can often find platitudes about code reusability. Such as:

“If you use a component three times, only then make it reusable”.

Or

“The wrong abstraction is more expensive than duplicate code.”

These statements are fine as general advice. But, often there is a lot more nuance to writing reusable code.

As we’ll explore in this article, often you can make a component more reusable with low risk, which makes you more nimble.

Reasoning about naming

We’ll look at some views for a more visual explanation.

Imagine we’re developing a Money Management app, containing a feature that gives personalized financial advice in the shape of advice cards. This feature is called MoneyBuddy.

Specifically, let’s take a look at the card component, highlighted below.

What would you call this component? MoneyBuddyCard perhaps? That’s a fine name, but it tells us it can only be used for the MoneyBuddy feature.

What goes “wrong” here is that the name is leaking how it’s used. As developers, we are now limited to use this card solely for the MoneyBuddy feature.

Or, what you sometimes see is that we use a “wrong” component for other features, but we just tell our coworkers: “I know the name is wrong, but it’s the component we should use”.

Removing the use-case from a name

Let’s try removing the use-case by giving the same component a more generic name. Then we can use this component in other features.

What if we named it AdviceView? Then it’s disconnected from the MoneyBuddy feature. It tells us more about what it’s used for.

It’s better, we can now use this component for other features. Unfortunately, as the name implies, we can only use it for advice. Because the use-case is still leaking in the name.

But this component doesn’t “care” what we’re using it for; advice, a promotion, a warning, or otherwise. From the perspective of this component, it has no specific use-case.

Let’s completely remove the use-case from the name by calling it after what this type is. In this case, it’s a card with some styling, let’s name it DecoratedCardView.

Then we can use it for the MoneyBuddy feature, or we can use it for advice somewhere else, and we can use it for many other scenarios.

It gives us more agility, and all we had to do was remove the use-case from its name!

Specific versus generic names

The more we leak the use-case into the name, the more we limit the component to a single use. As a result, components with specific names often live near a feature.

Conversely, when we disconnect the use-case from the name, the more reusable a component becomes. Then it might even live in a shared UI library.

Another example

Let’s look at another example, taken from the Mobile System Design book.

This is a screen where a student connects with a tutor — such as a guitar teacher. This tutor can create a custom learning plan, or curriculum, for a student.

Note that this UI is from a student’s perspective.

There is a TODO List in here. Let’s take a look specifically at this component where a student, can complete daily tasks.

In our case, we see a TODO List, so we might think “Let’s call it TODOListView!”.

It makes sense. After all, it is what we call it when talking about this screen. But try to disconnect the technical name from the feature.

With a name like TODOListView, we are again leaking how we are using the component into its type name. But, this component doesn’t “care” that it’s used for TODO lists.

Try to reason about the functionality, not the use-case of the component. Ask yourself: “What does this component do?”

The answer is that this list is a way to make selections.

Today, we use this component for TODO’s; But, tomorrow we could use it for something entirely different, such as configuring push notifications in a settings screen.

Let’s name it after the fact that we use it for selections, such as SelectionView.

By naming it SelectionView we can still use this component for TODO lists, and if we need to use it again later for something else, we have that option.

The best part: We did not spend any extra effort to make it reusable. It just is. All we had to do was give it a name that does not leak its use-case.

Generic type names

If making type names more generic seems odd to you, keep in mind that you most-likely already are using more generic components for your specific features.

For instance, let’s imagine you have an app where you can upload pictures of your meals.

To have users select a photo, would you use a custom made MealPhotoPicker? Most likely you would use the more reusable PhotoPicker offered by the platform. It probably has a filter applied to show photos of meals.

But it’s the same principle; The use-case of picking meals is not present in the PhotoPicker. It just is a PhotoPicker that can be used for picking meals, as well as millions of other use-cases.

Adding context in code

We tend to leak use-cases in type names when we want more context.

For instance, to depict a user’s avatar and name, we rather use something named UserProfileView over ThumbnailView. It tells us more about how to use this type.

Luckily, we can get the best of both worlds, where we deliver generic types, yet we retain this context.

When dealing with more generic type names, such as DecoratedCardView, you can still put the use-case in the instance name of a type, as opposed to its type name.

Below, you can see two examples. For both pairings, the use-case is in the instance name. In the first pairing, the types leak the use-case in their name. In the second pairing, the types are more generic in their name.

// Before
let moneyBuddyCard = MoneyBuddyCard(...)
let adviceView = AdviceView(...)

// After
let moneyBuddyCard = DecoratedCardView(...)
let adviceView = DecoratedCardView(...)

Adding context using factories

There are more ways to add context. One of them is using factories.

Below, we introduce a factory called MoneyBuddyFactory, which returns types for a feature. We may call makeAdviceCard() which will return a DecoratedCardView type.

final class MoneyBuddyFactory {

    func makeAdviceCard() -> DecoratedCardView {
        return DecoratedCardView(...)
    }

   // ... rest omitted
}

Now when calling this code, we get quite some context. Even though the type returned is still the reusable DecoratedCardView, we can see that we’re working with advice cards in the MoneyBuddy feature.

// We have a lot of context at creation
let adviceCard = moneyBuddyFactory.makeAdviceCard()

// The type is still DecoratedCardView
type(of:adviceCard) // DecoratedCardView

Adding context through aliases

Some languages, like Swift, allow you to use an alias to give a specific name to a generic component. This way, you can add more context without having to introduce a new component.

Below we introduce a MoneyBuddyCard alias, which is a new name for the DecoratedCardView. Then, within the MoneyBuddy feature, we can refer to MoneyBuddyCard instead of DecoratedCardView.

// We add a specific name
typealias MoneyBuddyCard = DecoratedCardView

// We can use the specific name.
let myCard = MoneyBuddyCard()

// But its type is secretly still DecoratedCardView.
// Below we explicitly define the type.
let anotherCard: DecoratedCardView = MoneyBuddyCard()

When using an alias, the “old” name still remains. In this case, the DecoratedCardView component is still available.

Now, inside the feature, we can use the specific MoneyBuddyCard name without introducing a new component.

Yet, we still keep the DecoratedCardView type that we can use in other features. We can even move it into a UI library if we so desire.

Heuristics for making reusable components upfront

One danger when making reusable components is to make something for hypothetical, future, use-cases.

We can’t accurately predict how components and requirements will evolve — if we could predict the future, we would probably be enjoying a luxurious vacation instead of reading this article.

There is a risk factor associated with making components reusable upfront. Because if we make a component more complex for reusability, then it’s a waste if no other feature uses this component. We would maintain a more complex component for no reason.

Alternatively, even if the complexity stays low, we may spend extra time making components reusable. Yet, that invested time would be wasted if the component is never reused in the future.

Instead of making a reusable component “just in case”, ask yourself: “Does making my component reusable…”

1: … increase the complexity of my component?
2: … require a large upfront time investment?

If the answer is no to both questions, then the risk is low to make a component reusable for a single use-case.

In the examples in this article, all we did was change the name. Specifically, we removed the use-case from the type name.

We did not increase the complexity or spend any time upfront to “make a component reusable just in case”.

Because of this, the components we made are now more reusable by nature without an upfront investment.

This keeps the risk low. Let’s imagine we never need these components to be reused, then there is little to no harm done.

The neat thing is that this concept applies to any component, such as UI flows, business models, or even the names of entire modules.

Conclusion

It may sound like a simple concept, but spending extra time on naming is critical. It is the difference between delivering a feature, or delivering a feature, plus a healthy library of reusable components.

Next time you make a new type, see if you can remove the use-case from its name. You’ll see that this exact same component becomes more generic and reusable, even though you’re only changing its name.

If you’re interested in learning more about reusable components, then check out chapter 10 and 11 from the Mobile System Design book.

Want to learn more?

From the author of Swift in Depth

Buy the Mobile System Design Book.

Learn about:

  • Passing system design interviews
  • Large app architectures
  • Delivering reusable components
  • How to avoid overengineering
  • Dependency injection without fancy frameworks
  • Saving time by delivering features faster
  • And much more!

Suited for mobile engineers of all mobile platforms.

Book cover of Swift in Depth


Written by

Tjeerd in 't Veen has a background in product development inside startups, agencies, and enterprises. His roles included being a staff engineer at Twitter 1.0 and iOS Tech Lead at ING Bank.