DEV Community

loading...
Cover image for Hide the constant, abstract the code

Hide the constant, abstract the code

macsikora profile image Maciej Sikora Updated on ・3 min read

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?

Discussion (2)

pic
Editor guide
Collapse
svitekpavel profile image
Pavel Svitek

Looking at the groups example and isPaymentComplete function.. it's true that you don't have to import constants, but you still have to import those functions. So the only real difference is that you don't have to duplicate the business logic, which in this case is elementary (however can get more complex in real world situations).

Or am I missing something?

Collapse
macsikora profile image
Maciej Sikora Author • Edited

Yes sure, you are right. In this situation the win is arguable, as the logic is very simple. So for sure I would not make it important so much if this check happens few times, even more if we have static types to support us with this check. But if this check is notorious in the app, and if it is connected with others it is better to abstract it.

For sure examples I made in the article are very simple (maybe even too much). But if we would have some && || copy pasted, so some combined logic then such abstraction would be far more valid.

Thanks for comment!