新聞中心
- 分解條件表達式
- 合并條件表達式
- 以衛(wèi)語句替代嵌套條件表達式
- 以多態(tài)取代條件表達式
- 引入特例
- 引入斷言
條件表達式易產(chǎn)生的問題:

- 復雜度極高: 表現(xiàn)是if嵌套兩三層設置更多
- 大型函數(shù)可讀性下降: 不知道為什么會發(fā)生這樣事情
重構手法1: 分解條件表達式
和任何大塊頭代碼一樣,我可以將它分解為多個獨立的函數(shù),根據(jù)每個小塊代碼的用途,為分解而得的新函數(shù)命名,并將原函數(shù)中對應的代碼改為調用新函數(shù),從而更清楚地表達自己的意圖。對于條件邏輯,將每個分支條件分解成新函數(shù)還可以帶來更多好處:可以突出條件邏輯,更清楚地表明每個分支的作用,并且突出每個分支的原因。本重構手法其實只是提煉函數(shù)的一個應用場景”。
if (!aDate.isBefore(plan.summerStart) && !aDate.isAfter(plan.summerEnd))
charge = quantity * plan.summerRate;
else
charge = quantity * plan.regularRate + plan.regularServiceCharge;
進行重構后將條件提煉到一個獨立的函數(shù), 用三元運算符重新安排條件語句 :
charge = summer() ? summerCharge() : regularCharge();
function summer() {
return !aDate.isBefore(plan.summerStart) && !aDate.isAfter(plan.summerEnd);
}
function summerCharge() {
return quantity * plan.summerRate;
}
function regularCharge() {
return quantity * plan.regularRate + plan.regularServiceCharge;
}
重構手法2: 合并條件表達式
檢查條件各不相同,最終行為卻一致。如果發(fā)現(xiàn)這種情況,就應該使用“邏輯或”和“邏輯與”將它們合并為一個條件表達式。
之所以要合并條件代碼,有兩個重要原因。首先,合并后的條件代碼會表述“實際上只有一次條件檢查,只不過有多個并列條件需要檢查而已”,從而使這一次檢查的用意更清晰。當然,合并前和合并后的代碼有著相同的效果,但原先代碼傳達出的信息卻是“這里有一些各自獨立的條件測試,它們只是恰好同時發(fā)生”。其次,這項重構往往可以為使用提煉函數(shù)(106)做好準備。將檢查條件提煉成一個獨立的函數(shù)對于厘清代碼意義非常有用,因為它把描述“做什么”的語句換成了“為什么這樣做”。
“function disabilityAmount(anEmployee) {
if ((anEmployee.seniority < 2)
|| (anEmployee.monthsDisabled > 12)) return 0;
if (anEmployee.isPartTime) return 0;這句條件表達式使用提煉函數(shù) ,使用邏輯與與邏輯或:
function disabilityAmount(anEmployee) {
if (isNotEligableForDisability()) return 0;
// compute the disability amount
function isNotEligableForDisability() {
return ((anEmployee.seniority < 2)
|| (anEmployee.monthsDisabled > 12)
|| (anEmployee.isPartTime));
}重構手法3: 以衛(wèi)語句替代嵌套條件表達式
條件表達式通常有兩種風格,一種是兩個條件分支都屬于正常行為,第二種風格是只有一個分支是屬于正常行為,另一個分支則是異常行為。如果兩個分支都屬于正常行為,就應該使用例如if else條件表達式;如果某個條件極其罕見,就應該“就應該單獨檢查該條件,并在該條件為真時立刻從函數(shù)中返回。這樣的單獨檢查常常被稱為“衛(wèi)語句"
以衛(wèi)語句取代嵌套條件表達式的精髓就是:給某一條分支以特別的重視。如果使用if-then-else結構,你對if分支和else分支的重視是同等的。這樣的代碼結構傳遞給閱讀者的消息就是:各個分支有同樣的重要性。衛(wèi)語句就不同了,它告訴閱讀者:“這種情況不是本函數(shù)的核心邏輯所關心的,如果它真發(fā)生了,請做一些必要的整理工作,然后退出。
每個函數(shù)只能有一個入口和一個出口”的觀念,根深蒂固于某些程序員的腦海里。我發(fā)現(xiàn),當我處理他們編寫的代碼時,經(jīng)常需要使用以衛(wèi)語句取代嵌套條件表達式?,F(xiàn)今的編程語言都會強制保證每個函數(shù)只有一個入口,至于“單一出口”規(guī)則,其實不是那么有用。在我看來,保持代碼清晰才是最關鍵的:如果單一出口能使這個函數(shù)更清楚易讀,那么就使用單一出口;否則就不必這么做。
以下例子中的代碼可能我們很多人都寫過,我現(xiàn)在認為它是壞味道的代碼:
function payAmount(employee) {
let result;
if(employee.isSeparated) {
result = {amount: 0, reasonCode:"SEP"};
}
else {
if (employee.isRetired) {
result = {amount: 0, reasonCode: "RET"};
}
else {
// logic to compute amount
lorem.ipsum(dolor.sitAmet);1
consectetur(adipiscing).elit();
sed.do.eiusmod = tempor.incididunt.ut(labore) && dolore(magna.aliqua);
ut.enim.ad(minim.veniam);
result = someFinalComputation();
}
}
return result;思考下位語句的概念,如果用位語句對其進行清晰的重構:
首先我們可以對isDead以及isSeparated進行改寫位語句。
function payAmount(employee) {
let result;
if (employee.isSeparated) return {amount: 0, reasonCode: "SEP"};
if (employee.isRetired) return {amount: 0, reasonCode: "RET"};
xxxxxxxxxx
return someFinalComputation();
}有的時候不容易使用位語句,我們可以使用條件反轉來實現(xiàn)以衛(wèi)語句取代嵌套條件表達式。
function adjustedCapital(anInstrument) {
let result = 0;
if (anInstrument.capital > 0) {
if (anInstrument.interestRate > 0 && anInstrument.duration > 0) {
result = (anInstrument.income / anInstrument.duration) * anInstrument.adjustmentFactor;
}
}
return result;
}位語句使用重點在與處理一條分支的正常情況的另一種特殊情況,即大多的少走的else,所以這次想將anInstrument.capital >0用位語句表示出來:
function adjustedCapital(anInstrument) {
let result = 0;
if (anInstrument.capital <= 0) return result;
if (anInstrument.interestRate > 0 && anInstrument.duration > 0) {
result = (anInstrument.income / anInstrument.duration) * anInstrument.adjustmentFactor;
}
return result;
}下一步將anInstrument.interestRate >0&& anInstrument.duration >0進行反轉。
function adjustedCapital(anInstrument) {
let result = 0;
if (anInstrument.capital <= 0) return result;
if (!(anInstrument.interestRate > 0 && anInstrument.duration > 0)) return result;
result = (anInstrument.income / anInstrument.duration) * anInstrument.adjustmentFactor;
return result;
}!(anInstrument.interestRate >0&& anInstrument.duration >0) 看起來并不好,進行改變“anInstrument.interestRate <= 0 || anInstrument.duration <= 0)并且使用重構方法2 合并條件表達式進行。
“function adjustedCapital(anInstrument) {
let result = 0;
if ( anInstrument.capital <= 0
|| anInstrument.interestRate <= 0
|| anInstrument.duration <= 0) return result;
result = (anInstrument.income / anInstrument.duration) * anInstrument.adjustmentFactor;
return result;
} 最使用去除多余變量進行將result去除。
“function adjustedCapital(anInstrument) {
if ( anInstrument.capital <= 0
|| anInstrument.interestRate <= 0
|| anInstrument.duration <= 0) return 0;
return (anInstrument.income / anInstrument.duration) * anInstrument.adjustmentFactor;
}重構手法4: 以多態(tài)取代條件表達式
以上介紹的重構的手法基本上是實用條件邏輯本身的結構就足以表達,但是我想尋求給條件邏輯添加結構,實用類和多態(tài)可以把邏輯的拆分表述的更加清晰,一個常見的場景是:我可以構造一組類型,每個類型處理各自的一種條件邏輯。例如,我會注意到,圖書、音樂、食品的處理方式不同,這是因為它們分屬不同類型的商品。最明顯的征兆就是有好幾個函數(shù)都有基于類型代碼的switch語句。若果真如此,我就可以針對switch語句中的每種分支邏輯創(chuàng)建一個類,用多態(tài)來承載各個類型特有的行為,從而去除重復的分支邏輯。
另一種情況是:有一個基礎邏輯,在其上又有一些變體?;A邏輯可能是最常用的,也可能是最簡單的。我可以把基礎邏輯放進超類,這樣我可以首先理解這部分邏輯,暫時不管各種變體,然后我可以把每種變體邏輯單獨放進一個子類,其中的代碼著重強調與基礎邏輯的差異。
多態(tài)一直是解決復雜條件邏輯改善這種情況工具,并非所有條件邏輯都要用多態(tài)來取代,避免濫用。
例子:
function plumages(birds) {
return new Map(birds.map(b => [b.name, plumage(b)]));
}
function speeds(birds) {
return new Map(birds.map(b => [b.name, airSpeedVelocity(b)]));
}
function plumage(bird) {
switch (bird.type) {
case 'EuropeanSwallow':
return "average";
case 'AfricanSwallow':
return (bird.numberOfCoconuts > 2) ? "tired" : "average";
case 'NorwegianBlueParrot':
return (bird.voltage > 100) ? "scorched" : "beautiful";
default:
return "unknown";
}
}
function airSpeedVelocity(bird) {
switch (bird.type) {
case 'EuropeanSwallow':
return 35;
case 'AfricanSwallow':
return 40 - 2 * bird.numberOfCoconuts;
case 'NorwegianBlueParrot':
return (bird.isNailed) ? 0 : 10 + bird.voltage / 10;
default:
return null;
}
}使用多態(tài)進行重構的標志是,對于同一個對象或者屬性進行判斷邏輯操作,有兩個不同的操作,其行為都隨著“鳥的類型”發(fā)生變化,因此可以創(chuàng)建出對應的類,用多態(tài)來處理各類型特有的行為。
function plumages(birds) {
return new Map(birds
.map(b => createBird(b))
.map(bird => [bird.name, bird.plumage]));
}
function speeds(birds) {
return new Map(birds
.map(b => createBird(b))
.map(bird => [bird.name, bird.airSpeedVelocity]));
}
function createBird(bird) {
switch (bird.type) {
case 'EuropeanSwallow':
return new EuropeanSwallow(bird);
case 'AfricanSwallow':
return new AfricanSwallow(bird);
case 'NorwegianBlueParrot':
return new NorwegianBlueParrot(bird);
default:
return new Bird(bird);
}
}
class Bird {
constructor(birdObject) {
Object.assign(this, birdObject);
}
get plumage() {
return "unknown";
}
get airSpeedVelocity() {
return null;
}
}
class EuropeanSwallow extends Bird {
get plumage() {
return "average";
}
get airSpeedVelocity() {
return 35;
}
}
class AfricanSwallow extends Bird {
get plumage() {
return (this.numberOfCoconuts > 2) ? "tired" : "average";
}
get airSpeedVelocity() {
return 40 - 2 * this.numberOfCoconuts;
}
}
class NorwegianBlueParrot extends Bird {
get plumage() {
return (this.voltage > 100) ? "scorched" : "beautiful";
}
get airSpeedVelocity() {
return (this.isNailed) ? 0 : 10 + this.voltage / 10;
}
}重構手法5: 引入特例
該手法主要是對一些特殊的值進行邏輯判斷,之后才會進行處理,“一個數(shù)據(jù)結構的使用者都在檢查某個特殊的值,并且當這個特殊值出現(xiàn)時所做的處理也都相同。如果我發(fā)現(xiàn)代碼庫中有多處以同樣方式應對同一個特殊值,我就會想要把這個處理邏輯收攏到一處。處理這種情況的一個好辦法是使用“特例”(Special Case)模式:創(chuàng)建一個特例元素,用以表達對這種特例的共用行為的處理。這樣我就可以用一個函數(shù)調用取代大部分特例檢查邏輯。
特例有幾種表現(xiàn)形式。如果我只需要從這個對象讀取數(shù)據(jù),可以提供一個字面量對象(literal object),其中所有的值都是預先填充好的。如果除簡單的數(shù)值之外還需要更多的行“為,就需要創(chuàng)建一個特殊對象,其中包含所有共用行為所對應的函數(shù)。特例對象可以由一個封裝類來返回,也可以通過變換插入一個數(shù)據(jù)結構。一個通常需要特例處理的值就是null,這也是這個模式常被叫作“Null對象”(Null Object)模式的原因——Null對象是特例的一種特例。
我們會首先創(chuàng)建一個關于判斷特例對象為真的一個類,這個類具有這個特例為null的所有屬性以及方法,我們將對即將要對某個對象的屬性的獲取判斷,轉化為代理這個值,如果存在就使用正常對象,如果不存在就要使用我們創(chuàng)建的那個特例對象的屬性以及方法了。
Class Site {
get customer() {return this._customer;}
}
class Customer{
get name() {...}
get billingPlan() {...}
set billingPlan(arg) {...}
get paymentHistory() {...}
}
client one
“const aCustomer = site.customer;
// ... lots of intervening code ...
let customerName;
if (aCustomer === "unknown") customerName = "occupant";
else customerName = aCustomer.name;”
clint two
const plan = (aCustomer === "unknown") ?
registry.billingPlans.basic
: aCustomer.billingPlan;
clint three
const weeksDelinquent = isUnknown(aCustomer) ?
0
: aCustomer.paymentHistory.weeksDelinquentInLastYear;改寫:
class Site {
get customer() {
return (this._customer === "unknown") ? createUnknownCustomer() : this._customer;
}
}
function createUnknownCustomer() {
return {
isUnknown: true,
name: "occupant",
billingPlan: registry.billingPlans.basic,
paymentHistory: {
weeksDelinquentInLastYear: 0,
}
};
}
function isUnknown(arg) {
return arg.isUnknown;
}
const customerName = aCustomer.name;
const plan = aCustomer.billingPlan;
const weeksDelinquent = aCustomer.paymentHistory.weeksDelinquentInLastYear;重構手法6:引入斷言
這樣的假設通常并沒有在代碼中明確表現(xiàn)出來,你必須閱讀整個算法才能看出。有時程序員會以注釋寫出這樣的假設,而我要介紹的是一種更好的技術——使用斷言明確標明這些假設?!皵嘌允且粋€條件表達式,應該總是為真。如果它失敗,表示程序員犯了錯誤。
斷言的失敗不應該被系統(tǒng)任何地方捕捉。整個程序的行為在有沒有斷言出現(xiàn)的時候都應該完全一樣。實際上,有些編程語言中的斷言可以在編譯期用一個開關完全禁用掉。“??匆娪腥斯膭钣脭嘌詠戆l(fā)現(xiàn)程序中的錯誤。這固然是一件好事,但卻不是使用斷言的唯一理由。斷言是一種很有價值的交流形式——它們告訴閱讀者,程序在執(zhí)行到這一點時,對當前狀態(tài)做了何種假設。另外斷言對調試也很有幫助。而且,因為它們在交流上很有價值,即使解決了當下正在追蹤的錯誤,我還是傾向于把斷言留著。自測試的代碼降低了斷言在調試方面的價值,因為逐步逼近的單元測試通常能更好地幫助調試,但我仍然看重斷言在交流方面的價值。
假設這擴率永遠為整數(shù)。
applyDiscount(aNumber) {
return (this.discountRate)
? aNumber - (this.discountRate * aNumber)
: aNumber;
}先將三元改成if-else形式。
applyDiscount(aNumber) {
if (!this.discountRate) return aNumber;
else return aNumber - (this.discountRate * aNumber);
}
------------------
加入斷言
applyDiscount(aNumber) {
if (!this.discountRate) return aNumber;
else {
assert(this.discountRate >= 0);
return aNumber - (this.discountRate * aNumber);
}
}注意,不要濫用斷言。我不會使用斷言來檢查所有“我認為應該為真”的條件,只用來檢查“必須為真”的條件。濫用斷言可能會造成代碼重復,尤其是在處理上面這樣的條件邏輯時。所以我發(fā)現(xiàn),很有必要去掉條件邏輯中的重復,通??梢越柚釤捄瘮?shù)手法。我只用斷言預防程序員的錯誤。如果要從某個外部數(shù)據(jù)源讀取數(shù)據(jù),那么所有對輸入值的檢查都應該是程序的一等公民,而不能用斷言實現(xiàn)——除非我對這個外部數(shù)據(jù)源有絕對的信心。斷言是幫助我們跟蹤bug的最后一招,所以,或許聽來諷刺,只有當我認為斷言絕對不會失敗的時候,我才會使用斷言。
網(wǎng)頁標題:Javascript條件邏輯設計重構
文章網(wǎng)址:http://www.5511xx.com/article/djcoije.html


咨詢
建站咨詢
