复杂项目中通过使用全局变量解决问题的思维方式
最近接手了一个公司的老系统的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语言,其它的编程语言也类似,你可以自由发挥。