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} />
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 />
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" />
We can go forward and make other kinds of errors:
const PermissionError = () => <Error message="No permission" />
const AuthError = () => <Error message="Not authenticated" />
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)
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);
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);
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 takesuser
as argument and has pre-populated group already. In FP terms we would sayisGroupMember
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)
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
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
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?
Top comments (3)
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?
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!
Nice article! I am really intrigued by the usage of currying. Could you recommend more posts/articles/books about applying FP in JavaScript development?