上一篇:https://blog.csdn.net/weixin_42523774/article/details/105619681
· 爲何 簡化邏輯結構 單獨作爲一篇闡述?
· 如果代碼邏輯複雜,如何才能理清代碼中的浮雲,顯現出其最原本的邏輯,爲後續修改邏輯來鋪平道路。 這就需要一系列的手法,我稱之爲 技能基礎。而這些技能遵循二八法則,學到這20%,使用會佔到80%。因此學習這些手法是很有必要的。
· 由於重構用到了很多面向對象的思維,如果對C語言面向對象編程不熟悉,請查看這篇《學會C語言面向對象編程,弄清面向對象實質。》
· 首先介紹一下四個名詞的含義:
(1)提煉——增加代碼邏輯的層次,增加代碼邏輯的中間層;
(2)內聯——減少代碼邏輯的層次,幹掉代碼邏輯的中間層;
(3)組合——將代碼放到一起,增加相關性;
(4)拆分——將代碼分開放置,減少耦合。
· 下面逐步介紹各種方法:
1. 提煉函數
· 將諸多的代碼提煉成一個函數,那麼何時需要提煉呢?
· 如果你要花一段時間來理解代碼的意圖,你就需要將它提煉到一個函數中,讓人一看到這個函數就知道它在做什麼。
· 函數不要太大,我的經驗是超過6行的函數就感覺有點多了,你可能會擔心短函數會導致大量的函數調用,而影響性能,
· 但是現在短函數常常能讓編譯器的優化功能運轉更爲良好,因此不用過多擔心性能問題。
· 小函數需要有好名字,你可以從註釋中得到提示。
· 範例:
(1)無局部變量:這部分最簡單,就是把一段無影響的功能提出來
比如有一行異常打印:
printf("error happened in func!\n");
提煉成:
void print_error(void) {
printf("error happened in func!\n");
}
(2)有局部變量:
比如我想把這個func提出來,作爲一個參數輸入,就可以這樣;
提煉成:
void print_error(const char *func_name) {
printf("error happened in %s!\n", func_name);
}
(3)對局部變量再賦值:
· 這種情況就是函數修改的變量,需要在函數外使用;我們可以把參數作爲輸入,然後用返回值返回;
· 比如上面的打印函數需要記錄打印出錯的次數:
提煉成:
int print_error(const char *func_name) {
static count = 0;
printf("error %d happened in %s!\n", ++count, func_name);
return count;
}
· 如果需要返回的函數參數有多個,我建議可以用多個函數來提煉,保證只返回一個,如果實在是需要多個,建議通過封裝對象(struct)的方式範返回。
2.內聯函數
· 內聯函數就是提煉函數的反向重構。
· 當函數的內部代碼和函數名稱同樣清晰易讀,也可能是你重構了函數實現,使其內容和其名稱同樣清晰時,這樣就建議你直接使用其中的代碼。
· 間接性可能帶來幫助,但是非必要的間接性總是讓人不舒服。通過內聯手法,可以找出有用的間接層,也可以將無用的間接層去除。
做法:
找出函數的所有調用點,逐個替換。
範例:
(1)簡單情況
int rating(struct Driver_info aDriver) {
return moreThanFiveLateDeliveries(aDriver) ? 2 : 1;
}
int moreThanFiveLateDeliveries() {
return aDriver.numberOfLateDeliveries > 5;
}
內聯爲:
int rating(struct Driver_info aDriver) {
return aDriver.numberOfLateDeliveries > 5? 2 : 1;
}
(2)複雜情況:
當需要移出的函數內容很複雜時,你就需要"剪切-粘貼-調整"來進行,當調用函數衆多,則需要調整一次就測試一次;
如果你遇到了麻煩,就意味着需要使用更精細的重構手法:搬移語句到調用者(217)。
3. 提煉變量
· 有時表達式可能非常難以閱讀,這時,局部變量可以幫助我們將表達式分解爲比較容易管理的形式,讓我們理解這部分的邏輯是幹什麼的。
· 命名:如果考慮使用提煉變量,就意味着我要給代碼中的一個表達式命名。
· 如果這個名字只在當前函數中有意義,提煉變量是個不錯的選擇。
· 如果這個命名,在更寬的上下文中,我就會考慮將其暴露出來,通常以函數的形式。
範例:
(1)簡單計算提取
int price(struct goods order) {
//price is base price - quantity discount + shipping
return order.quantity * order.itemPrice -
max(0, order.quantity - 500) * order.itemPrice * 0.05 +
min(order.quantity * order.itemPrice * 0.1, 100);
}
提煉出basePrice爲:
int price(struct goods order) {
//price is base price - quantity discount + shipping
const int basePrice = order.quantity * order.itemPrice;
return basePrice - max(0, order.quantity - 500) * order.itemPrice * 0.05 +
min(basePrice * 0.1, 100);
}
再提煉出quantity discount爲:
int price(struct goods order) {
//price is base price - quantity discount + shipping
const int basePrice = order.quantity * order.itemPrice;
const quantityDiscount = max(0, order.quantity - 500) * order.itemPrice * 0.05;
return basePrice - quantityDiscount + min(basePrice * 0.1, 100);
}
最後提煉出shipping爲,修改之後,註釋也就不需要了:
int price(struct Order order) {
const int basePrice = order.quantity * order.itemPrice;
const quantityDiscount = max(0, order.quantity - 500) * order.itemPrice * 0.05;
const shipping = min(basePrice * 0.1, 100);
return basePrice - quantityDiscount + shipping;
}
(2)類對象提取
· 同樣的內容,比如在類對象中,可以如下操作:
· C語言面向對象操作,見另一篇文章《學會C語言面向對象編程,弄清面向對象實質。》
順帶介紹一下:
struct Order {
int data;
struct OrderOperations *orderOp;
};
struct OrderOperations {
int (*price)(struct Order *order);
};
int price(struct Order *order) {
//price is base price - quantity discount + shipping
return order->quantity * order->itemPrice -
max(0, order->quantity - 500) * order->itemPrice * 0.05 +
min(order->quantity * order->itemPrice * 0.1, 100);
}
struct Order* alloc_Order(void) {
#define ORDER_DEFAULT_DATA 0
static struct OrderOperations orderOp = {
.price = price,
};
struct Order* order = (struct Order* )malloc(sizeof(struct Order));
order->orderOp = orderOp;
order->data = ORDER_DEFAULT_DATA;
return order;
}
int main(int argc, char *argv[]) {
struct Order* order = alloc_Order();
printf("order price is %d.\n", order->orderOp->price(order));
return 0;
}
提煉爲:
struct OrderOperations {
int (*getPrice)(struct Order *order);
int (*getBasePrice)(struct Order *order);
int (*getQuantityDiscount)(struct Order *order);
int (*getShipping)(struct Order *order);
};
int getBasePrice(struct Order *order) {
return order->quantity * order->itemPrice;
}
int getQuantityDiscount(struct Order *order) {
return max(0, order->quantity - 500) * order->itemPrice * 0.05;
}
int getShipping(struct Order *order) {
return min(order->quantity * order->itemPrice * 0.1, 100);
}
int getPrice(struct Order *order) {
return order->getBasePrice(order) - order->getQuantityDiscount(order) + order->getShipping(order);
}
struct Order* alloc_Order(void) {
#define ORDER_DEFAULT_DATA 0
static struct OrderOperations orderOp = {
.getPrice = getPrice,
.getBasePrice = getBasePrice,
.getQuantityDiscount = getQuantityDiscount,
.getShipping = getShipping,
};
struct Order* order = (struct Order* )malloc(sizeof(struct Order));
order->orderOp = orderOp;
order->data = ORDER_DEFAULT_DATA;
return order;
}
int main(int argc, char *argv[]) {
struct Order* order = alloc_Order();
printf("order price is %d.\n", order->orderOp->price(order));
}
· 在一個簡單的對象中暫時看不出太明顯的好處,但是當這個對象很大的時候,如果找出了可以共用的行爲,賦予它獨立的概念,起個好名字,對於使用對象的人來說會很有幫助。
4.內聯變量
· 雖然有時候,變量可以給表達式提供更有意義的名字;但是有時候,這個名字並不比表達式本身更有表現力,甚至妨礙重構附近的代碼,這時就該通過內聯手法消除變量。
範例:
int basePrice = anOrder.basePrice;
return (basePrice > 1000);
內聯爲:
return anOrder.basePrice > 1000;
5.改變函數聲明
· 函數是我們將程序拆成小塊的主要方式,而這中方式其中最重要的當屬函數的名字。一個好名字能讓我一眼看出函數的用途,而不必查看其實現代碼。
· 如果看到函數名字滿足不了上面的要求,一旦發現更好的名字,就得儘快給函數改名。對於函數的參數列表也是一樣的道理。對函數的參數也是同理。
這裏說的改變函數聲明,包括函數改名,函數參數改名,函數參數增減等項。
1)簡單做法(可以一步到位)
直接將函數修改爲新的函數聲明,然後測試。
如果你既想修改函數名,又想添加參數,最好分2步做。
2)遷移式做法(不能做到一步到位)
.1.如果有必要的話,可以先對函數體內部進行重構,使得後面的提煉步驟易於開展;
.2.先提煉函數,將函數提煉成一個新的函數,如果想沿用舊函數名,建議先給新函數起一個便於查找的臨時名字;
.3.如果需要添加參數,就用之前的方式添加;
.4.測試;
.5.對舊函數使用內聯函數,釋放函數中的內容;
.6.如果新函數使用了臨時名字,在此使用改變函數聲明將其改回來。
範例:
(1)函數改名(簡單做法)
int circum(int radius) {
return 2 * PI * radius;
}
修改成:
int circumference(int radius) {
return 2 * PI * radius;
}
(2)函數改名(遷移式做法)
int circum(int radius) {
return 2 * PI * radius;
}
修改成:
int circum(int radius) {
return circumference(radius);
}
int circumference(int radius) {
return 2 * PI * radius;
}
然後測試,通過後使用內聯函數手法。
(3)添加參數
遷移式做法類似,不在重複介紹。
(4)把參數修改爲屬性
遷移式做法類似,不在重複介紹。
6.封裝變量
· 數據的重構相對於函數要麻煩得多。
· 如果想要搬移一處被廣泛使用的數據,最好的辦法是先以函數的形式封裝所有對數據的訪問。
· 對於所有的可變數據,只要它的作用域超過單個函數,我就會將其封裝起來,只允許通過函數訪問。數據的作用域越大,封裝就越重要。處理遺留代碼時,一旦需要修改或增加使用可變數據的代碼,我就會藉機把這份數據封裝起來,從而避免繼續加重耦合。面向對象的方法如此強調對象的數據應該保持私有,背後也是同樣的原理。
· 相比於封裝數據,不可變的數據更重要,不可變讓大家可以放心使用舊數據,不用做搬移,不用擔心代碼失效。
範例:
(1)全局變量
賦值:
struct Owner defaultOwner = {
.firstName = "Martin",
.lastName = "Fowler",
};
使用:
owner = defaultOwner;
更新:
defaultOwner.firstName = "Rebecca";
defaultOwner.lastName = "Parsons";
· 重構第一步:封裝成函數
· 獲取值的時候,建議不用指針,返回的內容就是副本,這樣原來數值不會被修改。獲取時建議不加get。
struct Owner defaultOwner(void) {
return defaultOwner;
}
void setDefaultOwner(struct Owner newOwner) {
defaultOwner = newOwner;
return;
}
· 重構第二步:修改變量限制
然後可以將原來的變量增加限制,比如加上 static 限制在此文件中使用,如果做不到,建議將變量取一個有意義有難看的名字。
例如 __privateOnly_defaultOwner 。本次將其改爲
static struct Owner defaultOwnerData = {
.firstName = "Martin",
.lastName = "Fowler"
};
· 重構第三步:將其封裝爲一個對象,但是C語言不支持,因此封裝成結構體是唯一選擇。前面已經做好。
7.變量改名
· 好的命名是整潔編程的核心。變量名可以很好的解釋這段程序在幹啥——如果名字起的好的話。
· 使用範圍越廣,名字的好壞就越重要。
· 我習慣將變量的類型信息也放進名字裏面,我的類型對於的名字前綴表:
char - c,
unsigned char - uc,
short - s,
unsigned short - us
int - i,
unsigned int - ui,
struct - t或對應的類型名字,
union - u,
指針 - p,
enum - e,
數組 - a,
float - f,
double - d,
· 如果變量被廣泛使用,建議使用封裝變量的方法,將其封裝起來。
· 常量改名,通常先複製這個常量,用新常量複製給舊的常量,這樣刪除就常量時會稍微快一點。
範例:此處前面變量封裝例子類似,就不做此範例。
8.引入參數對象
· 當同一組數據項經常同時出現在多個函數的參數列表中時,我喜歡代之以一個數據結構,將其組合起來。
· 這件事情的價值,在於將數據項之間關係變得明晰,而進一步圍繞該結構來捕捉共用行爲,這個結構將提升爲新的抽象概念。這個力量是強大的。
做法:
· 在該函數增加一個創建的數據結構的參數,然後一個一個的將參數項移到這個結構中,記得每一步都要保證測試通過。
範例:
int readingsOutsideRange(int station, int min, int max) {
return (station < min)? min : ((station > max)? max: station);
}
首先逐步重構爲:
struct NumberRange {
int station;
int min;
int max;
};
int readingsOutsideRange(struct NumberRange range) {
return (range.station < range.min)? range.min : ((range.station > range.max)? range.max: range.station);
}
對此結構中的數據提取最好封裝成函數:
struct NumberRange {
int station;
int min;
int max;
};
int min(struct NumberRange *range) {
return range->min;
}
int max(struct NumberRange *range) {
return range->max;
}
int readingsOutsideRange(struct NumberRange range) {
return (range.station < min(&range))? min(&range) : ((range.station > max(&range) )? max(&range): range.station);
}
9.函數組合成類
將數據組合起來,將數組的操作通過函數也放進來。
· 函數組合成類是將函數重新組織的一種方式,當一組函數形影不離地操作同一塊數據,這是就該組件一個類了。一般來說,類可以提供一套環境,可以讓我們的函數少傳許多參數,而一個對象也可以更方便的傳遞給系統的其他部分。只是在C語言中,沒有面向對象的支持,但是仍然可以將其組合成結構體使用。
做法:
(1)運用封裝記錄 對多個函數共用的數據記錄加以封裝;
(2)對於使用該記錄結構的每個函數,運用搬移函數 將其移入新類;
(3)用以處理數據記錄的邏輯,可以用提煉函數 的方法提煉出,並移入新類。
範例:
· 延續運用第8節中的例子,上一節中使用瞭如下代碼
struct NumberRange {
int station;
int min;
int max;
int (*getMin)(struct NumberRange *range);
int (*getMax)(struct NumberRange *range);
};
int min(struct NumberRange *range) {
return range->min;
}
int max(struct NumberRange *range) {
return range->max;
}
struct NumberRange* allocNumberRange(void) {
static struct OrderOperations orderOp = {
.getMin = min,
.getMax = max,
};
struct NumberRange* range = (struct NumberRange* )malloc(sizeof(struct NumberRange));
range->min = ORDER_DEFAULT_DATA;
range->max = ORDER_DEFAULT_DATA;
range->getMin = min;
range->getMax = max;
return range;
}
int readingsOutsideRange(struct NumberRange *range) {
return (range->station < min(range))? min(range) : ((range->station > max(range) )? max(range): range->station);
}
int main(void **argc,void *argv[]) {
struct NumberRange* range = allocNumberRange();
printf("OutsideRange is %d.\n", readingsOutsideRange(range));
}
· 我想把 readingsOutsideRange函數搬移到 NumberRange 的內部;
struct NumberRange {
int station;
int min;
int max;
int (*getMin)(struct NumberRange *range);
int (*getMax)(struct NumberRange *range);
int (*readingsOutsideRange)(struct NumberRange *range);/*添加*/
};
static int min(struct NumberRange *range) {
return range->min;
}
static int max(struct NumberRange *range) {
return range->max;
}
static int _readingsOutsideRange(struct NumberRange *range) {
return (range->station < min(range))? min(range) : ((range->station > max(range) )? max(range): range->station);
}
struct NumberRange* allocNumberRange(void) {
#define ORDER_DEFAULT_DATA 1
struct NumberRange* range = (struct NumberRange* )malloc(sizeof(struct NumberRange));
range->min = ORDER_DEFAULT_DATA;
range->max = ORDER_DEFAULT_DATA;
range->getMin = min;
range->getMax = max;
range->readingsOutsideRange = _readingsOutsideRange;/*添加*/
return range;
}
int main(void **argc,void *argv[]) {
struct NumberRange* range = allocNumberRange();
range->station = 3;
printf("OutsideRange is %d.\n", range->readingsOutsideRange(range));/*修改*/
}
10.函數組合成變換
將數據組合起來,將各種函數的操作集合成一個函數。
· 我們經常會將數據"喂"給一個程序,讓它產生很多派生信息。這些派生數據可能在多個不同的地方用到,而計算的邏輯也就會在多個地方重複。
我更願意將所有計算派生數據的邏輯收攏到一處,這樣可以在固定的地方找到和更新這些邏輯,避免重複。
· 本方案的替代方案是 函數組合成類,如何判斷使用哪一個呢?如果代碼中會對源數據進行更新,那麼使用類要好得多。使用變換的話,源數據更新之後會導致與派生數據不一致。
使用方式:
(1)創建一個變化函數,入參是需要變換的記錄,並直接返回記錄的值。
(2)選擇一塊邏輯,將主體移入該函數中,並將結果添加到輸出記錄中。
(3)測試並重覆上述步驟。
範例:
延續使用第8節的例子,用函數組合成變換的方式進行重構。
struct NumberRange {
int station;
int min;
int max;
};
int min(struct NumberRange *range) {
return range->min;
}
int max(struct NumberRange *range) {
return range->max;
}
int readingsOutsideRange(struct NumberRange range) {
return (range.station < min(&range))? min(&range) : ((range.station > max(&range) )? max(&range): range.station);
}
/*新增一個獲取範圍寬度的函數*/
int readingsRangeWidth(struct NumberRange range) {
return max(&range) - min(&range);
}
· 首先,readingsOutsideRange 和 readingsRangeWidth 的兩個函數是獲取的擴展信息,首先將擴展信息放入struct中,然後創建一個計算擴展信息的函數,並逐步把函數移到這裏。
struct NumberRange {
int station;
int min;
int max;
/*擴展信息*/
int outsideRange;
int rangeWidth;
};
struct NumberRange enrichReadings(struct NumberRange range) {
range.outsideRange = readingsOutsideRange(range);
range.rangeWidth = readingsRangeWidth(range);
return range;
}
· 這裏要記住,不能一步到位,沒改一步都要測試,調用的位置要非常仔細。
11.拆分階段
學會了組合,也要學會拆分。每當同一塊代碼在同時處理兩件不同的事情,我就想將其拆分成獨自的模塊,這是運用解耦的思想。因爲到了要修改的時候,我就可以單獨處理每個主題而不用考慮兩個不同的主題。
最簡潔的方法:將一大塊行爲分成順序執行的兩個階段。如果數據不符合要求,你可能需要先對輸入數據做調整。
做法:
(1)將第二階段的代碼提煉成獨立的函數,並測試。(假設只有2段需要拆分的邏輯)
(2)引入一箇中轉數據結構,將其作爲參數添加到提煉的新函數參數列表中,並測試。
(3)判斷入參是否被第一階段代碼使用,是就將入參逐步搬移到 中轉數據結構中,注意每次搬移都要測試一次;
(4)對第一階段的代碼運用提煉函數,將提煉出的函數返回中轉數據結構。
範例:
這裏有一個計算價格的函數:
struct Product {
int basePrice;
int discountThreshold;
int discountRate;
};
struct ShippingMethod {
int discountedFee;
int feePerCase;
int discountThreshold;
};
int priceOrder(struct Product product, int quantity, struct ShippingMethod shippingMethod) {
const int basePrice = product.basePrice * quantity;
const int discount = max(quantity - product.discountThreshold, 0) * product.basePrice * product.discountRate;
const int shippingPerCase = (basePrice > shippingMethod.discountThreshold)? shippingMethod.discountedFee : shippingMethod.feePerCase;
const int shippingCost = quantity * shippingPerCase;
int price = basePrice - discount + shippingCost;
return price;
}
· 該函數中有點混亂,需要逐步拆分,首先拆出shipping配送相關的部分;
int priceOrder(struct Product product, int quantity, struct ShippingMethod shippingMethod) {
int basePrice = product.basePrice * quantity;
int discount = max(quantity - product.discountThreshold, 0) * product.basePrice * product.discountRate;
int price = applyShipping(basePrice, shippingMethod, quantity, discount);
return price;
}
int applyShipping(int basePrice, struct ShippingMethod shippingMethod, int quantity, int discount) {
int shippingPerCase = (basePrice > shippingMethod.discountThreshold)? shippingMethod.discountedFee : shippingMethod.feePerCase;
int shippingCost = quantity * shippingPerCase;
int price = basePrice - discount + shippingCost;
return price;
}
· 增加一個結構,審視各個參數,將函數參數逐步放入。shippingMethod第一階段沒用到,可以不放,quantity這個參數可以繼續選擇放不放,我還是想儘可能放進去,得到如下代碼。
struct PriceData {
int basePrice;
int quantity;
int discount;
};
int priceOrder(struct Product product, int quantity, struct ShippingMethod shippingMethod) {
int basePrice = product.basePrice * quantity;
int discount = max(quantity - product.discountThreshold, 0) * product.basePrice * product.discountRate;
struct PriceData priceData = {basePrice, quantity, discount};/*新建結構體*/
int price = applyShipping(priceData, shippingMethod/*, basePrice, quantity, discount*/);
return price;
}
int applyShipping(struct PriceData priceData, struct ShippingMethod shippingMethod/*, int basePrice, int quantity, int discount*/) {
int shippingPerCase = (priceData.basePrice > shippingMethod.discountThreshold)? shippingMethod.discountedFee : shippingMethod.feePerCase;
int shippingCost = priceData.quantity * shippingPerCase;
int price = priceData.basePrice - priceData.discount + priceData.shippingCost;
return price;
}
· 最後將第一階段也組合成一個函數:
struct PriceData {
int basePrice;
int quantity;
int discount;
};
/*新的函數,計算出priceData*/
struct priceData caculatePriceData(struct Product product, int quantity) {
struct PriceData priceData;
priceData.basePrice = product.basePrice * quantity;
priceData.discount = max(quantity - product.discountThreshold, 0) * product.basePrice * product.discountRate;
priceData.quantity = quantity;
return priceData;
}
int priceOrder(struct Product product, int quantity, struct ShippingMethod shippingMethod) {
/*int basePrice = product.basePrice * quantity;
int discount = max(quantity - product.discountThreshold, 0) * product.basePrice * product.discountRate;*/
struct PriceData priceData = caculatePriceData(product, quantity);{basePrice, quantity, discount};/*新建結構體*/
int price = applyShipping(priceData, shippingMethod/*, basePrice, quantity, discount*/);
return price;
}
int applyShipping(struct PriceData priceData, struct ShippingMethod shippingMethod/*, int basePrice, int quantity, int discount*/) {
int shippingPerCase = (priceData.basePrice > shippingMethod.discountThreshold)? shippingMethod.discountedFee : shippingMethod.feePerCase;
int shippingCost = priceData.quantity * shippingPerCase;
int price = priceData.basePrice - priceData.discount + priceData.shippingCost;
return price;
}
最後去掉註釋,整理下,得最終結果。
struct PriceData {
int basePrice;
int quantity;
int discount;
};
int priceOrder(struct Product product, int quantity, struct ShippingMethod shippingMethod) {
struct PriceData priceData = caculatePriceData(product, quantity);
return applyShipping(priceData, shippingMethod/*, basePrice, quantity, discount*/);
}
/*第一階段計算出priceData*/
struct priceData caculatePriceData(struct Product product, int quantity) {
struct PriceData priceData;
priceData.basePrice = product.basePrice * quantity;
priceData.discount = max(quantity - product.discountThreshold, 0) * product.basePrice * product.discountRate;
priceData.quantity = quantity;
return priceData;
}
/*第二階段計算出price*/
int applyShipping(struct PriceData priceData, struct ShippingMethod shippingMethod) {
int shippingPerCase = (priceData.basePrice > shippingMethod.discountThreshold)? shippingMethod.discountedFee : shippingMethod.feePerCase;
int shippingCost = priceData.quantity * shippingPerCase;
return priceData.basePrice - priceData.discount + priceData.shippingCost;
}
12. 總結
· 本文介紹了簡化邏輯結構的各種方法,主要思想就是用 2 對武器 {提煉,內聯},{組合,拆分}。就像2把快刀,將程序修剪得整整齊齊。這2對武器大部分情況下都是夠用的了。
· 學到東西,就要去實踐,實踐中出現的問題,也請大家給我留言反饋,謝謝!