張凱威
    • Create new note
    • Create a note from template
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Write
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Engagement control Commenting, Suggest edit, Emoji Reply
    • Invite by email
      Invitee

      This note has no invitees

    • Publish Note

      Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

      Your note will be visible on your profile and discoverable by anyone.
      Your note is now live.
      This note is visible on your profile and discoverable online.
      Everyone on the web can find and read all notes of this public team.
      See published notes
      Unpublish note
      Please check the box to agree to the Community Guidelines.
      View profile
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Note Insights
    • Engagement control
    • Transfer ownership
    • Delete this note
    • Save as template
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Versions and GitHub Sync Note Insights Sharing URL Create Help
Create Create new note Create a note from template
Menu
Options
Engagement control Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Write
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Engagement control Commenting, Suggest edit, Emoji Reply
  • Invite by email
    Invitee

    This note has no invitees

  • Publish Note

    Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

    Your note will be visible on your profile and discoverable by anyone.
    Your note is now live.
    This note is visible on your profile and discoverable online.
    Everyone on the web can find and read all notes of this public team.
    See published notes
    Unpublish note
    Please check the box to agree to the Community Guidelines.
    View profile
    Engagement control
    Commenting
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    • Everyone
    Suggest edit
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    Emoji Reply
    Enable
    Import from Dropbox Google Drive Gist Clipboard
       owned this note    owned this note      
    Published Linked with GitHub
    Subscribed
    • Any changes
      Be notified of any changes
    • Mention me
      Be notified of mention me
    • Unsubscribe
    Subscribe
    <style> .h1 { font-size: 64px; font-weight: 700; } .h2 { font-size: 48px; font-weight: 700; } .h3 { font-size: 36px; font-weight: 700; } .h4 { font-size: 24px; font-weight: 700; } .p { font-size: 20px; } .fixed { position: fixed; } .markdown-body b { font-weight: 700; } .left { text-align: start; margin-left: 28px; padding-top: 16px; } .ol { font-size: 20px; margin: 0 !important; } .ol li { margin: 8px 0; /* 5px top and bottom, 0 left and right */ } .markdown-body code, code { font-size: 16px !important; color: #99999 !important; background-color: #4A4A4A; } </style> <div style="position: relative; min-width: 1080px; min-height: 320px;"> <p class="h1 fixed" style="top: 180px; left: 50%; transform: translateX(-50%); width:1080px;" >Grokking Simplicity FP CH4-5</p> <p class="h4 fixed" style="right: 0px; bottom: -140px;" >Presenter : Steven Chang</p> <p class="h4 fixed" style="right: 82px; bottom: -180px;" >Date : 2025/04/17</p> </div> --- <p class="h2"> 前次讀書會回顧 </p> <div class="left"> <p class="p">1. Side Effect 定義:一個函數在計算過程中除了返回一個值之外,還對外部產生了影響,簡單來說,如果一個函數在運算時除了計算結果外,還改變了其他部分的狀態,那麼這個函數就有副作用。</p> <p class="p">2. 實務中 FP 仍需要 Side Effect,這些 Side Effect 是我們使用軟體的根本原因</p> <p class="p">3. Actions: 執行結果會因為不同時間點或次數而有所不同 </p> <p class="p">4. Calculations: 不管在何時或執行多少次,只要輸入不變,輸出都會相同 </p> <p class="p">5. Data: 資料可紀錄與事件有關的事實 </p> <p class="p">6. 對 FP Programmer 來說,在程式碼中分辨 Actions、Calculations 與 Data 是自然且重要的 </p> <p class="p">7. FP Programmers 喜好 Data > Calculations > Actions,因為 Actions 在不同的時間跟執行次數會產生不同的結果,所以盡量將 Action 拆分為 Calculation 或 Data,可以降低程式執行中的不確定性,尤其是在複雜的分散式系統架構下。</p> </div> --- <p class="h3"> Actions </p> <div class="left" style="margin-left: 48px;"> <p class="p">執行結果會因為不同時間點或次數而有所不同</p> <p class="p">例子:擀麵團、送披薩、進原料</p> </div> ```javascript sendEmail(to, from, subject, body) // 寄送電子郵件 saveUserDB(user) // 儲存資料到資料庫 getCurrentTime() // 每次呼叫,傳回的時間都不一樣 ``` --- <p class="h3"> Calculations </p> <div class="left" style="margin-left: 48px;"> <p class="p">不管在何時或執行多少次,只要輸入不變,輸出都會相同</p> <p class="p">例子:準備食譜、列採購清單</p> </div> ```javascript sum(numbers) // 快速求出一串數字的總和 string_length(str) // 輸入相同字串,回傳的字串長度值永遠相同 ``` --- <p class="h3"> Data </p> <div class="left" style="margin-left: 48px;"> <p class="p">資料紀錄與事件有關的事實</p> <p class="p">例子:庫存清單、披薩食譜、點餐資訊、收據</p> </div> ```javascript {"firstname":"Eric", "lastname": "Normand"} // 關於某人的資訊 [1, 10, 2, 45, 3, 98] // 單純的數字串列 ``` --- <p class="h2"> 第四章 擷取 Actions 函式中的 Calculations </p> --- <p class="h3"> 本章著重於從 Actions 中抽離出 Calculations </p> <p class="h4"> 以 MegaMart 線上商店作為本章範例 </p> --- <p class="h3"> 第一段程式碼:更新購物車金額 </p> ```javascript var shopping_cart = []; // 全域變數 var shopping_cart_total = 0; // 全域變數 function add_item_to_cart(name, price) { shopping_cart.push ({ // 加入購物車 name: name, price: price }); calc_cart_total(); // 計算總金額 } ``` ```javascript function calc_cart_total() { shopping_cart_total = 0; for(var i = 0; i < shopping_cart.length; i++) { var item = shopping_cart[i]; shopping_cart_total += item.price ; // 加總購物車金額 } set_cart_total_dom(); // 更新 DOM 來顯示最新的價格 } ``` --- <p class="h4"> 第二段程式碼:確認是否免運</p> ```javascript function update_shipping_icons() { var buy_buttons = get_buy_buttons_dom(); // 取得所有的購買按鈕,然後遍歷他們 for(var i = 0; i < buy_buttons.length; i++) { var button = buy_buttons[i]; var item = button.item; if(item.price + shopping_cart_total >= 20) // 判斷是否符合免運的條件 button.show_free_shipping_icon(); // 顯示圖示 else button.hide_free_shipping_icon(); // 隱藏圖示 } } ``` <p class="p"> 在 <code>calc_cart_total()</code> 呼叫這個新函式,每當購物總金額改變時,會更新所有的圖示</p> ```javascript function calc_cart_total() { // 與之前相同的函數 shopping_cart_total = 0; for(var i = 0; i < shopping_cart.length; i++) { var item = shopping_cart[i]; shopping_cart_total += item.price; } set_cart_total_dom(); update_shipping_icons(); // 新增一行,用來更新圖示 } ``` --- <p class="h4"> 第三段程式碼:計算稅金</p> ```javascript function update_tax_dom() { // ❶ 更新稅金的函式 set_tax_dom(shopping_cart_total * 0.10); // ❷ 更新 DOM,將總額乘 10% } ``` <p class="p"> 在 <code>calc_cart_total()</code> 計算出新的購物車總額後,再呼叫這個函式</p> ```javascript function calc_cart_total() { shopping_cart_total = 0; for(var i = 0; i < shopping_cart.length; i++) { var item = shopping_cart[i]; shopping_cart_total += item.price; } set_cart_total_dom(); update_shipping_icons(); update_tax_dom(); // ❶ 更新頁面上的稅額 } ``` --- <p class="h3"> 我們需要使程式碼更容易測試 </p> <p class="h4" style="margin-left: 40px; text-align:left"> 程式碼中包含一些不易測試的業務規則 (business rule) </p> <div class="left"> <p class="p" style="margin-left: 16px;">每次程式碼改動時,都得寫一個測試來確認下列步驟:</p> <ol class="ol"> <li>設定瀏覽器</li> <li>點擊按鈕將商品加入購物車</li> <li>等待 DOM 更新</li> <li>從 DOM 擷取出值</li> <li>將字串解析成數字</li> <li>將結果與預期值做比對</li> </ol> </div> <div class="left"> <p class="p" style="margin-left: 16px;">為了能更輕鬆地進行測試,需要完成下列步驟:</p> <ol class="ol"> <li>將業務規則與 DOM 的更新分離</li> <li>消除那些全域變數!</li> </ol> </div> --- <p class="h3"> 我們需要讓程式碼更具可重用性 </p> <div class="left"> <p class="p" style="margin-left: 16px;">會計和運輸部門希望能重用我們的程式碼,但以下幾個原因使他們無法重用:</p> <ol class="ol"> <li>程式碼從全域變數中讀取購物車資料,但他們需要從資料庫中處理訂單,而不是從變數中取資料</li> <li>程式碼直接寫入 DOM,但他們需要列印稅單和運送標籤</li> </ol> </div> --- ```javascript function update_shipping_icons() { var buy_buttons = get_buy_buttons_dom(); for(var i = 0; i < buy_buttons.length; i++) { var button = buy_buttons[i]; var item = button.item; if(item.price + shopping_cart_total >= 20 ) // 想要重用的業務規則(價格大於等於 20) button.show_free_shipping_icon(); // 此函式只能設定 shopping_cart_total 之後才可執行 else button.hide_free_shipping_icon(); // 這些操作只有在 DOM 已建立時才能正常運作 } } // 由於沒有回傳值,因此無法直接從函式取得結果 ``` <div class="left"> <p class="p" style="margin-left: 16px;">為了使它更具可重用性,請做到以下幾點:</p> <ol class="ol"> <li>不要依賴全域變數</li> <li>不要假設答案會被寫入 DOM</li> <li>讓函式回傳答案</li> </ol> </div> --- <p class="h3">區分 Actions, Calculations, and Data </p> --- ```javascript // 全域變數是可變的 ==> Actions var shopping_cart = []; // A var shopping_cart_total = 0; // A ``` ```javascript // 修改一個全域變數 ==> Actions function add_item_to_cart(name, price) { // A shopping_cart.push({ name: name, price: price }); calc_cart_total(); } ``` ```javascript function update_shipping_icons() { // A var buy_buttons = get_buy_buttons_dom(); // 從 DOM 讀取資料 ==> Action for (var i = 0; i < buy_buttons.length; i++) { var button = buy_buttons[i]; var item = button.item; if (item.price + shopping_cart_total >= 20) button.show_free_shipping_icon(); // 修改 DOM ==> Action else button.hide_free_shipping_icon(); // 修改 DOM ==> Action } } ``` --- ```javascript function update_tax_dom() { // A set_tax_dom(shopping_cart_total * 0.10); // 修改 DOM ==> Action } ``` ```javascript function calc_cart_total() { // A shopping_cart_total = 0; // 修改全域變數 ==> Action for (var i = 0; i < shopping_cart.length; i++) { var item = shopping_cart[i]; shopping_cart_total += item.price; } set_cart_total_dom(); update_shipping_icons(); update_tax_dom(); } ``` --- <p class="h3"> Key Points </p> <p class="h4"> 1. Action 會擴散:只要在一個函式內發現一個 Action,那該函式就被視為 Action</p> <p class="h4"> 2. 上面這些程式碼全都是 Actions,沒有單純的 Calculations 及 Data</p> </br> <p class="h3"> 接下來看如何透過 FP 設計來重構程式碼 </p> --- <p class="h3"> 函式擁有輸入與輸出 </p> <div class="left"> <ol class="ol"> <li>每個函式都有輸入和輸出</li> <li>輸入是函式在計算時使用的外部資訊</li> <li>輸出是從函式中離開的資訊或動作</li> <li>我們使用函式主要是為了獲得它的輸出,而輸入就是函式要產生輸出所必須的資料</li> </ol> </div> --- <p class="h3"> 程式範例 </p> ```javascript var total = 0; function add_to_total(amount) { // 參數就是輸入 console.log("Old total: " + total); // 將訊息印出與讀取全域變數都是一種輸入。 total += amount; // 修改全域變數則是一種輸出 return total; // 回傳值是輸出 } ``` </br> <p class="h3"> 整體而言就是關於追蹤資訊如何進入函式和離開函式 </p> --- <p class="h3"> 輸入與輸出可以是隱式 (Implicit) 或顯式 (Explicit) </p> <div class="left"> <ol class="ol"> <li>顯式 (Explicit) 輸入是那些以參數形式傳進函式的資料</li> <li>顯式 (Explicit) 輸出是函式的回傳值</li> <li>任何其他形式進入或離開函式的方式則屬於隱式 (Implicit) 輸入/輸出</li> </ol> </div> ```javascript var total = 0; function add_to_total(amount) { // 顯式 (Explicit) 輸入 console.log("Old total: " + total); // 隱式 (Implicit) 輸入 total += amount; // 隱式 (Implicit) 輸出 return total; // 顯式 (Explicit) 輸出 } ``` </br> <p class="h4"> FP 概念稱這些隱式的輸入與輸出為 副作用(side effects),它們並不是函式的主要作用(主要目的是計算並回傳一個值)</p> --- <p class="h3"> 若能夠消除 Action 中的所有隱式輸入與輸出 </p> <p class="h3"> 那其就會變成 Calculation </p> <div class="left" style="margin-left: 152px;"> <p class="h3" style="margin-left: 16px;"> 關鍵在於:</p> <ol class="ol"> <li>用參數取代隱式輸入</li> <li>用回傳值取代隱式輸出</li> </ol> </div> --- <p class="h3"> 測試跟可重用性與函式的輸入和輸出相關 </p> <div class="left" style="margin-left: 174px;"> <p class="h4" style="margin-left: 16px;">測試</p> <ol class="ol"> <li>將業務規則與 DOM 更新分離</li> <li>消除全域變數</li> </ol> </div> <div class="left" style="margin-left: 174px;"> <p class="h4" style="margin-left: 16px;">開發可重用性</p> <ol class="ol"> <li>不要依賴全域變數</li> <li>不要預設答案會寫入 DOM</li> <li>從函式中回傳答案</li> </ol> </div> --- <p class="h3"> 從 Action 中分離出 Calculation </p> <p class="h4" style="text-align: left; margin-left: 48px;">原始版本</p> ```javascript function calc_cart_total() { // 分離出計算總額的程式碼 /* shopping_cart_total = 0; for(var i = 0; i < shopping_cart.length; i++) { var item = shopping_cart[i]; shopping_cart_total += item.price; } */ set_cart_total_dom(); update_shipping_icons(); update_tax_dom(); } ``` --- <p class="h3" style="display: fixed; "> 從 Action 中分離出 Calculation </p> <p class="h4" style="text-align: left; margin-left: 48px;">分離後</p> ```javascript function calc_cart_total() { calc_total(); // 在原本的位置 Call 新創建的函式 set_cart_total_dom(); update_shipping_icons(); update_tax_dom(); } function calc_total() { shopping_cart_total = 0; for(var i = 0; i < shopping_cart.length; i++) { var item = shopping_cart[i]; shopping_cart_total += item.price; } } ``` --- <p class="h3"> 從 Action 中分離出 Calculation </p> <div class="left" style="margin-left: 174px;"> <p class="h4" style="margin-left: 16px;">開發可重用性:</p> <ol class="ol"> <li>先建立並命名了一個新的函式,並把原本的程式碼直接貼進這個函式中</li> <li>原本程式碼的位置則改為呼叫這個新函式</li> <li>目前這個新函式仍然是一個 Action</li> </ol> </div> </br> <div class="left" style="margin-left: 190px;"> <p class="h4"> 我們在第二步所做的操作,使程式在改動後仍能保持原本的功能 </p> <p class="h4"> 這類在改變程式碼卻不破壞既有功能的操作,稱為重構(refactoring)</p> </br> <p class="h4"> 這次重構過程可稱為「抽取子程序」(extract subroutine)</p> </div> --- <p class="h3"> 從 Action 中分離出 Calculation </p> <p class="h4"> 要將新函式轉換成一個計算,首先必須先辨識其輸入與輸出 </p> ```javascript function calc_total() { // 輸出,將全域變數進行賦值 shopping_cart_total = 0; for (var i = 0; i < shopping_cart.length; i++) { // 輸入,讀取全域變數 var item = shopping_cart[i]; shopping_cart_total += item.price; // 輸出,修改全域變數 } } ``` --- <p class="h3"> 從 Action 中分離出 Calculation </p> <p class="h4"> 替換全域變數為區域變數 </p> ```javascript function calc_total() { var total = 0; // 將全域變數替換成區域變數 for (var i = 0; i < shopping_cart.length; i++) { var item = shopping_cart[i]; total += item.price; // 對區域變數進行操作 } return total; // 返回區域變數 } ``` <p class="h4"> 如此就可將隱式 (Implicit) 的輸出轉換為顯式 (Explicit) 的</p> --- <p class="h3"> 從 Action 中分離出 Calculation </p> <p class="h4"> 將隱式輸入轉換為顯式 (Explicit) 的參數 </p> ```javascript function calc_total(cart) { var total = 0; for(var i = 0; i < cart.length; i++) { // 新增一個參數,用它取代全域變數 var item = cart[i]; total += item.price; } return total; } ``` <p class="h4"> 如此<code>calc_total()</code>其唯一的輸入與輸出就是參數和回傳值 </p> <p class="h4"> 成為一個純粹的 Calculation 函式 </p> --- <p class="h3"> 從 Action 中分離出 Calculation </p> ```javascript function calc_cart_total() { shopping_cart_total = calc_total(shopping_cart); // 在原處呼叫這個 Calculation set_cart_total_dom(); update_shipping_icons(); update_tax_dom(); } ``` <div class="left" style="margin-left: 174px;"> <p class="h4" style="margin-left: 16px;">在原處呼叫這個新的 Calculation 函式</p> <ol class="ol"> <li>在呼叫 <code>calc_total</code> 時,將全域變數 <code>shopping_cart</code> 作為參數傳入</li> <li>新增一個參數並在函式內部使用,取代對全域變數的直接讀取</li> </ol> </div> --- <p class="h3"> 確認是否成功的抽離出 Calculation </p> <div class="left" style="margin-left: 174px;"> <p class="h4" style="margin-left: 16px;">測試</p> <ol class="ol"> <li>將業務規則與 DOM 更新分離 ✅</li> <li>消除全域變數 ✅</li> </ol> </div> <div class="left" style="margin-left: 174px;"> <p class="h4" style="margin-left: 16px;">開發可重用性</p> <ol class="ol"> <li>不要依賴全域變數 ✅</li> <li>不要預設答案會寫入 DOM ✅</li> <li>從函式中回傳答案 ✅</li> </ol> </div> --- <p class="h3"> 再次練習從 Action 抽離出 Calculation </p> ```javascript function add_item_to_cart(name, price) { // 將這段程式碼抽離成新的函式 shopping_cart.push({ name: name, price: price }); calc_cart_total(); } ``` ```javascript function add_item_to_cart(name, price) { add_item(name, price); // 在原位置 call 新函式以取代原本的程式碼 calc_cart_total(); } function add_item(name, price) { // 新的函式 shopping_cart.push({ // 讀取且修改 shopping_cart 全域變數 name: name, price: price }); } ``` --- <p class="h3"> 再次練習從 Action 抽離出 Calculation </p> <div class="left" style="margin-left: 180px"> <p class="h3"> 提醒: </p> <p class="h4"> 讀取全域變數是一種輸入,因為資料是從外部進來 </p> <p class="h4"> 修改全域變數是一種輸出,因為資料離開了函式內部 </p> </div> --- <p class="h3"> 再次練習從 Action 抽離出 Calculation </p> ```javascript function add_item(name, price) { shopping_cart.push({ // 讀取且修改 shopping_cart 全域變數 name: name, price: price }); } ``` ```javascript function add_item(cart, name, price) { // 將全域變數以參數方式傳入 cart.push({ // 改為使用參數而非全域變數 name: name, price: price }); } ``` <p class="h4"> 因為仍然是透過 <code>.push()</code> 方法來修改全域陣列,仍屬於隱式輸出 </p> --- <p class="h3"> 再次練習從 Action 抽離出 Calculation </p> ```javascript function add_item(cart, name, price) { var new_cart = cart.slice(); // 複製傳入的陣列並將複製品賦值給局部變數 new_cart.push({ // 修改複製陣列 name: name, price: price }); return new_cart; // 返回修改後的複製陣列 } ``` <p class="h4"> <code>add_item()</code> 已經不包含任何隱式輸入或輸出,成為一個純粹的 Calculation 函式 </p> <div class="left" style="margin-left: 48px"> <p class="p"> 補充: </p> <p class="p"> 在修改一個可變的值之前先複製它,是實現不可變性的一種方法,稱為「寫時複製」(copy-on-write) </p> </div> --- <p class="h3"> 再次練習從 Action 抽離出 Calculation </p> <p class="h4 left" style="margin-left: 48px"> 修改後 </p> ```javascript function add_item_to_cart(name, price) { shopping_cart = add_item(shopping_cart, name, price); // 呼叫新的 Calculation 函式 calc_cart_total(); } ``` ```javascript function add_item(cart, name, price) { // Explicit Inputs var new_cart = cart.slice(); new_cart.push({ name: name, price: price }); return new_cart; // Explicit Output } ``` --- <p class="h3"> 確認是否成功的抽離出 Calculation </p> <div class="left" style="margin-left: 174px;"> <p class="h4" style="margin-left: 16px;">測試</p> <ol class="ol"> <li>將業務規則與 DOM 更新分離 ✅</li> <li>消除全域變數 ✅</li> </ol> </div> <div class="left" style="margin-left: 174px;"> <p class="h4" style="margin-left: 16px;">開發可重用性</p> <ol class="ol"> <li>不要依賴全域變數 ✅</li> <li>不要預設答案會寫入 DOM ✅</li> <li>從函式中回傳答案 ✅</li> </ol> </div> --- <p class="h3"> 觀念確認 </p> <div class="left" style="margin-left: 48px;"> <ol class="ol"> <li>看起來程式碼量在不斷增加,這正常嗎?難道不是程式碼越少越好嗎?</li> <li>重用性和可測試性真的是函數式程式設計唯一關注的點嗎?</li> <li>我注意到你將某些計算獨立出來,使它們能在原本的使用情境之外被單獨使用,這很重要嗎?</li> <li>在我們抽取出來的計算中,仍然對某個變數進行了修改。我聽說在 FP 設計中,所有東西都是不可變的,這是怎麼回事?</li> </ol> </div> --- <p class="h3"> 觀念確認 </p> <div class="left" style="margin-left: 48px;"> <p class="p" style="font-weight: 700;">Q1: 看起來程式碼量在不斷增加,這正常嗎?難道不是程式碼越少越好嗎?</p> <p class="p">A: 一般來說,程式碼越少越好,但我們的總行數卻在增加</p> <p class="p" style="margin-left: 24px; margin-top: -8px;">原因是我們新增了許多新函式,每個新函式至少需要兩行程式碼:一行用來定義函式的名稱,一行用來關閉大括號。但將程式拆解成許多函式,長遠來看一定會有回報</p> <p class="p" style="margin-left: 24px; margin-top: -8px;">我們已經開始看到這些回報:程式碼變得更容易重用和測試</p> </div> --- <p class="h3"> 觀念確認 </p> <div class="left" style="margin-left: 48px;"> <p class="p" style="font-weight: 700;">Q2: 重用性和可測試性真的是 FP 設計唯一關注的點嗎?</p> <p class="p">A: 絕對不是!FP 設計確實在這方面有幫助,但還有很多其他優點</p> <p class="p" style="margin-left: 24px; margin-top: -8px;">在書的後半部份會談到:並發(concurrency)、架構(architecture)以及資料建模(data modeling)。FP 設計是一個龐大的領域,這本書無法涵蓋所有內容</p> </div> <div class="left" style="margin-left: 48px;"> <p class="p" style="font-weight: 700;">Q3: 我注意到你將某些計算獨立出來,使它們能在原本的使用情境之外被單獨使用,這很重要嗎? </p> <p class="p">A: 相當重要。在 FP 設計中,我們喜歡將程式拆解,這樣可以組成更多、更小的部分</p> <p class="p" style="margin-left: 24px; margin-top: -8px;">而小的部分更容易重用、測試和理解</p> </div> --- <p class="h3"> 觀念確認 </p> <div class="left" style="margin-left: 48px;"> <p class="p" style="font-weight: 700;">Q4: 在我們抽取出來的計算中,仍然對某個變數進行了修改。我聽說在函數式程式設計中,所有東西都是不可變的,這是怎麼回事?</p> <p class="p">A: 這是個好問題。不可變性(immutability)指的是一個東西在創建後就不應再被修改</p> <p class="p" style="margin-left: 24px; margin-top: -8px;">然而,在創建過程中,它需要被初始化,而這個初始化過程就需要進行修改</p> <p class="p" style="margin-left: 24px; margin-top: -8px;">舉例來說,你可能需要初始化陣列中的值,初始化完成後你就不再修改它,但在最初的建立階段,你必須先新增項目</p> </div> --- <p class="h3"> 從 Action 中抽離 Calculation 是一個可重複進行的過程 </p> <div class="left" style="margin-left: 36px;"> <p class="h4" style="margin-left: 16px;">以下是具體步驟:</p> <ol class="ol"> <li>選取並抽離 Calculation 的程式碼</li> <li>辨認函式的隱式輸入與輸出</li> <li>將輸入轉換為參數,並將輸出轉換為回傳值</li> </ol> </div> <p class="p" style="margin: 40px 52px 0; text-align: start;">Note:</p> <p class="p" style="margin: 8px 52px; text-align: start;">參數和回傳值都是不可變的,也就是說它們不應在函式內部被修改。如果我們回傳一個值,然後函式的某個部分又改變了這個值,那就會變成一種隱式輸出。同樣,如果在函式接收參數後,參數的值被改變,那也算是一種隱式輸入</p> --- <p class="h3"> 練習題 1 </p> ```javascript function update_tax_dom() { set_tax_dom(shopping_cart_total * 0.10 ); // 將這個函式抽離為 Calculation } ``` <div class="left" style="margin: -32px 36px;"> <p class="h4" style="margin-left: 16px;">步驟:</p> <ol class="ol"> <li>選取並抽離 Calculation 的程式碼</li> <li>辨認函式的隱式輸入與輸出</li> <li>將輸入轉換為參數,並將輸出轉換為回傳值</li> </ol> </div> --- <p class="h3"> 練習題 </p> <p class="p" style="margin: 8px 52px; text-align: start;"> 1. 選取並抽離Calculation 的程式碼 </p> <p class="p" style="margin: 8px 52px; text-align: start;"> 2. 辨認函式的隱式輸入與輸出 </p> <p class="p" style="margin: 8px 52px; text-align: start;"> 3. 將輸入轉換為參數,並將輸出轉換為回傳值 </p> ```javascript function update_tax_dom() { set_tax_dom(calc_tax()); } function calc_tax() { // 抽離Calculation 的程式碼 return shopping_cart_total * 0.10; // 讀取全域變數,隱式輸入與輸出 } ``` <p class="p" style="margin: 8px 52px; text-align: start;"> 抽離後: </p> ```javascript function update_tax_dom() { set_tax_dom(calc_tax(shopping_cart_total)); } function calc_tax(amount) { // 抽離全域變數到參數 return amount * 0.10; // 將輸出轉換為回傳值 } ``` --- <p class="h3"> 練習題 2 </p> <div class="left" style="margin: -16px 36px;"> <p class="h4" style="margin: 0 16px;">步驟:</p> <ol class="ol"> <li>選取並抽離 Calculation 的程式碼</li> <li>辨認函式的隱式輸入與輸出</li> <li>將輸入轉換為參數,並將輸出轉換為回傳值</li> </ol> </div> ```javascript function update_shipping_icons() { var buy_buttons = get_buy_buttons_dom(); for(var i = 0; i < buy_buttons.length; i++) { var button = buy_buttons[i]; var item = button.item; if(item.price + shopping_cart_total >= 20) // 運輸部門想要重用這段程式碼 button.show_free_shipping_icon(); else button.hide_free_shipping_icon(); } } ``` --- <p class="h3"> 練習題 2 </p> ```javascript function update_shipping_icons() { var buy_buttons = get_buy_buttons_dom(); for(var i = 0; i < buy_buttons.length; i++) { var button = buy_buttons[i]; var item = button.item; if(gets_free_shipping(item.price)) // 抽離重用函式 button.show_free_shipping_icon(); else button.hide_free_shipping_icon(); } } ``` ```javascript function gets_free_shipping(item_price) { return item_price + shopping_cart_total >= 20; // Implicit Input } ``` --- <p class="h3"> 練習題 2 </p> ```javascript function update_shipping_icons() { var buy_buttons = get_buy_buttons_dom(); for(var i = 0; i < buy_buttons.length; i++) { var button = buy_buttons[i]; var item = button.item; if(gets_free_shipping(shopping_cart_total, item.price)) // 全域變數設為參數 button.show_free_shipping_icon(); else button.hide_free_shipping_icon(); } } ``` ```javascript function gets_free_shipping(total,item_price) { // 抽離為 Calculation,可重用 return item_price + total >= 20; } ``` <div class="left" style="margin: -16px 36px;"> <ol class="ol"> <li>沒有 DOM 操作參與 ✅</li> <li>沒有讀取/修改全域變數 ✅</li> <li>函式回傳值 ✅</li> </ol> </div> --- <p class="h3"> 第四章總結 </p> <div class="left" style="margin: -16px 144px;"> <p class="h4" style="font-weight: 400;">1. Action 函式內會有隱式 (Implicit) 輸入與輸出</p> <p class="h4" style="font-weight: 400;">2. Calculation 函式內不會有隱式 (Implicit) 輸入與輸出</p> <p class="h4" style="font-weight: 400;">3. 全域變數為常見的隱式 (Implicit) 輸入與輸出</p> <p class="h4" style="font-weight: 400;">4. 透過帶入函式的參數可去除隱式 (Implicit) 輸入</p> <p class="h4" style="font-weight: 400;">5. 作為回傳值可去除隱式 (Implicit) 輸出</p> <p class="h4" style="font-weight: 400;">6. FP 設計中,我們會將程式碼盡量從 Action 抽離成 Calculation </p> </div> --- <p class="h3"> 第四章 Q&A </p> <div class="left" style="margin-left: 48px;"> <p class="p" style="font-weight: 700;">Q1: 請辨識函式 <code>calc_tax()</code> 中的輸入/輸出,並說明是隱式 (Implicit) 還是顯示 (Explicit)</p> </div> ```javascript var total = 0; function add_to_total(amount) { // 1 Amount 是? console.log ("Old total: " + total ); // 2 console.log 是? 讀取 total 是? total += amount; // 3 修改 total 是? return total; // 4 回傳 total 是? } ``` --- <p class="h3"> 第四章 Q&A </p> <div class="left" style="margin-left: 48px;"> <p class="p" style="font-weight: 700;">Q1: 請辨識函式 <code>calc_tax()</code> 中的輸入/輸出,並說明是隱式 (Implicit) 還是顯示 (Explicit)</p> </div> ```javascript var total = 0; function add_to_total(amount) { // 參數是顯式輸入 // Console.log()是隱式輸出,讀取 total 全域變數是隱式輸入 console.log ("Old total: " + total ); // 修改全域變數 total 是隱式輸出 total += amount; // 3 // 回傳值是顯式輸出 return total; // 4 } ``` --- <p class="h3"> 第四章 Q&A </p> <div class="left" style="margin-left: 48px;"> <p class="p" style="font-weight: 700;">Q2: 請辨識下列函式中的 Actions,並說明原因</p> </div> ```javascript function update_shipping_icons() { var buy_buttons = get_buy_buttons_dom(); for(var i = 0; i < buy_buttons.length; i++) { var button = buy_buttons[i]; var item = button.item; if(item.price + shopping_cart_total >= 20) button.show_free_shipping_icon(); else button.hide_free_shipping_icon(); } } ``` --- <p class="h3"> 第四章 Q&A </p> <div class="left" style="margin-left: 48px;"> <p class="p" style="font-weight: 700;">Q2: 請辨識下列函式中的 Actions,並說明原因 </p> </div> ```javascript function update_shipping_icons() { var buy_buttons = get_buy_buttons_dom(); // Action,讀取 DOM for(var i = 0; i < buy_buttons.length; i++) { var button = buy_buttons[i]; var item = button.item; if(item.price + shopping_cart_total >= 20) button.show_free_shipping_icon(); // Action,操作 DOM else button.hide_free_shipping_icon(); // Action,操作 DOM } } ``` --- <p class="h3"> 第四章 Q&A </p> <div class="left" style="margin-left: 48px;"> <p class="p" style="font-weight: 700;"> Q3: 函式 <code>calc_tax()</code> 似乎還是 Action,怎麼做才能抽離成 Caculation 呢? </p> </div> ```javascript var shopping_cart_total = 0 function update_tax_dom() { set_tax_dom(calc_tax()); } function calc_tax() { shopping_cart_total * 0.10; } ``` --- <p class="h3"> 第四章 Q&A </p> <div class="left" style="margin-left: 48px;"> <p class="p" style="font-weight: 700;"> Q3: 函式 <code>calc_tax()</code> 似乎還是 Action,怎麼做才能抽離成 Caculation 呢? </p> </div> ```javascript var shopping_cart_total = 0 function update_tax_dom() { set_tax_dom(calc_tax(shopping_cart_total)); } function calc_tax(amount) { // 抽離全域變數到參數 return amount * 0.10; // 將輸出轉換為回傳值 } ``` --- <p class="h3"> 第四章 Q&A </p> <div class="left" style="margin-left: 148px;"> <p class="p" style="font-weight: 700;"> Q4: 把 Calculation 從 Action 抽離出來可以有哪些好處呢?(試舉兩項) </p> </div> --- <p class="h3"> 第四章 Q&A </p> <div class="left" style="margin-left: 148px;"> <p class="p" style="font-weight: 700;"> Q4: 把 Calculation 從 Action 抽離出來可以有哪些好處呢?(試舉兩項) </p> <p class="p"> 1. 增加可重用性 </p> <p class="p"> 2. 使其更容易進行測試 </p> </div> --- <p class="h2"> 第 5 章 改良 Actions 的設計 </p> --- <p class="h2"> 第 5 章 改良 Actions 的設計 </p> </br> <p class="h4"> 在第四章我們了解如何將隱式輸入及輸出移除來將 Action 轉換成 Calculation </p> <p class="h4"> 然而,我們也不能將所有的 Action 移除 </p> </br> <p class="h4"> 所以第五章我們要來了解,如何透過移除部分的隱式輸入及輸出,來改良 Actions 的設計 </p> --- <p class="h3"> 設計與業務需求保持一致 </p> <p class="h4"> 第四章重構 Action 的過程是比較機械化的動作,不一定是最佳設計 </p> <p class="h4"> 以下面 <code>gets_free_shipping</code> 及 <code>calc_total</code> 的函式為例 </p> ```javascript function gets_free_shipping(total, item_price) { // 這些參數並不是我們真正想要的 return item_price + total >= 20; // 這裡重複計算了 (總額 + 單品價格) } ``` ```javascript function calc_total(cart) { var total = 0; for (var i = 0; i < cart.length; i++) { var item = cart[i]; total += item.price; // 這裡重複計算了 (總額 + 單品價格) } return total; } ``` <div class="left" style="margin: -32px 48px;"> <p class="p"> Note: 在兩個地方重複了「單品價格加到總額」這段程式碼。重複不一定有問題,但它通常是一種 Code Smell,代表可能有更深層次的問題。 </p> </div> --- <p class="h3"> 設計與業務需求保持一致 </p> <p class="h4"> 我們希望將 <code>gets_free_shipping</code> 直接代入購物車為參數 </p> <p class="h4"> 並且重用 <code>calc_total</code> 來去除重複的程式碼邏輯 </p> ```javascript function gets_free_shipping(cart) { return calc_total(cart) >= 20; // 重用 calc_total() 來去除重複的程式碼邏輯 } ``` <p class="h4"> 現在這個函式直接對「購物車」操作,而不是對「總額」和「單品價格」分開處理 </p> --- <p class="h3"> 對齊函式功能與業務需求 </p> <p class="h4"> 這其實不算是重構(refactoring),因為我們正在改變行為 </p> --- <p class="h3"> 修改呼叫 <code>gets_free_shipping</code> 的函式 </p> ```javascript function update_shipping_icons() { var buttons = get_buy_buttons_dom(); for (var i = 0; i < buttons.length; i++) { var button = buttons[i]; // var item = button.item; var new_cart = add_item(shopping_cart, item.name, item.price); // if (gets_free_shipping(shopping_cart_total, item.price)) if (gets_free_shipping(new_cart)) button.show_free_shipping_icon(); else button.hide_free_shipping_icon(); } } ``` <div class="left" style="margin: -32px 48px;"> <p class="p"> 1. 先用 <code>add_item</code> 建立一個包含新商品的「新購物車」</p> <p class="p"> 2. 呼叫修改後的 <code>gets_free_shipping</code> 函式 </p> <p class="p"> 3. 現在的程式碼符合業務需求:它會告訴我們「某個購物車是否能享有免運優惠」</p> </div> --- <p class="h3"> 觀念討論 </p> <div class="left" style="margin: -32px 48px;"> <p class="h4"> Q1: 程式碼行數不斷增加!這樣到底有什麼好處?</p> <p class="p" style="margin-top: -10px;"> A: 程式碼行數的確是衡量程式庫撰寫與維護難易度的一個指標,但它並不完美 </p> <p class="p" style="margin: -10px 21px 0;">另一種衡量維護難度的方法是看每個函式的規模有多大,函式越精簡,就越容易理解和維護</p> <p class="h4" style="margin-top: 48px;"> Q2: 每次執行 <code>add_item()</code> 都會複製一次購物車陣列,這樣不會很耗效能嗎?</p> <p class="p" style="margin-top: -10px;"> A: 的確,這比直接修改陣列要耗效能,不過現今的垃圾回收機制對這種操作處理得很好 </p> <p class="p" style="margin: -10px 21px 0;"> 事實上,我們經常做「複製」這個動作:例如 JavaScript 的字串是不可變的</p> <p class="p" style="margin: 8px 21px 0;"> 每次把兩個字串串接在一起,就會產生一個新的字串,所有字元都必須被複製過去 </p> <p class="p" style="margin: 8px 21px 0;"> 這些好處通常大於額外的成本,能夠在不改變原物件的前提下,製作修改後的複本非常有用 </p> </div> --- <p class="h3"> 原則 - 將隱式輸入與輸出降到最低 </p> <div class="left" style="margin: -32px 48px;"> <p class="h4"> 隱式輸入:不是函式參數的輸入 </p> <p class="h4"> 隱式輸出:則指那些不是回傳值的輸出 </p> <p class="p"> 帶有隱式輸入與輸出的函式就像是焊死的線路,缺乏模組化,無法重用 </p> <p class="p"> 當將其轉換為顯式輸入與輸出後,就像在元件之間裝上易於拆卸的連接器 </p> </div> --- <p class="h3"> 原則 - 將隱式輸入與輸出降到最低 </p> <div class="left" style="margin: -32px 48px;"> <p class="p"> 隱式輸入及隱式輸出都會限制函式何時才能被呼叫 </p> <p class="p"> 由於呼叫時機受限,就使更難測試。要先準備好所有隱式輸入再執行函式,然後再檢查所有隱式輸出。因此,隱式輸入與輸出的數量越多,測試就越困難 </p> <p class="p"> Calculation 函式最容易測試,因為不含任何隱式輸入與輸出 </p> <p class="p"> 即便是 Action 函式,也應盡量消除隱式輸入與輸出,能明顯提升函式的可測試性與可重用性 </p> </div> --- <p class="h3"> 實作最小化隱式輸入與輸出 </p> <p class="h4"> 以函式 <code>update_shipping_icons()</code> 為例</p> <p class="h4"> 原始版本 </p> ```javascript function update_shipping_icons() { var buttons = get_buy_buttons_dom(); for(var i = 0; i < buttons.length; i++) { var button = buttons[i]; var item = button.item; var new_cart = add_item(shopping_cart, // 讀取全域變數 shopping_cart item.name, item.price); if(gets_free_shipping(new_cart)) button.show_free_shipping_icon(); else button.hide_free_shipping_icon(); } } ``` --- <p class="h3"> 實作最小化隱式輸入與輸出 </p> <p class="h4"> 修改後 </p> ```javascript function update_shipping_icons(cart) { // 新增 cart 參數,取代全域變數 var buttons = get_buy_buttons_dom(); for(var i = 0; i < buttons.length; i++) { var button = buttons[i]; var item = button.item; var new_cart = add_item(cart, // 新增 cart 參數,取代全域變數 item.name, item.price); if(gets_free_shipping(new_cart)) button.show_free_shipping_icon(); else button.hide_free_shipping_icon(); } } ``` --- <p class="h3">實作最小化隱式輸入與輸出</p> <p class="h4"> 在原呼叫 <code>update_shipping_icons()</code> 處加上參數 </p> ```javascript function calc_cart_total() { shopping_cart_total = calc_total(shopping_cart); set_cart_total_dom(); update_shipping_icons(shopping_cart); // shopping_cart 作為參數傳入 update_tax_dom(); } ``` --- <p class="h3"> 重新檢視程式碼 </p> ```javascript function add_item_to_cart(name, price) { // 這個函式會在按下「立即購買」按鈕時被呼叫 shopping_cart = add_item(shopping_cart, name, price); calc_cart_total(shopping_cart); } // calc_cart_total() 看起來有點多餘,可直接在 add_item_to_cart() 中做這件事? function calc_cart_total(cart) { var total = calc_total(cart); set_cart_total_dom(total); update_shipping_icons(cart); update_tax_dom(total); // 我們寫入了這個全域變數,卻沒有任何地方讀取它,可以直接刪除 shopping_cart_total = total; } function set_cart_total_dom(total) { … } function update_shipping_icons(cart) { var buy_buttons = get_buy_buttons_dom(); for (var i = 0; i < buy_buttons.length; i++) { var button = buy_buttons[i]; var item = button.item; var new_cart = add_item(cart, item.name, item.price); if (gets_free_shipping(new_cart)) button.show_free_shipping_icon(); else button.hide_free_shipping_icon(); } } function update_tax_dom(total) { set_tax_dom(calc_tax(total)); } ``` <div class="left" style="margin: -32px 48px;"> <p class="p"> 1. 全域變數 <code>shopping_cart_total</code> 並未被使用,可移除</p> <p class="p"> 2. <code>calc_cart_total()</code> 是多餘的函式,可將其程式碼移至 <code>add_item_to_cart()</code> 內執行</p> </div> --- <p class="h3"> 重新檢視程式碼 </p> <p class="h4"> 優化後 </p> ```javascript function add_item_to_cart(name, price) { shopping_cart = add_item(shopping_cart, name, price); var total = calc_total(shopping_cart); set_cart_total_dom(total); update_shipping_icons(shopping_cart); // 刪除 calc_cart_total() 以及全域變數 shopping_cart_total,並將程式碼移入 add_item_to_cart() update_tax_dom(total); } ``` <p class="h4">現在,Action 函式已經更乾淨</p> <p class="h4">接下來可以依照意義層次(layer of meaning)對這些函式做進一步分層整理</p> --- <p class="h3"> 為 Calculation 函式分類 </p> <p class="h4"> 分類對照表 </p> <p class="p"> C - Cart 操作</p> <p class="p"> I - Item 操作 </p> <p class="p"> B - 業務規則 </p> --- ```javascript function add_item(cart, name, price) { // C  I var new_cart = cart.slice(); // 用 .slice() 來複製陣列 new_cart.push({ name: name, price: price }); return new_cart; } ``` ```javascript function calc_total(cart) { // C  I  B var total = 0; for (var i = 0; i < cart.length; i++) { var item = cart[i]; total += item.price; } return total; } ``` ```javascript function gets_free_shipping(cart) { // B return calc_total(cart) >= 20; } function calc_tax(amount) { // B return amount * 0.10; } ``` --- <p class="h3"> 原則:設計就是將事物拆解 </p> <div class="left" style="margin: -32px 48px;"> <p class="h4"> 關注點分離 </p> <p class="p"> 函式(functions)提供了一種非常自然的方式來分離關注點 </p> <p class="p"> 把「值如何被傳入」(arguments)與「值如何被使用」分開處理 </p> <p class="p"> 人們常常傾向認為越大、越複雜的東西越有份量 </p> <p class="p"> 但被拆解開的部分隨時都能再組合起來,真正的挑戰在於,如何找到有用的拆解方式 </p> </div> --- <p class="h3"> 原則:設計就是將事物拆解 </p> <div class="left" style="margin: -32px 48px;"> <p class="h4"> 拆解帶來的好處 </p> <p class="p"> 1. 更易於重用 </p> <p class="p"> 2. 更易於維護 </p> <p class="p"> 3. 更易於測試 </p> </br> <p class="p"> 即便一支函式目前看不出任何明顯的問題,但如果有可以拆出的部分,至少試著把它抽取出來,往往這樣做可以帶來更好的設計 </p> </div> --- <p class="h3"> 透過拆解 <code>add_item()</code> 來改進設計 </p> <p class="h4"> 這個 <code>add_item()</code> 看似只是將物品加入購物車,但仔細一看他其實執行了四個不同的動作 </p> ```javascript function add_item(cart, name, price) { var new_cart = cart.slice(); // 1 複製一個陣列 new_cart.push({ // 2 建立一個物件 3 將物品加入複製的陣列 name: name, price: price }); return new_cart; // 4 回傳這個複製陣列 } add_item(shopping_cart, "shoes", 3.45); ``` <p class="h4" style="text-align: start;"> 這個函式同時「知道」購物車的結構,也「知道」物品的結構。我們可以把「物品」的部分抽出成自己的函式 </p> --- <p class="h4"> 拆解後 </p> ```javascript function make_cart_item(name, price) { // 建立一個「物品建構函式」 return { // 建立一個物件 name: name, price: price }; } function add_item(cart, item) { var new_cart = cart.slice(); // 複製一個陣列 ** Note new_cart.push(item); // 將物品加入複製的陣列 return new_cart; // 回傳這個複製陣列 } // 調整呼叫方式,先用 make_cart_item 建立物品,再傳給 add_item add_item(shopping_cart, make_cart_item("shoes", 3.45)); ``` <p class="p" style="text-align: start; margin-left: 48px;"> Note 1: 先複製再修改再回傳,是實現 Immutability 的常用策略,稱為「寫時複製」(copy-on-write)</p> <p class="p" style="text-align: start; margin-left: 48px;"> Note 2: 目前這兩個函式各自處理不同的物件結構,同時這樣拆解讓其不再特定於「購物車」或「物品」 </p> --- <p class="h3"> 抽離寫時複製(copy-on-write)模式 </p> <p class="h4"> <code>add_item()</code> 只是將元素加入陣列,是通用函式,而目前的命名還是針對購物車 </p> <p class="h4"> 試著把函式和參數改成更通用的名稱 </p> ```javascript /* function add_item(cart, item) { var new_cart = cart.slice(); new_cart.push(item); return new_cart; } */ function add_element_last(array, elem) { // 通用的名稱,可用於任何陣列和元素 var new_array = array.slice(); new_array.push(elem); return new_array; } ``` --- <p class="h3"> 抽離寫時複製(copy-on-write)模式 </p> <p class="h4"> 改名後我們就可以非常簡單地定義 <code>add_item()</code> </p> ```javascript function add_item(cart, item) { return add_element_last(cart, item); } ``` <p class="p"> 這樣就抽離出一個高度可重用的函式,能作用於任何陣列和元素,而不侷限於購物車與商品 </p> --- <p class="h3"> 使用 <code>add_item()</code> </p> <p class="h4"> <code>add_item()</code> 之前接受三個參數:cart、name 和 price,現在只接受 cart、item </p> <p class="h4"> 代表我們需要修改那些呼叫 <code>add_item()</code> 的函式,傳入正確的參數數量 </p> ```javascript function add_item_to_cart(name, price) { shopping_cart = add_item(shopping_cart, name, price); // 原先的寫法 var total = calc_total(shopping_cart); set_cart_total_dom(total); update_shipping_icons(shopping_cart); update_tax_dom(total); } ``` ```javascript function add_item_to_cart(name, price) { var item = make_cart_item(name, price); // 先建構出 item shopping_cart = add_item(shopping_cart, item); // 再把它傳給 add_item() var total = calc_total(shopping_cart); set_cart_total_dom(total); update_shipping_icons(shopping_cart); update_tax_dom(total); } ``` --- <p class="h3"> 幫 Calculation 函式分類 </p> <p class="h4"> 在修改了程式碼之後,再一次檢視這些 Calculation 函式 </p> <div class="left" style="margin-left: 180px;"> <p class="h4"> 分類對照表 </p> <p class="p"> C - Cart 操作</p> <p class="p"> I - Item 操作 </p> <p class="p"> B - 業務規則 </p> <p class="p"> A - Array 工具 </p> </div> --- <p class="h3"> 幫 Calculation 函式分類 </p> ```javascript function add_element_last(array, elem) { // A - Array 工具 var new_array = array.slice(); new_array.push(elem); return new_array; } function add_item(cart, item) { // C - Cart 操作 return add_element_last(cart, item); } ``` ```javascript function make_cart_item(name, price) { // I: Item 操作 return { name: name, price: price }; } ``` --- <p class="h3"> 幫 Calculation 函式分類 </p> ```javascript function calc_total(cart) { // C I B - Cart + Item + 業務規則 var total = 0; for (var i = 0; i < cart.length; i++) { var item = cart[i]; total += item.price; } return total; } ``` ```javascript function gets_free_shipping(cart) { // B - 業務規則 return calc_total(cart) >= 20; } // B: Business rule function calc_tax(amount) { // B - 業務規則 return amount * 0.10; } ``` --- <p class="h3"> 觀念討論 </p> <div class="left" style="margin: -32px 48px;"> <p class="h4"> Q1: 為什麼我們要再把函式分類成 C、I、B、A?</p> <p class="p" style="margin-top: -10px;"> A: 這只是對本書後面要學習的設計技巧做個鋪陳,最後會把這些分組整理成完全獨立的層次 </p> <p class="h4" style="margin-top: 48px;"> Q2: 那麼業務規則和購物車操作有什麼差別?電商業務不就是處理購物車嗎? </p> <p class="p" style="margin-top: -10px;"> A: 可以想成,多數電商都有購物車功能,這些購物車相關的操作對任何電商平台來說都是通用的 </p> <p class="p" style="margin: -10px 21px 0;"> 但「業務規則」則是針對 MegaMart 這家公司的獨特做法,例如在別家商店也會有購物車</p> <p class="p" style="margin: 8px 21px 0;"> 但不一定會有同樣的免運門檻 </p> </div> --- <p class="h3"> 觀念討論 </p> <div class="left" style="margin: -32px 48px;"> <p class="h4"> Q3: 一個函式可以同時算是「業務規則」又是「購物車操作」嗎?</p> <p class="p" style="margin-top: -10px;"> A: 目前看來確實可以,但這也是一種 Code Smell,當後續開始討論分層時,可能要把它們分開</p> <p class="p" style="margin: -10px 21px 0;"> 如果業務規則必須知道購物車是陣列實作,那可能帶來未來的維護問題,因為通常業務規則 </p> <p class="p" style="margin: 8px 21px 0;"> 變動購物車這類底層結構更頻繁 </p> </div> --- <p class="h3"> 更小的函式與更多 Calculations </p> ```javascript var shopping_cart = []; // A,定義全域變數 function add_item_to_cart(name, price) { // A var item = make_cart_item(name, price); shopping_cart = add_item(shopping_cart, item); // 讀取全域變數 var total = calc_total(shopping_cart); set_cart_total_dom(total); update_shipping_icons(shopping_cart); update_tax_dom(total); } function update_shipping_icons(cart) { // A var buttons = get_buy_buttons_dom(); for (var i = 0; i < buttons.length; i++) { var button = buttons[i]; var item = button.item; var new_cart = add_item(cart, item); if (gets_free_shipping(new_cart)) button.show_free_shipping_icon(); // 操作 DOM else button.hide_free_shipping_icon(); } } function update_tax_dom(total) { // A set_tax_dom(calc_tax(total)); // 操作 DOM } function add_element_last(array, elem) { // C 沒有隱式輸入/輸出 var new_array = array.slice(); new_array.push(elem); return new_array; } function add_item(cart, item) { return add_element_last(cart, item); // C 沒有隱式輸入/輸出 } function make_cart_item(name, price) { // C 沒有隱式輸入/輸出 return { name: name, price: price }; } function calc_total(cart) { // C 沒有隱式輸入/輸出 var total = 0; for (var i = 0; i < cart.length; i++) { var item = cart[i]; total += item.price; } return total; } function gets_free_shipping(cart) { // C 沒有隱式輸入/輸出 return calc_total(cart) >= 20; } function calc_tax(amount) { // C 沒有隱式輸入/輸出 return amount * 0.10; } ``` --- <p class="h3"> 小結 </p> <p class="h4"> 現在的 Action 函式不再需要了解資料的結構 </p> <p class="h4"> 出現許多有用且可重用的介面函式 </p> <p class="h4"> 購物車中還潛伏著更多的 bug,後續章節會持續探討 </p> --- <p class="h3"> 第五章總結 </p> <div class="left" style="margin: -16px 144px;"> <p class="h4" style="font-weight: 400;">1. 通則是,我們應該透過「參數」和「回傳值」來取代所有隱 </p> <p class="h4" style="font-weight: 400; margin: -16px 24px 0;"> 式輸入與隱式輸出 </p> <p class="h4" style="font-weight: 400;">2. 設計就是要將事物拆解,拆開後的部分隨時都能再組合回來 </p> <p class="h4" style="font-weight: 400;">3. 我們將程式拆得更細,並讓每個函式只負責一件事,就會發</p> <p class="h4" style="font-weight: 400; margin: -16px 24px 0;">現更容易依照概念來組織它們 </p> </div> --- <p class="h3"> 第五章 Q&A </p> <div class="left" style="margin-left: 48px;"> <p class="p" style="font-weight: 700;"> Q1: ”我們應該透過「OO」和「XXX」來取代所有隱式輸入輸出“ => OO 跟 XXX 是什麼呢? </p> </div> --- <p class="h3"> 第五章 Q&A </p> <div class="left" style="margin-left: 48px;"> <p class="p" style="font-weight: 700;"> Q1: ”我們應該透過「OO」和「XXX」來取代所有隱式輸入輸出“ => OO 跟 XXX 是什麼呢? </p> <p class="p" style="font-weight: 700;"> A: 「參數」及「回傳值」</p> </div> --- <p class="h3"> 第五章 Q&A </p> <div class="left" style="margin-left: 48px;"> <p class="p" style="font-weight: 700;"> Q2: 下列程式碼在設計上有哪些地方可以再優化呢? </p> </div> ```javascript function gets_free_shipping(total, item_price) { return item_price + total >= 20; } ``` ```javascript function calc_total(cart) { var total = 0; for (var i = 0; i < cart.length; i++) { var item = cart[i]; total += item.price; } return total; } ``` --- <p class="h3"> 第五章 Q&A </p> <div class="left" style="margin-left: 48px;"> <p class="p" style="font-weight: 700;"> Q2: 下列程式碼在設計上有哪些地方可以再優化呢? </p> </div> ```javascript function gets_free_shipping(total, item_price) { // 直接帶入購物車為參數會更好 return item_price + total >= 20; // 這裡重複計算了 (總額 + 單品價格) } ``` ```javascript function calc_total(cart) { var total = 0; for (var i = 0; i < cart.length; i++) { var item = cart[i]; total += item.price; // 這裡重複計算了 (總額 + 單品價格) } return total; } ``` --- <p class="h3"> 第五章 Q&A </p> <div class="left" style="margin-left: 48px;"> <p class="p" style="font-weight: 700;"> Q3: 在過去章節進行隱式輸入輸出替換的過程中,程式碼行數不斷增加,這樣有好處嗎? </p> </div> --- <p class="h3"> 第五章 Q&A </p> <div class="left" style="margin-left: 48px;"> <p class="p" style="font-weight: 700;"> Q3: 在過去章節進行隱式輸入輸出替換的過程中,程式碼行數不斷增加,這樣有好處嗎? </p> <p class="p" style="font-weight: 700;"> A: 雖然行數不斷增加,但個別函式越精簡,這樣也就更容易理解和維護 </p> </div> --- <p class="h3"> 第五章 Q&A </p> <div class="left" style="margin-left: 48px;"> <p class="p" style="font-weight: 700;"> Q4: 一般在 JavaScript 中,會用什麼方式直接複製一個陣列 </p> <p class="p" style="font-weight: 700;"> 1. <code>.copy()</code> </p> <p class="p" style="font-weight: 700;"> 2. <code>.clone()</code> </p> <p class="p" style="font-weight: 700;"> 3. <code>.duplicate()</code> </p> <p class="p" style="font-weight: 700;"> 4. <code>.slice()</code> </p> </div> --- <p class="h3"> 第五章 Q&A </p> <div class="left" style="margin-left: 48px;"> <p class="p" style="font-weight: 700;"> Q4: 一般在 JavaScript 中,會用什麼方式直接複製一個陣列 </p> <p class="p" style="font-weight: 700;"> 1. <code>.copy()</code> </p> <p class="p" style="font-weight: 700;"> 2. <code>.clone()</code> </p> <p class="p" style="font-weight: 700;"> 3. <code>.duplicate()</code> </p> <p class="p" style="font-weight: 700;"> 4. <code>.slice()</code> ✅ </p> </div> --- <p class="h3"> 第五章 Q&A </p> <div class="left" style="margin-left: 48px;"> <p class="p" style="font-weight: 700;"> Q5: 章節中有提到一個實現不可變性的策略是:先複製再修改再回傳,這種策略稱為?</p> <p class="p" style="font-weight: 700;"> 1. 即時複製 (Instant Copy) </p> <p class="p" style="font-weight: 700;"> 2. 讀時複製 (Copy-on-Read) </p> <p class="p" style="font-weight: 700;"> 3. 寫時複製 (Copy-on-Write) </p> <p class="p" style="font-weight: 700;"> 4. 延遲複製 (Lazy Copy) </p> </div> --- <p class="h3"> 第五章 Q&A </p> <div class="left" style="margin-left: 48px;"> <p class="p" style="font-weight: 700;"> Q5: 章節中有提到一個實現不可變性的策略是:先複製再修改再回傳,這種策略稱為?</p> <p class="p" style="font-weight: 700;"> 1. 即時複製 (Instant Copy) </p> <p class="p" style="font-weight: 700;"> 2. 讀時複製 (Copy-on-Read) </p> <p class="p" style="font-weight: 700;"> 3. 寫時複製 (Copy-on-Write) ✅ </p> <p class="p" style="font-weight: 700;"> 4. 延遲複製 (Lazy Copy) </p> </div> --- <div style="position: relative; min-width: 1080px; min-height: 320px;"> <p class="h3 fixed" style="top: 100px; left: 50%; transform: translateX(-50%); width:1080px;" > 下次讀書會預告 </p> <p class="h4 fixed" style="top: 168px; left: 50%; transform: translateX(-50%); width:1080px;" > 在接下來的兩章,我們將仔細探討「不可變性」: </p> <p class="h4 fixed" style="top: 208px; left: 50%; transform: translateX(-50%); width:1080px;" > 如何在撰寫新程式時實現不可變性,同時又能與現有程式互動? </p> <p class="h4 fixed" style="right: 132px; bottom: -40px;" >Presenter : Yo0</p> <p class="h4 fixed" style="right: 76px; bottom: -80px;" >Note Taker : Hannah</p> <p class="h4 fixed" style="right: 106px; bottom: -120px;" >Date : 2025/05/01</p> </div>

    Import from clipboard

    Paste your markdown or webpage here...

    Advanced permission required

    Your current role can only read. Ask the system administrator to acquire write and comment permission.

    This team is disabled

    Sorry, this team is disabled. You can't edit this note.

    This note is locked

    Sorry, only owner can edit this note.

    Reach the limit

    Sorry, you've reached the max length this note can be.
    Please reduce the content or divide it to more notes, thank you!

    Import from Gist

    Import from Snippet

    or

    Export to Snippet

    Are you sure?

    Do you really want to delete this note?
    All users will lose their connection.

    Create a note from template

    Create a note from template

    Oops...
    This template has been removed or transferred.
    Upgrade
    All
    • All
    • Team
    No template.

    Create a template

    Upgrade

    Delete template

    Do you really want to delete this template?
    Turn this template into a regular note and keep its content, versions, and comments.

    This page need refresh

    You have an incompatible client version.
    Refresh to update.
    New version available!
    See releases notes here
    Refresh to enjoy new features.
    Your user state has changed.
    Refresh to load new user state.

    Sign in

    Forgot password

    or

    By clicking below, you agree to our terms of service.

    Sign in via Facebook Sign in via Twitter Sign in via GitHub Sign in via Dropbox Sign in with Wallet
    Wallet ( )
    Connect another wallet

    New to HackMD? Sign up

    Help

    • English
    • 中文
    • Français
    • Deutsch
    • 日本語
    • Español
    • Català
    • Ελληνικά
    • Português
    • italiano
    • Türkçe
    • Русский
    • Nederlands
    • hrvatski jezik
    • język polski
    • Українська
    • हिन्दी
    • svenska
    • Esperanto
    • dansk

    Documents

    Help & Tutorial

    How to use Book mode

    Slide Example

    API Docs

    Edit in VSCode

    Install browser extension

    Contacts

    Feedback

    Discord

    Send us email

    Resources

    Releases

    Pricing

    Blog

    Policy

    Terms

    Privacy

    Cheatsheet

    Syntax Example Reference
    # Header Header 基本排版
    - Unordered List
    • Unordered List
    1. Ordered List
    1. Ordered List
    - [ ] Todo List
    • Todo List
    > Blockquote
    Blockquote
    **Bold font** Bold font
    *Italics font* Italics font
    ~~Strikethrough~~ Strikethrough
    19^th^ 19th
    H~2~O H2O
    ++Inserted text++ Inserted text
    ==Marked text== Marked text
    [link text](https:// "title") Link
    ![image alt](https:// "title") Image
    `Code` Code 在筆記中貼入程式碼
    ```javascript
    var i = 0;
    ```
    var i = 0;
    :smile: :smile: Emoji list
    {%youtube youtube_id %} Externals
    $L^aT_eX$ LaTeX
    :::info
    This is a alert area.
    :::

    This is a alert area.

    Versions and GitHub Sync
    Get Full History Access

    • Edit version name
    • Delete

    revision author avatar     named on  

    More Less

    Note content is identical to the latest version.
    Compare
      Choose a version
      No search result
      Version not found
    Sign in to link this note to GitHub
    Learn more
    This note is not linked with GitHub
     

    Feedback

    Submission failed, please try again

    Thanks for your support.

    On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

    Please give us some advice and help us improve HackMD.

     

    Thanks for your feedback

    Remove version name

    Do you want to remove this version name and description?

    Transfer ownership

    Transfer to
      Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

        Link with GitHub

        Please authorize HackMD on GitHub
        • Please sign in to GitHub and install the HackMD app on your GitHub repo.
        • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
        Learn more  Sign in to GitHub

        Push the note to GitHub Push to GitHub Pull a file from GitHub

          Authorize again
         

        Choose which file to push to

        Select repo
        Refresh Authorize more repos
        Select branch
        Select file
        Select branch
        Choose version(s) to push
        • Save a new version and push
        • Choose from existing versions
        Include title and tags
        Available push count

        Pull from GitHub

         
        File from GitHub
        File from HackMD

        GitHub Link Settings

        File linked

        Linked by
        File path
        Last synced branch
        Available push count

        Danger Zone

        Unlink
        You will no longer receive notification when GitHub file changes after unlink.

        Syncing

        Push failed

        Push successfully