--- tag: ["OOP", "SoftwareDesign"] --- # 簡化我的代碼 - 1 # 介紹 我發現很多人寫OOP的時候都會過度設計,例如之前的我,將簡單的代碼加上非常多酷炫的技巧,酷炫的寫法,酷炫的庫,反正就是帥氣爆表,但實際上可能只是把代碼變得更複雜了,時隔1年,是時候將我之前的代碼拿出來重構了。 # 重構 > 重構(Refactoring)的定義是在**不改變程式功能**的前提下,對程式碼進行修改,以改善程式的結構、提高程式碼的可讀性、可維護性和可擴展性,以降低修改程式碼所帶來的風險。重構通常是持續進行的過程,而非一次性的工作。它是一種經驗豐富的開發者通過多年開發維護程式的經驗得出的一種程式碼優化方法,可以使得程式碼更加容易維護和修改,並且更加容易理解。~~by ChatGPT~~。 由此可見,我們的目標是**提高可讀性、可維護性和可擴展性**,但前提是建立在**不改變程式功能的**的前提下,也就是說我們首先要清楚我們原先的代碼在幹嘛,才能寫出改善後的代碼,並且改善後的代碼也要符合要求,不然重構將不成立。 ```js= class Null { constructor(){} execute(current){ return current } } class Addition { constructor(value) { this.value = value } execute(current) { return current + this.value } } class Substraction { constructor(value) { this.value = value } execute(current) { return current - this.value } } class Multiplication { constructor(value) { this.value = value } execute(current) { return current * this.value } } class Division { constructor(value) { this.value = value } execute(current) { return current / this.value } } class Calculator { constructor() { this.current = null } view() { if (this.current === null) return "0" return this.current.toString() } execute(operation) { this.current = operation.execute(this.current) } } document.addEventListener('alpine:init', () => { Alpine.data('state', () => ({ calculator: new Calculator(), operation: new Null(), handleClick(action) { if (action === "C") { this.reset() return } if (action === "+" || action === "-" || action === "/" || action === "*") { this.calculator.execute(this.operation) this.operation = this.textToOperation(action, this.calculator.current) this.calculator.current = 0 return } if (action === "=") { this.calculator.execute(this.operation) return } this.calculator.current = parseInt(`${this.calculator.current || ""}${action}`) }, textToOperation(text, value) { switch (text) { case "+": return new Addition(value) case "-": return new Substraction(value) case "/": return new Division(value) case "*": return new Multiplication(value) default: return new Null() } }, reset() { this.calculator.current = 0 this.operation = new Null() } })) }) ``` 這段代碼有點長,但其實你最後就會發現其實,並沒有多麼複雜,其實就是一個簡單的計算機,如果看不懂沒關係,我來解釋一下我寫了什麼: - 我使用了空物件設計模式(Null Object Pattern),也就是會用一個物件來呈現空的狀態而不是直接使用**null**,來讓其他方法或函數不需要去檢查**null**的情況。 - 我使用了指令設計模式(Command Pattern),也就是會讓多個物件當作指令,執行他們的共同方法,來達到可以替換的效果。 - 傳進去handleClick是數字就紀錄,如果是+、-、/、\*,則紀錄運算符。 於是我選擇去忘記這些複雜的設計模式(pattern),用最簡單,也是最符合需求的方式來去實現這個東西,除非需要他們。 # 資料 這部分我想了一陣子,主要思考的是他的狀態,他主要有兩個狀態: - 還未輸入任何運算符(+, -, *, /) - 已經輸入運算符 這兩種狀態的區別是第一種不會包含運算符和另一個值,所以只會有一個值。然後我就大概想出一個草稿: ## 第一種狀態 ```json= { "left": 0, "operation": null, "right": 0 } ``` ## 第二種狀態 ```json= { "left": 10, "operation": "+", "right": 0 } ``` 但我最後發現一件事就是,如果我們綁定了`left`用來顯示資料,那我們在輸入運算符之後要怎麼將顯示的資料替換成`right`?所以仔細想了一下決定把結構改一下。然後我就仔細思考,要不繼續使用之前的寫法?但是這次我將好幾個複雜,多餘的的class,簡化成一個tuple?最終結構變成了這樣: ## 第一種狀態 ```json= { "value": 0, "operation": null } ``` ## 第二種狀態 ```json= { "value": 0, "operation": ["+", 10] } ``` 雖然做法類似,但我們將原本的設計模式(pattern)簡化了,接下來就是**Control flow**。 # Control Flow ```js= if (action === "C") { this.reset() return } if (action === "+" || action === "-" || action === "/" || action === "*") { this.calculator.execute(this.operation) this.operation = this.textToOperation(action, this.calculator.current) this.calculator.current = 0 return } if (action === "=") { this.calculator.execute(this.operation) return } this.calculator.current = parseInt(`${this.calculator.current || ""}${action}`) ``` 如你們所見,這個**control flow**老實說稍微有點複雜,我當時很喜歡用**early return**和**guard pattern**,也不是說這種寫法不好,運用的正確還是可以簡化流程的,像是查看值是不是**null**等等,但我其實可以使用一個switch來簡化著一堆邏輯,最後代碼就變成: ```js= switch (action) { case "C": // ... case "=": // ... default: // ... } ``` 之後在default加上一些查看運算符的邏輯,這部分就完成了。 # 操作 現在加上改變狀態的邏輯和其它一些東西就可以完成這個專案了,稍微分析一下每個指令會做的事: - C: 重置狀態 - =: 計算值,但是如果運算符還未被儲存將保持相同資料 - +,-,\*,\/: 紀錄這個運算符 - 數字: 將顯示的數字推到前面,把新的數字放到最後面 分析完後,我們就可以很輕易地寫好了: ```js= const OPERATORS = ["+", "-", "*", "/"] document.addEventListener('alpine:init', () => { Alpine.data('state', () => ({ value: 0, operation: null, handleClick(action) { switch (action) { case "C": this.value = 0 this.operation = null return case "=": if (this.operation !== null) { this.value = this.calculate() this.operation = null } return default: if (OPERATORS.includes(action)) { this.operation = [action, this.value] this.value = 0 } else { this.value = this.value * 10 + action } return } }, calculate() { const [operator, value] = this.operation switch (operator) { case "+": return value + this.value case "-": return value - this.value case "*": return value * this.value case "/": return value / this.value default: return value } } })) }) ``` 可以看到現在代碼非常的簡潔,簡潔到我覺得不需要使用任何解釋。使用return是因為,switch之後沒有邏輯了,比起用break,我覺得用return會比較好。 # 結論 最後我們將我們的複雜的設計拿掉了,重新分析了需求,使得誤打誤撞的多餘部分被除去掉了,最後呈現的代碼比之前好了許多,並沒有之前繞來繞去的感覺。 > 注意:並不是表示這些設計模式不好,而是我們並沒有在正確的地方使用它,沒有使用正確的方式,其實我甚至同時稍微借鑑了函數式的聯合類型(Sum types a.k.a Tagged Union a.k.a Variant Type)和指令設計模式的兩種方法。 > 注意2: 所有被改善的代碼都有可能再次被改善,這也說明,你也不需要從一開始就寫到完美,可以慢慢的一步步改善。這也代表說,我們重構後的成果也不是最完美的,如果有其他建議可以跟我提出。