Why Interviewers Use the FizzBuzz Problem
FizzBuzz is a well-known, simple coding assignment that is commonly used as an entry, technical whiteboard exercise in interviews.
Since the problem is "simple", the focus isn't on whether or not someone can solve it, but how one goes about solving problems and gives that developer a chance to showcase their process on writing maintainable code as a member of a team.
Bad interviewers only care whether or not the interviewee can actually solve the problem. Good interviewers are looking out for strong coding habits:
- Writing self-documenting/commented code
- Naturally writing/refactoring code to be DRY
- Deliver code that can be easily maintained
Let's go over some solutions for the FizzBuzz problem and highlight bad patterns to avoid in an interview.
We'll also highlight good alternatives that demonstrate an understanding on how to write maintainable code.
The FizzBuzz Problem
There are many variations of this problem, we'll be using:
Write a function that accepts one argument: Array of possible whole numbers.
The return of the function should map each value of the Array to some string given these cases:
1. If the number is divisible by 3, map to "Fizz"
2. If the number is divisible by 5, map to "Buzz"
3. If the number is divisible by 3 and 5, map to "Fizzbuzz" instead
4. If the number is NOT divisible by 3 or 5, map number to itself
I've always hated how the instructions for displaying "Fizzbuzz" came somewhere in the middle or the end when in most cases displaying "Fizzbuzz" handled first. I would suggest clarifying that order matters and request that the requirements reflect it:
1. If the number is divisible by 3 and 5...
2. If the number is divisible by 3...
3. If the number is divisible by 5...
4. Else...
I believe exercises start with badly ordered requirements on purpose to simulate the real world: you're likely to get badly ordered requirements. In this situation, it's important for developers to be proactive.
Solutions to the FizzBuzz Problem
Here is a clever solution to the problem: [01]
/**
* Map an Array of numeric values to Fizzbuzz values
* @param values Array of numeric values
* @returns Fizzbuzz values
*/
export const getFizzbuzzValues = (values: number[]): string[] => {
let results = [];
for (let i = 0; i < values.length; i++) {
results.push(
(!(values[i] % 3) && ((!(values[i] % 5) && 'Fizzbuzz') || 'Fizz')) || (!(values[i] % 5) && 'Buzz') || `${values[i]}`
);
}
return results;
};
If this was an open PR, I'd suggest a refactor that made the code changes more clear on what's going on. One-liner solutions are all fun and games until someone reports a bug and I have to cypher all this madness.
Here's a quick refactor to break down the one-liner solution: [02]
/**
* Map an Array of numeric values to Fizzbuzz values
* @param values Array of numeric values
* @returns Fizzbuzz values
*/
export const getFizzbuzzValues = (values: number[]): string[] => {
let results = [];
for (let i = 0; i < values.length; i++) {
if (!(values[i] % 3)) {
if (!(values[i] % 5)) {
results.push('Fizzbuzz');
} else {
results.push('Fizz');
}
} else {
if (!(values[i] % 5)) {
results.push('Buzz');
} else {
results.push(`${values[i]}`);
}
}
}
return results;
};
I'm happier with this solution since it is more readable. I value readability over optimization every time since when trying to achieve both, it's always easier to refactor readable code. To further improve the readability of the code, I'd then suggest avoiding nested if statements: [03]
/**
* Map an Array of numeric values to Fizzbuzz values
* @param values Array of numeric values
* @returns Fizzbuzz values
*/
export const getFizzbuzzValues = (values: number[]): string[] => {
let results = [];
for (let i = 0; i < values.length; i++) {
if (!(values[i] % 3) && !(values[i] % 5)) {
results.push('Fizzbuzz');
continue;
}
if (!(values[i] % 3)) {
results.push('Fizz');
continue;
}
if (!(values[i] % 5)) {
results.push('Buzz');
continue;
}
results.push(`${values[i]}`);
}
return results;
};
Now I'm happy, but it could be better. One big problem is that this code is imperative. Since we know that there is a direct mapping of values of the Array. Why not use Array.map
? [04]
/**
* Map an Array of numeric values to Fizzbuzz values
* @param values Array of numeric values
* @returns Fizzbuzz values
*/
export const getFizzbuzzValues = (values: number[]): string[] => {
return values.map(value => {
if (!(value % 3) && !(value % 5)) {
return 'Fizzbuzz';
}
if (!(value % 3)) {
return 'Fizz';
}
if (!(value % 5)) {
return 'Buzz';
}
return `${value}`;
});
};
Now I'm very happy: code is declarative. Any seasoned developer should understand what this code is doing at a glance.
But if we wanted to consider some optimizations, !(value % 3)
is done twice. And there could be minor confusion on how the remainder operator works to determine if something is divisible by another number: [05]
/**
* Checks if a number (dividend) is divisible by another number (divsor)
* @param dividend - number being checked if divisible
* @param divisor {number} - what is used to determine if number is divisible by
* @returns number is divisible
*/
export const isDivisible = (dividend: number, divisor: number): boolean => {
// modulo operation will return 0 if there's no remainders => divisible
return !(dividend % divisor);
};
/**
* Map an Array of numeric values to Fizzbuzz values
* @param values Array of numeric values
* @returns Fizzbuzz values
*/
export const getFizzbuzzValues = (values: number[]): string[] => {
return values.map((value) => {
const divisibleByThree = isDivisible(value, 3);
const divisibleByFive = isDivisible(value, 5);
if (divisibleByThree && divisibleByFive) {
return 'Fizzbuzz';
}
if (divisibleByThree) {
return 'Fizz';
}
if (divisibleByFive) {
return 'Buzz';
}
return `${value}`;
});
};
Now I'm pretty happy. There should be a very clear understanding on what each part does. Now there's enough information for even those who don't know how the remainder operator works.
Only thing I'd suggest is that we migrate the logic for getting a "Fizzbuzz value" to its own function since determining a "Fizzbuzz value" isn't dependent on the Array of values as a whole, but instead depends on each specific value.
Also, how do we explain that the order of the if statements matter: write some comments! [06]
/** fizz-buzz.ts */
/**
* Checks if a number (dividend) is divisible by another number (divsor)
* @param dividend - number being checked if divisible by some other number (divsor)
* @param divisor {number} - what is used to determine if a number (dividend) is divisible
* @returns number (dividend) is divisible
*/
export const isDivisible = (dividend: number, divisor: number): boolean => {
return !(dividend % divisor);
}
/**
* Get a Fizzbuzz value based on a numeric value
* @param value Numeric value
* @returns Fizzbuzz value
*/
export const getFizzbuzzValue = (value: number): string => {
const divisibleByThree = isDivisible(value, 3);
const divisibleByFive = isDivisible(value, 5);
// Check if value is divisible by 3 and 5 first
if (divisibleByThree && divisibleByFive) {
return 'Fizzbuzz';
}
// It is important to check if number is divisible 3 after checking if divisible by 3 and 5
if (divisibleByThree) {
return 'Fizz';
}
// It is important to check if number is divisible 5 after checking if divisible by 3 and 5
if (divisibleByFive) {
return 'Buzz';
}
// Default case: return numeric value as a string
return `${value}`;
};
/**
* Map an Array of numeric values to Fizzbuzz values
* @param values Array of numeric values
* @returns Fizzbuzz values
*/
export const getFizzbuzzValues = (values: number[]): string[] => {
return values.map(value => getFizzbuzzValue(value));
};
And how do I ensure that the implementation actually works: write some tests! [07]
/** fizz-buzz.spec.ts */
import * as lib from './fizz-buzz';
describe('fizz-bizz', () => {
describe('isDivisible()', () => {
it('should return false if NOT divisible by', () => {
expect(lib.isDivisible(1, 3)).toBe(false);
expect(lib.isDivisible(2, 3)).toBe(false);
});
it('should return true if divisible by', () => {
expect(lib.isDivisible(3, 3)).toBe(true);
});
});
describe('getFizzbuzzValue()', () => {
it('should return Fizzbuzz if divisible by 3 and 5', () => {
expect(lib.getFizzbuzzValue(30)).toBe('Fizzbuzz');
});
it('should return Fizz if divisible by 3 and NOT 5', () => {
expect(lib.getFizzbuzzValue(27)).toBe('Fizz');
});
it('should return Fizz if divisible by 5 and NOT 3', () => {
expect(lib.getFizzbuzzValue(35)).toBe('Buzz');
});
it('should return input number if NOT divisible by 5 or 3', () => {
expect(lib.getFizzbuzzValue(4)).toBe('4');
});
});
describe('getFizzbuzzValues()', () => {
beforeEach(() => {
jest.restoreAllMocks();
});
it('should use getFizzbuzzValue to create its return value', () => {
const spy = jest
.spyOn(lib, 'getFizzbuzzValue')
.mockImplementation((value: number) => { return `${value}_mock`});
expect(lib.getFizzbuzzValues([9, 35, 30, 101])).toStrictEqual([
'9_mock',
'35_mock',
'30_mock',
'101_mock',
]);
expect(spy).toHaveBeenNthCalledWith(1, 9);
expect(spy).toHaveBeenNthCalledWith(2, 35);
expect(spy).toHaveBeenNthCalledWith(3, 30);
expect(spy).toHaveBeenNthCalledWith(4, 101);
});
it('should map values of the input array to Fizzbuzz values', () => {
expect(lib.getFizzbuzzValues([9, 35, 30, 101])).toStrictEqual([
'Fizz',
'Buzz',
'Fizzbuzz',
'101',
]);
});
});
});
Awesome, now I'm super happy:
- We have comments to show that the order matters.
- Our code is fully tested to help ensure it doesn't break.
- Code is very readable and declarative.
Maybe there's even more improvements you'd suggest. Or maybe you'd prefer different test cases or patterns to be used. Please leave a comment to share your thoughts!
Conclusion
Good interviewers don't only care whether or not the developer can find a solution to a problem.
They want to know if a developer can ask the right questions to clarify requirements and to convey they understand the problem. They care about hiring developers that can deliver maintainable code.
In an interview setting where time is a critical factor. We might not have enough time to go over everything we've discussed in this post:
- Avoiding complex one-liner solutions
- Avoiding nested if statements to make code more readable
- When to use comments
- Moving duplicate code into separate functions
- Writing declarative code over imperative code
- Writing tests to ensure code is working as expected
That's okay! Discussion is an important part of technical interviews. Use discussion to save time and demonstrate an understanding of these concepts.
Wanna learn more about imperative vs declarative code? Check out this informative post.
Top comments (0)