Refactoring: Choosing abstractions

How do I pick the right abstractions? Are there rules that can be followed?

Ikechi Michael
3 min readDec 4, 2021

Let’s look at an example, and see if we can apply some rules to make code cleaner.

Example

Assume you are a dev working on a Web3 project where users can send tokens as gifts to one another.

Given a function:

😰

This is a huge function that does many things. But it’s not uncommon to find functions like this in the wild.

It is our task to refactor this function, clean it up, and make it better.

Step 1: Define what the function does

The first thing I like to do when refactoring a function, is figure out exactly what the function does. A well written pseudocode for the function will help us with cleaning it up.

From the steps, we can see that our function is concerned with details like:

getTokenById
updating amount if !build_number
wallets
wallet balances
verifying sender <> receiver
verifying token type is !private

before actually doing the thing it’s supposed to do, which is creating the transaction in escrow.

Step 2: Refine what the function does

For every step in the pseudocode above, we should ask ourselves:

  • Does this really need to be here?
  • Can this be part of a previous step?

For instance, the step that updates the amount if !build_number is obscure in what it does. What exactly is a build_number and what does it have to do with gifting tokens?

Does this really need to be here? No!

We can have the amount, resolved in a previous step, and explicitly passed into this function as an argument.

const giftToken = (jwtUser, values, amount, token_id);

For another instance, the walletsand wallet balances steps are not referenced anywhere else, and only exist to confirm that the user has the amount of the token they want to gift. They can be combined into assertUserHasTheTokenAmountTheyWantToGift .

So our steps become:

getTokenById
assertUserHasTheTokenAmountTheyWantToGift
verifying sender <> receiver
verifying token type is !private
create escrow transaction

The verify steps seem to be fine since they don’t reference details that are external to gifting tokens.

The create escrow transaction steps are external to gifting tokens, so they can be abstracted.

Step 3: Summarize into an explicit theory

An explicit theory is the simplest language that can be used to describe what a function does.

In our case, our explicit theory can be written as:

A token is gifted from a sender to a receiver, only if the sender has a token balance that is greater than or equal to the amount of the token they are attempting to send. When all verifications are successful, an escrowTransaction is created on behalf of the sender.

Using our explicit theory, we can then come up with our abstractions:

You will notice that the names of the variables have changed from jwtUser — > sender and this change is informed by our explicit theory, which is formed by our understanding of what the function should do.

--

--

Ikechi Michael

I’ve learned I don’t know anything. I've also learned that people will pay for what I know. Maybe that's why they never pay.