Ch13: 讓不良的程式碼凸顯出來
反重構
- 將不良的程式碼凸顯出來,能一眼看出的做法
- 在確保不會破壞程式碼的前提下,故意將程式寫爛
發出流程問題的訊號
- 我們沒辦法總是將程式寫得好
- 程式碼或問題本身的複雜性
- 時間不夠
- 不確定該怎麼做
- 交付「糟糕的程式碼」會比交付「將問題隱藏起來的程式碼」來的好
Discussion
下列兩個範例,哪個比較好?
Example1
function animate() {
handleChosen();
handleDisplaying();
handleCompleted();
handleMoving();
}
function handleChosen() {
if (value >= threshold
&& banner.state === "chosen") {
// ...
}
}
function handleDisplaying() {
if (value >= target
&& banner.state === "displaying") {
// ...
}
}
function handleCompleted() {
if (banner.state === "completed") {
// ...
}
}
Example2
function animate() {
// FIXME: All concern banner.state
if (value >= threshold
&& banner.state === State.Chosen) {
// ...
}
if (value >= target
&& banner.state === State.Displaying) {
// ...
}
if (banner.state === State.Completed) {
// ...
}
}
分離優質和遺留程式碼
- 程式碼越糟,就越容易被發現
- 不可能寫出沒有問題的程式碼
- 迅速發現問題很重要
- 「如果沒有辦法做得很好,就讓他顯眼一點」
- Quite good > Bad > Good enough
- 重構是連鎖反應
- 為了讓程式碼變好,意味著需要讓周圍的程式碼也變好
- 為了避免「破窗效應」
- 💔 反正這裡的code寫得很糟,我寫糟在旁邊也不會怎麼樣
- ✅ 把好的code和壞的code分開放,在好code的旁邊會減少出現糟糕的code的機會
不良程式碼的特徵
簡單而具體 - 本書的規則
違背良好規則的程式碼
function minimum(arr: number[][]) {
let result = 99999;
for (let x = 0; x < arr.length; x++) {
for (let y = 0; y < arr[x].length; y++) {
if (arr[x][y] < result)
result = arr[x][y];
}
}
return result;
}
程式碼異味
- 簡單的異味:「魔法數字」「重複的程式碼」
- 有的很難,需要練習重構一段時間才會吸引你的目光
function minimum(arr: number[][]) {
let result = 99999; // Bad smells: Magic number
for (let x = 0; x < arr.length; x++) {
for (let y = 0; y < arr[x].length; y++) {
if (arr[x][y] < result)
result = arr[x][y];
}
}
return result;
}
循環複雜度 - 客觀指標
- 一個迴圈、一個if,都會增加循環複雜度(cyclomatic complexity)
- 通常可以用縮排深度計算
function minimum(arr: number[][]) { // +1
let result = 99999;
for (let x = 0; x < arr.length; x++) { // +1
for (let y = 0; y < arr[x].length; y++) { // +1
if (arr[x][y] < result) // +1
result = arr[x][y];
}
}
return result;
}
// total cyclomatic complexity = 4
認知複雜度 - 主觀指標
- 「讀取這段code的用途,需要在腦袋中記住多少資訊量」
- 更接近人們閱讀code的評估
function minimum(arr: number[][]) {
let result = 99999;
for (let x = 0; x < arr.length; x++) { // +1
for (let y = 0; y < arr[x].length; y++) { // +2
if (arr[x][y] < result) // +3
result = arr[x][y];
}
}
return result;
}
// total cognitive complexity = 6
破壞程式碼
- 不要破壞正確的資訊
- 不要讓未來的重構變困難
- 結果應該要引人注目
使用enum (4.1.3的反重構)
Before
class Package {
private priority: boolean; scheduleDispatch() {
if (this.priority)
dispatchImmediately(this);
else
queue.push(this);
}
}
After
enum Importance {
Priority,
Regular
}
class Package {
private priority: Importance; scheduleDispatch() {
if (this.priority === Importance.Priority)
dispatchImmediately(this);
else
queue.push(this);
}
}
使用整數和字串作為型別碼
String as Type
functionarea(width:number, shape:string) {
if (shape === "circle")
return (width/2) * (width/2) * Math.PI;
else if (shape === "square")
return width * width;
}
Integer as Type
const CIRCLE = 0;
const SQUARE = 1;
function area(width: number, shape: number) {
if (shape === CIRCLE)
return (width/2) * (width/2) * Math.PI;
else if (shape === SQUARE)
return width * width;
}
將魔法數字加入程式碼中
- 幾乎所有人看到magic number都會有反應
- 💔 會不會感謝你的用心良苦就難說了
- ✅ 這種code很容易被改正
Before
const FOUR_THIRDS = 4/3;
class Sphere {
volume() {
let result = FOUR_THIRDS;
for (let i = 0; i < 3; i++)
result = result * this.radius;
return result * Math.PI;
}
}
After
class Sphere {
volume() {
let result = 4/3; // Magic
for (let i = 0; i < 3; i++)
result = result * this.radius;
return result * 3.141592653589793; // Magic
}
}
新增註釋到程式碼中 (Ch8的反重構)
Before
function subMin(arr: number[][]) {
let min = Number.POSITIVE_INFINITY;
for (let x = 0; x < arr.length; x++) {
for(let y = 0; y < arr[x].length; y++) {
min = Math.min(min, arr[x][y]);
}
}
}
for (let x = 0; x < arr.length; x++) {
for(let y = 0; y < arr[x].length; y++) {
arr[x][y] -= min;
}
}
After
function subMin(arr: number[][]) {
// Find miniumn
let min = Number.POSITIVE_INFINITY;
for (let x = 0; x < arr.length; x++) {
for(let y = 0; y < arr[x].length; y++) {
min = Math.min(min, arr[x][y]);
}
}
}
// Sub from each element
for (let x = 0; x < arr.length; x++) {
for(let y = 0; y < arr[x].length; y++) {
arr[x][y] -= min;
}
}
在程式碼中加入空白
Before
let cursor = cursor+1 % arr.length;
// 可能不是你想要的
After
let cursor = (cursor + 1) % arr.length;
// 凸顯出優先序,而不是依賴運算子本身的優先序
Javascript與空白
在JS中,空白與斷行不會被視為執行的一部份
// Initial variable
let init = 20
// Run IIFE
((data) => {
console.log(data+1)
})(init)
依據命名對事物分組 (11.6的反模式)
- 透過命名的共同字首、字尾,凸顯出參數間的關係
- 後續執行規則「封裝資料」進行修正
Before
class PopupWindow {
private windowPosition: Point2d;
private hasFocus: number;
private screenWidth: number;
private screenHeight: number;
private windowSize: Point2d;
}
After
class PopupWindow {
private windowPosition: Point2d; // prefix
private windowSize: Point2d; // prefix
private hasFocus: number;
private screenWidth: number; // prefix
private screenHeight: number; // prefix
}
在名稱中加上脈絡
Before
function avg(arr: number[]) {
return sum(arr) / size(arr);
}
function size(arr: number[]) {
return arr.length;
}
function sum(arr: number[]) {
let sum = 0;
for (let i = 0; i < arr.length; i++) {
sum += arr[i];
}
return sum;
}
After
function avg_ArrUtil(arr: number[]) {
return sum(arr) / size(arr);
}
function size_ArrUtil(arr: number[]) {
return arr.length;
}
function sum_ArrUtil(arr: number[]) {
let sum = 0;
for (let i = 0; i < arr.length; i++) {
sum += arr[i];
}
return sum;
}
建立長方法
- 指行數很長的function
- 很明顯,引人注目想回來改它
function animate() {
if (value >= threshold
&& banner.state === State.Chosen) {
// ...
}
if (value >= target
&& banner.state === State.Displaying) {
// ...
}
if (banner.state === State.Completed) {
// ...
}
}
讓方法有很多參數
- 作者最喜歡的一個提醒的方法
function stringConstructor(
prefix: string, // 參數很多,引人注目
joiner: string,
postfix: string,
parts: string[]
) {
return prefix + parts.join(joiner) + postfix;
}
使用getter和setter (Ch6的反重構)
- 避免使用全域變數或公開欄位的妥協做法
class Screen {
constructor(
private width: number,
private height: number
) {}
getWidth() { return this.width; }
getHeight() { return this.height; }
}
let screen: Screen;