I have some beliefs on programming.
I always re-write, re-think, research, re-invent and refactor.
That did cost me time in the beginning but now, it's not a problem.
Whilst doing that learned how and why things work in JavaScript.
I never settle, even if something works.
Everything matters, function names, variable names, even prop sorting.
I was doing a code review and reached to a function.
I need to mention here that refactoring took me no more than five minutes.
The function was responsible for formatting a given address depending on its country code.
E.g.
const address = {
city: "Belgreen",
countryCode: 'us',
county: null,
state: "Vermont",
suburb: null,
zip: "4636"
}
The implementation was the following.
const addressTextView = () => {
const city = address.city;
const state = address.state;
const zip = address.zip;
const suburb = address.suburb;
const county = address.county;
switch (address.countryCode) {
case 'uk':
return (
(zip != null ? zip + ', ' : '') +
(suburb != null ? suburb + ', ' : '') +
(city != null ? city + ', ' : '') +
(state != null ? state : '')
);
case 'us':
return (
(city != null ? city + ', ' : '') +
(state != null ? state + ', ' : '') +
(zip != null ? zip : '')
);
case 'nz':
return (
(zip != null ? zip + ', ' : '') +
(city != null ? city + ', ' : '') +
(county != null ? county + ', ' : '') +
(state != null ? state : '')
);
default:
return (
(zip != null ? zip + ', ' : '') +
(suburb != null ? suburb + ', ' : '') +
(state != null ? state + ', ' : '') +
(county != null ? county : '')
);
}
}
The first thing that bothered me was the ternaries in each case.
Then the repetition.
I started thinking of every return as an array of keys in different order.
I didn't care about null values.
I just started creating patterns.
//
switch (address.countryCode) {
case 'uk':
return [zip, suburb, city, state].join(', ');
case 'us':
return [city, state, zip].join(', ');
case 'nz':
return [zip, city, county, state].join(', ');
default:
return [zip, suburb, state, county].join(', ');
//
Then, I saw the pattern and I created a function to handle each return.
const joinStrings = (...args) => args.join(', ');
And the switch looked like this.
//
switch (address.countryCode) {
case 'uk':
return joinStrings(zip, suburb, city, state);
case 'us':
return joinStrings(city, state, zip);
case 'nz':
return joinStrings(zip, city, county, state);
default:
return joinStrings(zip, suburb, state, county);
//
Then I did something that amazed some people.
The way I filtered out null values from each array.
const joinStrings = (...args) => args.filter(Boolean).join(', ');
And final code was this.
const joinStrings = (...args) => args.filter(Boolean).join(', ')
const formatAddress = ({ city, county, countryCode, state, suburb, zip }) => {
switch (countryCode) {
case 'uk':
return joinStrings(zip, suburb, city, state);
case 'us':
return joinStrings(city, state, zip);
case 'nz':
return joinStrings(zip, city, county, state);
default:
return joinStrings(zip, suburb, state, county);
}
};
My thoughts.
Both functions work. Business is happy.
It's OK to deliver it, but...
always try to improve and never settle if something works.
We had a function called addressTextView
which was not very clear what it does. It also used the address object from the parent scope.
Then we had a lot of logic with ternaries which where not very clear to read at first either.
I renamed the function to formatAddress
to be clear and passed the address object as an argument.
I isolated the logic to a different function named joinStrings
. That function is independent from formatAddress
and can be reused if necessary.
We also went from 45 lines of code down to 13. 😍
That's it.
I am not bragging, I'm trying to say it doesn't matter if it works but if you want to learn and grow as a developer there are a lot of ways to do that.
Top comments (30)
Great post, I like how you took the time to explain step by step.
I'd suggest also the following change to make the "business rules" clearer by extracting them from the formatting logic:
Unless formatMap is something that needs to be dynamic, I suggest not to do this.
This mixes up type and data and also smells like premature abstraction.
this is also what i like to do whenever a switch case appears
Actually I’ve just written a post about this kind of pattern, if you’d like to take a look:
dev.to/tomazlemos/keeping-your-cod...
Thanks for sharing this. 🙏
Well one way or another the
formatAddress
function needs to know the business rules. If I went with your approach I would pass the rules as an argument.This is the final image in a magneto_perfection.jog meme. I love it.
this is nice.. considering formatMap value can be saved in config file or database. Its easier to scale.
I’d never seen this pattern in JS, looks just as nice as in Java 👍🏼👍🏼
Thanks for posting Ioannis, good job!
You can take this to the next level with Replace conditional with Polymorphism. Even though you have successfully improved the code in this example, every new format will require you to modify this algorithm.
Great! Thanks for sharing.
const { city, state, zip, suburb, county } = address;
Literally had to 😂
I gotta say. The refactor looks good, but the lack of consistency in the order of your arguments really triggers me.
While you are mapping the args, the order doesn't matter in terms of working code, but it could be a wee bit more readable to the human eyes that work with the code. ;)
Hmm, I don't know if you're talking about
joinStrings
function which in this case the order of the arguments does matter. On the other hand, I could simply pass a single array argument which was my first attempt.I like the refactor in general, but I'm not 100% sure I'm on board with the joinStrings function as opposed to a .join().filter() in each switch case. Seems like maybe more a case of abstraction for abstraction's sake than valuable code reuse.
I'd also maybe prefer a more explicit null check (still using the filter method) for clarity sake. But that may be just be preference.
All that aside, the simplification from all the conditional checks and concatenations to a single join and filter of an array is spot on. 👍
Hi Ioannis, I think you cleaned very well the code, but it is still rigid an fragile. For example, try to change the way the message is formated in some countries, like using dots for some properties instead of comas, or try to add a new field to every country. In every case you must change the written code breaking with the SOLID's open-closed principle.
The source of evil here is the switch, your function formatAddress has two responsabilities, (1) to determine what country code has the adress object, and (2) choose the properties to be passed to joinStrings for each case.
An easy pattern that bring us some flexibility in these cases is the chain of responsabilities. If you dislike this approach you have others more functional but the responsabilities coupled there are a big issue from my point of view, because limits the extention of features that has a lot of probabilities to change.
Btw, all of I'm saying here do not take away the fact that you have done an awesome job until here!
Sorry for the bad english,
Have fun!
You're totally correct.
The format and number of countries we serve is fixed so I didn't take into consideration these principles. I should but this was a function with a specific purpose and posted this as an example for cleaner code. Thank you for your feedback and don't worry for your English. 😀
Code is art! Keep refactoring, striving for beauty. Simplify as much as possible but no more.
Beautiful solution ;)
Excellent post, thanks.
Please remember that zip is called "postal code" everywhere outside of US
Tell that to my back end! 😍
Some comments may only be visible to logged-in visitors. Sign in to view all comments.