The Case for Writing the Same Logic Twice
I love DRY-ing (Don’t Repeat Yourself) repetitive code into reusable chunks. In many cases, the codebase becomes easier to read and maintain as changes only need to be made in one place. However, there are instances where the keen developer’s urge to DRY up the code can result in overly complicated logic.
One way this can happen is with premature optimisation. Suppose you write a function and see a great section of code that could be abstracted into a utility:
// Original repetitive code
const user1FullName = `${user1.firstName} ${user1.lastName}`;
const user2FullName = `${user2.firstName} ${user2.lastName}`;
// A keen developer DRYs it up
function formatFullName(user: { firstName: string; lastName: string }) {
return `${user.firstName} ${user.lastName}`;
}
const user1FullNameDRY = formatFullName(user1);
const user2FullNameDRY = formatFullName(user2);
You think to yourself: “this is going to be super useful next time we need to do this someplace else”.
Another dev comes along and needs to add a similar, but subtly different, feature. Your teammate feels honour-bound not to remove your utility, and is probably pressed for time in any case, so adds an if block within it to support the new feature.
// Existing utility
function formatFullName(user: { firstName: string; lastName: string; middleName?: string }) {
if (user.middleName) {
return `${user.firstName} ${user.middleName} ${user.lastName}`;
}
return `${user.firstName} ${user.lastName}`;
}
// They feel obliged to keep using it, so add the if block
const user3FullName = formatFullName({ firstName: "Alice", middleName: "M.", lastName: "Smith" });
A third dev is asked to add a different feature that overlaps somewhat with the first two features. This subsequent edit to the utility requires yet another if block to be added.
function formatFullName(
user: { firstName: string; lastName: string; middleName?: string },
style: "normal" | "lastFirst" = "normal"
) {
if (style === "lastFirst") {
return user.middleName
? `${user.lastName}, ${user.firstName} ${user.middleName}`
: `${user.lastName}, ${user.firstName}`;
}
// original formatting
return user.middleName
? `${user.firstName} ${user.middleName} ${user.lastName}`
: `${user.firstName} ${user.lastName}`;
}
Soon the utility is overly complex and is doing three (or more) separate tasks. In hindsight, it may have been better not to add it and prefer the duplication. Maybe a different abstraction would have presented itself that could have made more sense.
A good rule of thumb to limit this problem is this: have you implemented a feature three times? If only once or twice, just duplicate the logic. But by the third time, you’ll have a pretty good idea of which logic can be shared and which can’t be. This is a good point to make the utility.
Suppose you’ve found yourself in a situation where there are some very unhelpful abstractions in your app. What do you do? I recently read this blog which I found helpful on this topic. My process now is:
- Delete the abstraction by moving the code back to each place the abstraction was used (ie, duplicate it).
- Refactor each place the logic was duplicated
- Build new abstractions with the simplified code, if it makes sense to do so
TLDR: Not everything in programming needs to be DRY (don’t repeat yourself). Repeating yourself is fine if the alternative is too complex.