前言
FizzBuzz 大概是最廣為人知的程式設計題目之一,它的題目是這樣的:
- 打印出
1
到100
的數字- 假如數字是
3
的倍數,則打印Fizz
- 假如數字是
5
的倍數,則打印Buzz
- 假如數字是
3
和5
的公倍數,則打印FizzBuzz
- 假如數字是
解題
基本解
最直接的解法即是製作一個函式會打印 1 ~ 100
的數字,並依照規則判斷是否要打印 Fizz
、Buzz
或 FizzBuzz
。但明顯這樣的做法會將所有數值都寫死在程式當中,未來要擴充條件時就需要修改程式碼,尚不夠靈活。
function fizzBuzz(max) { for (let i = 1; i <= max; i++) { if (i % 3 === 0 && i % 5 === 0) { console.log('FizzBuzz'); } else if (i % 3 === 0) { console.log('Fizz'); } else if (i % 5 === 0) { console.log('Buzz'); } else { console.log(i); } }}
fizzBuzz(100);
分離資料與邏輯
既然可以預期規則都是 某數字
要打印 某結果
那麼或許可以使用物件來紀錄這些 Key-Value 結構的資料:
function fizzBuzz(max) { const map = { 3: 'Fizz', 5: 'Buzz', 15: 'FizzBuzz', };
for (let i = 1; i <= max; i++) { if (i % Object.keys(map)[0] === 0 && i % Object.keys(map)[1] === 0) { console.log(Object.values(map)[2]); } else if (i % Object.keys(map)[0] === 0) { console.log(Object.values(map)[0]); } else if (i % Object.keys(map)[1] === 0) { console.log(Object.values(map)[1]); } else { console.log(i); } }}
資料是抽離出來了,但邏輯還是依賴指定 map
物件當中的特定內容,讓我們再寫個迴圈自動的將 map
物件的內容取出來,讓資料來驅動邏輯:
function fizzBuzz(max) { const map = { 3: 'Fizz', 5: 'Buzz', };
for (let i = 1; i <= max; i++) { let output = ''; for (const key in map) { if (i % key === 0) { output += map[key]; } } console.log(output || i); }}
保持數值不變(Immutable)
以上解方可以觀察到目前定義了 i
以及 output
兩個變數並且於程式中持續的變動其內容,在某些程式開發風格當中會被視為應當避免的習慣,我們可以嘗試看看將複寫變數的部分改為純粹函式。
numberReplacer(100, { 3: 'Fizz', 5: 'Buzz',}).forEach((value) => console.log(value));
function numberReplacer(max, replacementMap) { return Array.from({ length: max }, (_, index) => { const currentNumber = index + 1; const replacement = Object.entries(replacementMap).reduce( (acc, [divisor, replaceWord]) => (currentNumber % divisor === 0 ? acc + replaceWord : acc), '', ); return replacement || currentNumber; });}
補齊文件與檢查邊界案例
JavaScript 是動態型別語言,因此其他人在使用這個函式時可能不清楚具體參數的需求,這裡透過 JSDoc 來補齊文件,也可以考慮使用 TypeScript 來檢測型別或於 Runtime 進行型別檢測:
/** * 生成一陣列,將 1 到 max 之間的數字取代為指定字串 * * @param {number} max - 最大數值 * @param {Object<number, string>} replacementMap - key 為除數,value 為取代字串 * @returns {Array<string | number>} - 取代後的數值 * @throws {TypeError} 如 max 不是正整數 * @throws {TypeError} 如 replacementMap 不是非空物件 * @throws {TypeError} 如 replacementMap 內的 key 不是是正整數 * @example * numberReplacer(15, { 3: 'Fizz', 5: 'Buzz' }); * // Returns [1, 2, 'Fizz', 4, 'Buzz', 'Fizz', 7, 8, 'Fizz', 'Buzz', 11, 'Fizz', 13, 14, 'FizzBuzz'] */function numberReplacer(max, replacementMap) { if (!Number.isInteger(max) || max <= 0) { throw new TypeError('max 必須是正整數'); }
if (typeof replacementMap !== 'object' || replacementMap === null) { throw new TypeError('replacementMap 必須是非空物件'); }
return Array.from({ length: max }, (_, index) => { const currentNumber = index + 1; const replacement = Object.entries(replacementMap).reduce((acc, [divisor, replaceWord]) => { const numDivisor = Number(divisor); if (!Number.isInteger(numDivisor) || numDivisor <= 0) { throw new TypeError('replacementMap 內的 key 必須是正整數'); } return currentNumber % numDivisor === 0 ? acc + replaceWord : acc; }, ''); return replacement || currentNumber; });}
try { numberReplacer(100, { 3: 'Fizz', 5: 'Buzz', }).forEach((value) => console.log(value));} catch (error) { console.error(error.message);}
藉由局部應用來簡化 numberReplacer
你可能會想說:老天!只是要印個 FizzBuzz
為什麼每次都要透過 numberReplacer
來計算?有沒有辦法單純創個函式只傳入最大值就好?就像一開始一樣?讓我們用局部應用函式來透過創造抽象達成簡化:
const createFizzBuzz = (max) => { return numberReplacer(max, { 3: 'Fizz', 5: 'Buzz', });};const fizzBuzz100 = createFizzBuzz(100);fizzBuzz100.forEach((value) => console.log(value));
const fizzBuzz15 = createFizzBuzz(15);fizzBuzz15.forEach((value) => console.log(value));
在這次解題過程中我貫徹 DRY 原則,並且透過不斷的重構來提升程式碼的可讀性與可維護性,但也可以思考真的有必要製造更多抽象嗎?過早的最佳化是萬惡之源,或許 YAGNI。