# q11~q17 code review ## code review by Mango ### common * `askQuestion()`設計上要pure一些: ```javascript= //原寫法: import readline from "node:readline/promises"; // input setting {question, strValidators, numValidators}, validates and return userInput. export async function askQuestion(setting) { const rl = readline.createInterface({ input: process.stdin, output: process.stdout, }); const { question, strValidators, numValidators } = setting; const userInput = await rl.question(question); rl.close(); try { validator(strValidators, userInput); if (numValidators.length === 0) { //如果驗數字陣列為空 return userInput; //傳出字串 } else { //如果驗數字陣列不為空 const number = Number(userInput); validator(numValidators, number); return number; //傳出數字 } } catch (e) { console.log(e.message); return askQuestion(setting); } } function validator(validators, input) { validators.forEach((currentValid) => currentValid(input)); } ``` >我設計的`askQuestion()`得到`input`後判斷了「驗證字串」的陣列跟「驗證數字」的陣列,如果驗證數字的陣列是空的,傳出的結果會是「字串」,如果驗證數字陣列不為空,則傳出「數字」。 >但pure一點應該是都先傳出字串比較好,轉型別應該是得到結果以後再另外做。 * ~~validators可以嘗試使用`class`來做~~ >~~用想像的可能會是在每一題用`Validators`建立新的`validator`物件,然後從`validator`去呼叫裡面的`method`,ex.`validator.isNotEmpty()`、`validator.isNumber`...~~ ~~還沒想好所以暫時這樣想~~ > Chris認為over design * 設計上可以參考Titan、Serry、Angela、Vic、Ajay >Titan:對於模組設計,Titan設計了很多,不一定要全懂,其中有些概念看懂且覺得很棒就理解完後做做看自己的版本。 Sherry、Angela、Vic、Ajay:對於解題的方法邏輯,Mango比較喜歡看這些人的。 Tim:本來說Tim也可以看,解果去看他的q17發現思維很「特殊」,所以先不看 * 對於迴圈的選擇 >討論了之前`do...while`、`for`...等之間迴圈的選擇,結論還是看情境,我跟他說我覺得`for`是用在確定次數的迴圈;`while`用在不確定次數的時機,Mango也這麼認為,所以他認為我用的`do...while`沒什麼問題,(我想大概問題就是在如果你用了但你==不知道為何而用==) * 討論`return`出去的內容 >對於`return`出去的結果,分成「變數派」跟「運算派」 Mango認為多令一個變數來存運算結果通常是因為運算複雜,可能會無法快速看出這段運算到底做什麼,才會令一個名字更清楚的變數來存結果再傳出去,所以這段運算本身就很清楚他覺得就不需要 * 對於寫一段程式碼如何開始 >我自己是理解完題目後,先將題目會用到的邏輯寫出來,可能一個可能很多個,然後測試沒問題再將每個邏輯串起來,最後再將主體`main()`包好,Mango自己一開始也是這樣,他說像Chris或其他邏輯想法很完善的,一開始可能就設計架構跟大方向,考慮很多面向,但我們這個階段沒辦法也沒關係。 * 對於邏輯中需不需要驗證 >我跟她闡述了Chris的驗證garbage in一說,她也覺得OK,Mango覺得眾多學長姐中,Chris的寫測試經驗比較多,所以他對測試的想法會比較完整,Mango自己跟他看其他人在開發好像也比較少在寫測試。 話題回到邏輯中要不要驗證,當然說得通就好(Chris說的),而且==測試跟邏輯是分開的==,所以你在邏輯中加入驗證不會影響邏輯就無所謂。 ### q17 跟Mango討論出題目的目的,他認為是如何將看到的圖表用程式來解讀與實現(我的寫法很明顯沒有練到): ``` //題目 17. 運用 Borda Count。算出下面的候選人分數。 (第一順位得4分、第二順位得3分、第三順位得2分、第四順位得1分) | 51 | 5 | 23 | 21 1st | 1 | 3 | 2 | 4 2nd | 3 | 2 | 3 | 3 3rd | 2 | 4 | 4 | 2 4th | 4 | 1 | 1 | 1 ``` ```javascript= //我用人工將它加總 const candidateOne = new Candidate("No.1", 51, 0, 0, 5 + 23 + 21); const candidateTwo = new Candidate("No.2", 23, 5, 51 + 21, 0); const candidateThree = new Candidate("No.3", 5, 51 + 23 + 21, 0, 0); const candidateFour = new Candidate("No.4", 21, 0, 5 + 23, 51); ``` 所以參考了Angela、Vic、Sherry的,都是表格用不同方式切開處理。 ## Code Review by Wendy ### common * Wendy傾向一個function做一件事,裡面在做驗證他個人不喜歡 * 對於如何寫好code,多次提到了『Code Complete 2』這本書 * 什麼樣的code才叫好,他認為接手的人的觀感很重要 ### q11 * magic number:只一些沒有經過解釋莫名其妙出現的數字,例如我的第一期會費打79折,乘上了0.79,但我沒有對這個數字多另一個變數或多做解釋,就會看不懂,而且這個0.79也可能改動,所以盡量不要有magic number的出現 ### q15 * 我將器材跟重量都分開做出個別的陣列,比較散落,可以用物件包起來 * 命名可以更仔細,例如我的器材陣列叫`itemName`,`item`通常會用在array method這類的地方裡面 * code complete 2中提到function的參數不要超過7個,如果超過應該要想如何包裝分類,包成物件或陣列 ### q17 * `getMaxNumber(array){}`的array命名問題,我自己發現裡面很亂,變數命名要重新設計 * 變數`eachDistrictVotes`命名也要修正 ### validator * 驗證可看看[source code](https://github.com/logaretm/vee-validate/tree/main/packages/rules/src) 裡面將所有驗證器都個別以檔案分開,最後在`import`進`index.ts`裡 **Chris說over design哈哈哈哈** * `isNumber`這個驗證偏複雜,可以在裡面多一些註解 * 我設計`setting`這個物件給`getAnswer()`用,裡面有`strValidators`跟`numValidators`,但有時候不需要驗證數字`numValidators`這個陣列就會是空的,Wendy不習慣可能有空的陣列出現,可能可以設計成同一個驗證器陣列 * 裡面寫了`dotCode = 46`,但可以寫成`dotCode = ".".charCodeAt(0)`去存CharCode,就不用再去找也不怕寫錯代碼。 * 通常要被比較的會寫在左邊例如:`charCode < zeroCode` * 驗證function的命名也可以參考上面的[source code](https://github.com/logaretm/vee-validate/tree/main/packages/rules/src) # Code Review by Chris ## common * 設計問題:function的參數哪些是必要哪些不是,如果非必要是不是就可以給預設值,如果使用者沒給設定就用預設值去跑。 * 使用新語法要知道版本支援度,我只記得最積極的做法就是要找出整個專案的版本最低限度,node在哪個版本以上才確定一切正常,然後寫在專案的聲明之類的地方((忘記哪裡我再去問問 > 我最後一版的`getAnswer()`使用了`readline/promise`版本,至少要知道最低要求是`^18.13.0 of node` * 只要用到async/await或是promise,都要練到兩者無痛切換,因為兩者是同一個東西,一定可以切換,、以每次寫到就盡量兩種都練熟。 * 對於class概念他寫了: ```javascript const S = new N(); //class的組成,可從英文文法發想 S.V(O); //class是主詞,method是動詞 ``` > `S`是`new`一個`class N`的結果,然後S就是主詞,N裡面的method是動詞,接收的參數是受詞(可有可無)。 他認為物件導向這樣設計比較合理 ## getAnswer() * 前面提到過的使用新語法要知道版本支援度 * 我的參數`setting`是一個物件,包含`question`、`strValidators`、`numValidators`,但Chris認為只有`question`是必要的,所以另外兩個驗證器可以給預設值(後來預設是空陣列),讓使用者不需要有驗證器也能得到問題的答案: ```javascript= //原寫法 async function getAnswer(setting) { const rl = readline.createInterface({ input: process.stdin, output: process.stdout, }); const { question, strValidators, numValidators} = setting; const strInput = await rl.question(question); rl.close(); try { validator(strValidators, strInput); if (numValidators !== undefined) { const numInput = Number(strInput); validator(numValidators, numInput); } return strInput; } catch (e) { console.log(e.message); return getAnswer(setting); } } ``` ```javascript= //Chris建議 async function getAnswer(question, setting = {}) { //預設setting為空物件 const rl = readline.createInterface({ input: process.stdin, output: process.stdout, }); const { strValidators = [], numValidators = [] } = setting; //這邊預設兩個驗證器是空陣列 const strInput = await rl.question(question); rl.close(); try { validator(strValidators, strInput); const numInput = Number(strInput); validator(numValidators, numInput); return strInput; } catch (e) { console.log(e.message); return getAnswer(setting); } } ``` 然後Chris說try的範圍要包整個,我只包取得答案以後,所以改成: ```javascript= async function getAnswer(question, setting = {}) { try { const rl = readline.createInterface({ input: process.stdin, output: process.stdout, }); const { question, strValidators = [], numValidators = [] } = setting; const strInput = await rl.question(question); //promise兩種都練到 rl.close(); validator(strValidators, strInput); const numInput = Number(strInput); validator(numValidators, numInput); return strInput; } catch (e) { console.log(e.message); return getAnswer(question, setting); } } ``` ## q11 * 還是變數命名問題,用敘述的方式說明變數意義((取名子好難,誰能救我 例如這題的每期500元,要完整說明用什麼來計算價格: ```javascript const eachTermsPrice = 500; ``` 用“期數”來計算價格,每期價格 = 500。 --- 折抵用負數: ```javascript const eachFiveTermsReturn = -200; ``` 但這裡我覺得不通順,我名字都叫退回多少錢了,還退回-200,那豈不是負負得正。 ```javascript= function getTotalPriceByTerms(terms) { //計算完總價return總價 } function messageOfPriceForMember(total_price) { return `總會費為:${total_price}`; //將總價印成字串 } function countPriceAndPrint(terms) { const total_price = getTotalPriceByTerms(terms); return messageOfPriceForMember(total_price); } ``` Chris覺得這樣閱讀起來比較通順,每個function名稱也要清楚說明,本來我叫`getTotalPrice()`,但他是根據什麼來計算總價,如果沒有參數名字就看不出來,所以改成`getTotalPriceByTerms()`。 ## q13 這題就不寫哪裡要改,他直接現場展示他的想法: ```javascript= const speed_meter_per_second = 20; const back_step = -1; const back_per_second = 5; let total_step = 0; export class Rabbit { constructor() {} goOneSecond() { //動作:走一秒 total_step += speed_meter_per_second; } useProvocative(current_second) { //動作:使用挑釁 if (current_second % back_per_second) total_step += back_step; } getTotalStep() { //取得當下位置(公尺) return total_step; } } let current_second = 0; const finish_line = 1000; const rabbit = new Rabbit(); const turtle = new Turtle(); //這還沒寫,先假裝有這個class const contestants = [rabbit, turtle]; do { //背景設定好開始講故事 rabbit.useProvocative(current_second); contestants.forEach((animal) => animal.goOneSecond()); // rabbit.goOneSecond() current_second++; } while (!(rabbit.getTotalStep() >= finish_line)); contestants.some((animal) => animal.getTotalStep() >= finish_line); ``` 這邊Chris在class這個檔案中寫變數,就沒辦法從外面取得,他很自豪他超越JS語言實現真正的Private變數,因為他覺得使用者不需要知道任何動物的時速讓步等等資訊。 但實際怎麼樣等我測試後再說: ```javascript= const speed_meter_per_second = 20; const back_step = -1; const back_per_second = 5; let total_step = 0; ``` class的上方宣告了這幾個變數,這些變數理論上就只有在這個檔案才能取用,也就是其中的class。 --- 然後這題他不考慮後退的時間,直接設定兔子5秒往後瞬移1公尺 `do`開始後: ```javascript rabbit.useProvocative(current_second); //兔子先使用挑釁,但只有當秒數為5的倍數才會成功使出 contestants.forEach((animal) => animal.goOneSecond()); //接著每隻動物都跑一秒 ``` Chris說這裡`contestants.forEach(...)`是『替代』,他可能想說借代修辭吧,就是本來平鋪直敘的敘述他用更簡單的方式代換,原本應該是: ```javascript rabbit.goOneSecond(); turtle.goOneSecond(); //然後如果有其他種動物就接下去 ``` 然後寫完就會發現能將它們做成array然後用forEach替代 --- 最後只要有任何一種動物通過終點就停止之類的: ```javascript while(contestants.some((animal) => animal.getTotalStep() >= finish_line)){...} ``` 以上程式碼都是想像後寫出來,還沒有實測過,所以主要只是分享概念,等我之後成功做出來再跟大家分享 ## q14 寫得太簡短不想給他看結果不小心被看到,一樣是magic number的問題,20分鐘成長兩倍的20跟2沒有說明清楚,如果是Chris他就會註解寫『橫空出世的20跟2』,或者『20跟2是三小拉』,這樣就會知道沒有解釋的數究竟有多可怕。 ## q17 思考一下建構class物件的時候,需要去思考哪些是必要參數,才需要放在constructor內,其他不必要參數就設定預設值,直到使用者想設定再覆蓋過去,例如:weights、orderTable這兩個參數,就台灣的選舉法,不會有這兩個變因,一人一票沒有順位問題,所以也沒有權重,所以預設就是沒有這兩個也能跑,直到使用者想跑Borda Count時,才需要這兩個要素,這時候再提醒使用者要設定後才能使用就好。 ```javascript= //原寫法 import { Election } from "./classElection.js"; export function useBordaCountAndPrintResult( numberOfCandidates, eachDistrictVotes, weights, orderTable ) { const vote = new Election( numberOfCandidates, eachDistrictVotes, weights, orderTable ); const votesResultMessage = vote .useBordaCount() .map((currentPoints, index) => { return `${index + 1} 號候選人得票數:${currentPoints}`; }) .join("\n"); const winnerInfo = vote.getBordaCountWinner(); const winnerMessage = `${winnerInfo.index .map((element) => element + 1) .join(", ")} 號獲勝,得票數:${winnerInfo.maxNumber}`; //也可做成method return { votesResultMessage, winnerMessage }; } ``` --- 以下是我想像,還沒實測過: ```javascript= //Chris建議 import { Election } from "./classElection.js"; export function useBordaCountAndPrintResult(vote) { //參數直接丟vote這個繼承Election類別的物件 vote.setWeights(weights); //權重這個參數必要嗎?如果非必要是不是在這邊設定就好,不用在建構的時候就列進去,看不懂來問我 vote.setOrderTable(orderTable); //順位選票表格也是,沒有必要就不放參數,在這邊設定就好 vote.useBordaCount(); //用Borda Count算 const votesResultMessage = vote.getMessageOfBordaCountResult(); //印出結果字串 vote.getBordaCountWinner(); //判斷贏家 const winnerMessage = vote.getWinnerMessage(); //印出贏家字串 return { votesResultMessage, winnerMessage }; } ``` ```javascript= //將印出結果這個行為也做成method class Election { constructor(numberOfCandidates, eachDistrictVotes) { this.numberOfCandidates = numberOfCandidates; this.eachDistrictVotes = eachDistrictVotes; //... } getMessageOfBordaCountResult() { this.bordaCountResult.map((currentPoints, index) => { return `${index + 1} 號候選人得票數:${currentPoints}`; }) .join("\n"); } getWinnerMessage() { return `${this.winnerInfo.index .map((element) => element + 1) .join(", ")} 號獲勝,得票數:${this.winnerInfo.maxNumber}` } } ``` 再來就是測試,改太多就來不及寫報錯的測試(garbage in)... Chris說從第一題寫到十七題還是沒有寫((笑 你越笑我越慌 但我有寫報錯拉,只是忘了寫到測試裡((汗 這邊的測試可能要寫的內容有: * 如果參選者數量跟順位數量不同要報錯 * 如果orderTable表格跟各選區票數對不起來也報錯 * 順位數量跟各權重數量不同報錯 * 還沒給weights跟orderTable就想執行useBordaCount()報錯 * 再來可能要測都沒有順位(台灣投票算法) * 最後當然就側Borda Count算法