最近接手了一個公司的老系統的PHP項目,里面的代碼比較混亂,排查解決了一個問題,決定將這個思路記錄下來,希望能幫助更多的人。
其中一部分的代碼信息如下:
備注:為了避免公司的相關數據信息暴露,我對原本的代碼邏輯中的一些變量和文字關鍵詞進行了替換,但是整體的代碼流程沒有發生變化。因為,本篇文章我主要想分享一種解決問題的思路。
對于以上代碼,我相信稍微有個幾年經驗的程序員看著都會頭大。沒錯,我看了之后也是非常頭大。這段代碼中存在以下幾個重大問題:
- 代碼中出現了很多枚舉值,例如
1/0/3/1/2
,這些枚舉值大概率是數據表中的某些int類型的數據,應該放到常量里面; - 代碼中有許多
Log::info(...)
的部分,直接寫入日志,不方便擴展。另外,寫入日志的文本也沒有封裝,存在大量重復的中文文本,如果需要修改,涉及到多處修改,而且,也不方便將來擴展多語言。 - 這個方法被引用的地方太多,現在需要增加一個參數,需要修改的地方太多,稍不注意就會出現問題。
當然,這個方法還有更多其它的問題,就不一一分析了。針對這類前人留下大坑的代碼,這里我主要分享兩個思路,基本適用于各類大坑代碼。
首先,這類已經穩定運行的代碼, 別管有多亂,非必要千萬別動,一不小心就會出現各種問題。但是,現在產品要求在這個業務中增加一個字段,傳統的做法,可能是在方法體后面增加一個參數。例如,原本的方法體:
public function calculateCookie($goods_id, $user_id, $sys_id, $list = [])
增加參數后的方法體:
public function calculateCookie($goods_id, $user_id, $sys_id, $list = [], $new_param)
這樣確實能解決問題,但是,調用這個方法的所有的上層代碼都需要加一個參數。就算把這個參數設置一個默認值,也依然有風險。而且,如果這個方法的上層鏈路特別長,那么,需要將原始參數層層傳遞下來,非常麻煩,而且會有風險。比如下面這樣:
按照傳統的思路,就得這么改:
你想想,如果這個方法的上層鏈路像老太太的裹腳布一樣,又臭又長,你稍微不留神,在哪一個環節忘記加參數,就出錯了。所以,最好不要這么玩。這個時候就可以考慮定義一個全局變量,專門處理這次的改動點。
仔細梳理一下這個調用鏈路:
step1()->step2()->step31()->calculateCookie()
上面傳統的方法是逐層傳遞,就像是接力賽一樣,上游傳遞給下游。那么,換一種思路,我直接從step1()
把參數傳遞到calculateCookie()
可不可以?答案是:當然可以。而且這樣做,簡單高效,省去了“中間商賺差價”。
具體實現方法:
新增一個公共類文件,定義一個全局變量:
<?php
class common{public static $new_param;
}
然后在鏈路的最上層直接給這個全局變量賦值:
public function step1($goods_id, $user_id)
{//.... 省略更多代碼//$new_param = '這是我新增的參數,需要一層一層傳遞下去...';common::$new_param = '這是我新增的參數,可以一步到位啦!';return $this->step2($goods_id, $user_id, 100);
}
然后在需要使用的地方,直接使用:
public function calculateCookie($goods_id, $user_id, $sys_id, $list = [])
{if (common::$new_param == 'hello') {//.....}
}
這樣一來,直接省去了中間鏈路的調用過程,一步直達。如果能確定這個新增的參數只在當前類的業務中使用,也可以直接定義到當前類下面:
其實,這種思路也可以應用在需要記錄和使用全局變量的場景下。比如,我需要記錄一次請求生命周期的唯一ID,用來記錄到日志的traceId
中,就可以在接口請求的入口中設定好一個全局變量值(比如時間戳),然后在需要記錄日志的地方讀取這個全局變量。
另外,在我排查這個代碼塊的時候,發現里面的業務邏輯很長很復雜,我也沒有時間和精力去分析這堆代碼的背景。但是,現在有個問題,我需要復用這個代碼塊,并且把里面的 Log::info(...)
部分的內容提取出來。這個方法是 thinkphp框架自帶的一個方法,用來寫入日志內容。
我現在想把這個方法塊中好幾十個的寫入日志的內容收集起來,并且盡可能少的改動原有的代碼邏輯。我就需要使用到全局變量的思維。
首先,我要改造上面的方法,在這個class上面定義兩個全局變量和set/get方法:
public $is_set_log_context = false; //定義一個全結局的標記,默認false
public $log_context = []; //記錄全局的日志內容,存儲到數組中// 設置全局標記為true
public function setLogContext()
{$this->is_set_log_context = true;
}
//讀取全局的日志數組內容,并將全局標記改為false
public function returnLogContext()
{$this->is_set_log_context = false;return $this->log_context;
}
然后修改上面的方法如下:
public function info($message, array $context = []): void
{if($this->is_set_log_context){ //如果設置了全局標記$set_context = $message;if(!empty($context)){$set_context.="context:".returnjson($context);}$this->log_context[] = $set_context; //則將本次的日志內容收集到全局的日志內容數組中}$this->log(__FUNCTION__, $message, $context);
}
效果如下:
然后,在入口方法處調用一下:
public function step1($goods_id, $user_id)
{Log::setLogContext(); //設置全局標記入口$result = $this->step2($goods_id, $user_id, 100);$log_info_context = Log::returnLogContext(); //讀取本次收集的日志數組內容,并將全局標記設為falseprint_r($log_info_context); //提取到所有的 Log::info(....) 中的內容,存儲到數組中return $result;
}
這樣一來,我在不改動calculateCookie()
方法塊的任何內容的情況下,把里面的Log::info(...)
的內容收集起來了。
這個思路,不僅限于PHP語言,其它的編程語言也類似,你可以自由發揮。