Hide the constant, abstract the code

macsikora

Pragmatic Maciej

Posted on March 13, 2021

Hide the constant, abstract the code

We like to use constants, put them in some shared files and use them everywhere. Although, it happens that we don't recognise when not only the constant repeats, but also how the constant is used repeats.

Let me tell you about some error message

Somewhere deep in the source code, repeats an import, in this import, on top of many files, there is a "nicely" named constant STANDARD_ERROR_MSG.

Deep in all these files, there is also the use of the constant, the same, exactly the same in all of them:

import {STANDARD_ERROR_MSG} from 'constants';
// usage
<Error message={STANDARD_ERROR_MSG} />
Enter fullscreen mode Exit fullscreen mode

Why my precious constant, why you need to be exposed to all these files, not better to sit in one spot? Some privacy would do, why everybody need to know you, my precious constant.

Precious constant hiding

const Error = ({message = "This is my precious error message"}) => 
  <p>{message}</p>;

// usage in code
<Error />
Enter fullscreen mode Exit fullscreen mode

No constant anymore, but also one import less in every file using <Error />, no more copy/pasted props.

Implicit default value, bleh

Ok, some of you can say, previous was explicit and now we have implicit default value. True, but we can make it explicit and still not use the shared constant.

const Error = ({message}) => 
  <p>{message}</p>;

const StdError = () => <Error message="Std eror" />
Enter fullscreen mode Exit fullscreen mode

We can go forward and make other kinds of errors:

const PermissionError = () => <Error message="No permission" />
const AuthError = () => <Error message="Not authenticated" />
Enter fullscreen mode Exit fullscreen mode

After that we don't import constants, instead we import reusable components.

The story about groups

Developer task requires different logic for different users groups. No problem, said developer, no problem at all. Firstly as every good developer should, he checked how we distinguish users in the code base, and there he found:

import {Group} from 'constants';
// 3 times in the code base
user.groups.includes(Group.Marketing)
// 9 times in the code base
user.groups.includes(Group.IT)
// 22 times in the code base
user.groups.includes(Group.Management)
Enter fullscreen mode Exit fullscreen mode

So let's add another use of these, shall we? No! No! Shouted developer. We copy the same logic, import the same constants, and we use these constants in the same way everywhere. I can do that better, said developer with a big dose of faith.

Let's name this, in other words, let's make abstraction from available use examples. Firstly abstract the calculation/logic:

const isGroupMember = (group) => (user) => user.groups.includes(group);
Enter fullscreen mode Exit fullscreen mode

Ah, developer wants to look smart by this function returning another function. But looks like this has a reason:

// not exposed private enum
enum Group {
  Marketing,
  IT,
  Management
}
const isMarketingMember = isGroupMember(Group.Marketing);
const isITMember = isGroupMember(Group.IT);
const isManagmentMember = isGroupMember(Group.Management);
Enter fullscreen mode Exit fullscreen mode

Wow, this clever developer made isGroupMember in such a way, that it is a factory for functions which address specific group. Clever!

Pay attention that we do apply first argument to isGroupMember and we get out function which takes user as argument and has pre-populated group already. In FP terms we would say isGroupMember is curried function, and we partially apply it.

Now the codebase has:

// 3 times in the code base
isMarketingMember(user)
// 9 times in the code base
isITMember(user)
// 22 times in the code base
isManagmentMember(user)
Enter fullscreen mode Exit fullscreen mode

No constant use, but new primitives in form of functions, no copy/paste of logic. Our developer can play some games in the evening, he has earned that.

Check my status

Paid or not, the question should be ask in the code, so it is:

import {PaymentStatus} from 'constants';
payment.status === PaymentStatus.Completed
Enter fullscreen mode Exit fullscreen mode

And we check like that in ten places maybe, but will be more. All these places need to import the constant, and make the check. Abstraction will save us again:

const isPaymentComplete = (payment) => 
  payment.status === PaymentStatus.Completed
Enter fullscreen mode Exit fullscreen mode

No constant imports, no need to remember which field compare to which status (people using TS can say now - this argument doesn't apply to TS, and I agree), everything nicely abstracted and we have our new primitive.

Domain Specific Language

All these functions isManagementMember, isITMember or isPaymentComplete are our new primitives, and can be used in the codebase. They abstract implementation details, and we can focus on the higher business rules. Using constants without reusing the logic will not level up the abstraction, the detail remains. If we see the same constant used in the same way few times in the codebase, maybe it is a place for our new domain primitive expression?

đź’– đź’Ş đź™… đźš©
macsikora
Pragmatic Maciej

Posted on March 13, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related