业务代码风格的几个建议
关于循环体内重复调用的问题
代码示例:
// $array是一个可能超出1000个数据的大变量 foreach ($array as $ar) { $model = new Model(); $modelData = $model->findOne(['id' => $ar['id']]); $modelData['is_less'] = $modelData['tag'] == 3 ? true : false; $model2 = new AnotherModel(); $model2->find(['id' => $ar['id']])->update(['is_less' => $modelData['is_less']]); }
以上代码块是有非常严重问题的。
在循环体中,不能重复使用数据库查询太多次,尤其是相似或一致的sql,一定要批量查询获取数据之后再做相应逻辑层面的处理。如果循环次数较多,不仅仅会体现在循环逻辑较慢上,而且在并发读写的业务中由于频繁读取硬盘以及锁表等可能会给数据库服务器造成巨大压力。
所以以上代码可以改造成:
$ids = array_columns($array, 'id'); if ($ids) { $model = new Model(); $modelDataList = $model->where(['in', 'id', $ids])->all(); $modelDataList = array_combine(array_columns($modelDataList, 'id'), $modelDataList); } foreach ($array as $ar) { $modelData = empty($modelDataList[$ar['id']]) ? [] : $modelDataList[$ar['id']]; if (!$modelData) { continue; } $modelData['is_less'] = $modelData['tag'] == 3 ? true : false; $model2 = new AnotherModel(); $model2->find(['id' => $ar['id']])->update(['is_less' => $modelData['is_less']]); }
还有一个例子,如:
foreach($ids as $id) { $data = RpcService::getMyData(['my_id' => $id]); $data['field1'] = $data['field2'] + $data['field3']; $sendPost = RpcService::sendToBoss(['field1' => $data['field1']]); }
像这种通过接口获取数据或者更新数据的,一般不能在循环体内重复调用,因为http或者其他实现rpc的网络协议或多或少都会慢在数据传输上,而且对方业务的实现一般也不建议调用方循环调用,所以如果有批量调用接口的需求,应该要求接口提供方提供批量操作的接口,在循环体外进行操作。
以上代码可修改为:
$dataList = RpcService::getMyDataList(['ids' => $ids]); array_walk($dataList, function () { // 处理字段 }); RpcService::multiSendToBoss(['list' => array_columns($dataList, 'field1')]);
还有文件读取也是类似,php读写文件效率并不高,应避免频繁读写相同文件。例如:
$readFilePath = 'current_file'; foreach($writeFilePaths as $path) { $content = file_get_content(realpath($readFilePath)); file_put_content($path, $content); }
应将文件内容放在循环体外部。
其他但凡是有耗时或不建议频繁调用的逻辑,都应该写在循环体外。
关于业务层面类调用或方法调用可读性的问题。
关于这一点,先看一下代码示例:
class DemoClass { public $handler = []; public function handlerRegister(Handler $handler) { $this->handler[] = $hanlder; } public function run($id, $params) { foreach ($this->handler as $h) { if ($h->id === $id) { return $h->handle($params); } } } } class Handler { public $id; public function handle($params) { $result = call_user_func($params['callback'], $params['params']); $resultData = (new $result)->getData(); $next = $params['next']; return $next($resultData); } }
call_user_func、call_user_func_array、Reflection类等可以对变量里的某些内容直接实例化或者调用,这在机器编译运行看来是没什么问题的,但是人看的话就比较费心了,你要追根溯源搞清楚调用的是啥,反射的是啥,虽然写起来很简单粗暴,但对读的人不太友好。况且,即便是phpstorm这样的强大的IDE,也不能帮你识别追溯这些变量的源头。
所以,业务代码应尽量避免使用类似代码。
但是,如果你写的是底层脚手架,是丰富框架功能的一个工具包,那么你可以按照自己的想法来,使用者看不看得懂就不重要了。所以像以上的例子往往出现在vendor依赖包中的比较多,写起来比较简单,也不必担心别人看不懂的问题。
关于辅助方法编写的问题
这一点不同的框架有不同的表现,可能有一些框架有自身实现思想的考虑,不方便全局调用一些东西,但目前很多框架都使用了容器,所以使用辅助方法进一步精简代码就有些必要了。
写有辅助方法文件,需要在框架加载过程中,业务使用之前引入,最好是全局引入。
比如Yii2中如果要实现一个json返回,需要写以下代码:
Yii::$app->response->format = Response::FORMAT_JSON; return [ 'code' => 1, 'message' => 'success' ];
如果你写了一个辅助方法如下:
function ajax(array $data) { Yii::$app->response->format = Response::FORMAT_JSON; return $data; }
那么代码可以写成:
return ajax([ 'code' => 1, 'message' => 'success' ]);
又比如:获取一个post提交的所有参数:
$post = Yii::$app->request->post(); // 获取所有 $field1 = Yii::$app->request->post('field1'); // 获取其中的某个参数
辅助方法如下:
function post($key = null, default = null) { if ($key === null) { return Yii::$app->request->post(); // 获取所有 } return Yii::$app->request->post($key, $default) }
调用如下:
$post = post(); $field1 = post('field1');
又比如,根据键销毁数组中的某一个值。
// 原生写法 if (isset($arr[$key])) { unset($key); } // 辅助方法 function array_pull(&$arr, $key) { if (isset($arr[$key])) { unset($key); } } // 调用 array_pull($arr, $key);
关于逻辑块复用和可读性的问题
比如:
public function handle($params) { $dataList = (new Model)->query($params)->all(); $return = []; foreach ($dataList as $data) { if ($data['key'] == 1) { $data['field1'] = "123"; } else if ($data['key'] == 2) { $data['field1'] = '678'; $data['field2'] = '7hj'; } else if ($data['key'] == 3) { $data['field1'] = 'uyo'; } else { $data['field1'] = 'other'; } if ($data['field1'] == "123") { $other = [ 'other1' => 34, 'other2' => 35, 'other3' => 98 ]; $return[] = $other; } elseif ($data['field1'] == "678") { $other = [ 'other1' => 341, 'other2' => 351, 'other3' => 981 ]; if ($data['field2'] == '7hj') { $other = [ 'other1' => 3412, 'other2' => 3512, 'other3' => 9812 ]; } $return[] = $other; } else if ($data['field1'] === 'uyo') { $other = [ 'other1' => 3412, 'other2' => 3512, 'other3' => 9812 ]; $return[] = $other; } else { $other = [ 'other1' => 34123, 'other2' => 35123, 'other3' => 98123 ]; $return[] = $other; } } }
以上代码出现的问题主要有三个:无注释、判断条件太多、逻辑块无法复用。
如果出现更复杂的逻辑,这段代码无疑可能会超过100多行,这在开发维护人员看起来是很艰难的。
注释问题和判断条件太多可能由于业务的问题,有时候无法避免,这里重点说一下逻辑块复用。
以上的代码片段中,
dataList
的获取,应该是一个独立的方法,因为将来可能其他功能也会使用同样的方法获取对应数据;针对于data['field1']
的取值,也应该是一个独立的方法;下面对于data['field1']
的判断,也应该是一个独立方法。这就需要对代码拆分,既能够保证代码简洁性、复用性和可读性,也避免多个无用变量在一个逻辑块中积累。在代码逻辑中,一个功能应该由一个方法来实现,一个方法也应该只做一件事。
这样把这段代码拆分之后,将会变为:
public function getDataList($params) { return (new Model)->query($params)->all(); } public function handleField($data) { if ($data['key'] == 1) { $data['field1'] = "123"; } else if ($data['key'] == 2) { $data['field1'] = '678'; $data['field2'] = '7hj'; } else if ($data['key'] == 3) { $data['field1'] = 'uyo'; } else { $data['field1'] = 'other'; } return $data; } public function getOther() { if ($data['field1'] == "123") { $other = [ 'other1' => 34, 'other2' => 35, 'other3' => 98 ]; } elseif ($data['field1'] == "678") { $other = [ 'other1' => 341, 'other2' => 351, 'other3' => 981 ]; if ($data['field2'] == '7hj') { $other = [ 'other1' => 3412, 'other2' => 3512, 'other3' => 9812 ]; } } else if ($data['field1'] === 'uyo') { $other = [ 'other1' => 3412, 'other2' => 3512, 'other3' => 9812 ]; } else { $other = [ 'other1' => 34123, 'other2' => 35123, 'other3' => 98123 ]; } return $other; } public function handle($params) { $dataList = $this->getDataList($params); $return = []; foreach ($dataList as $data) { $data = $this->handleField($data); $return[] = $other; } return $return; }
这几个方法各自承担一个功能,只完成一件事。
handle()
方法只是用来组织几个方法的数据,简单明了。建议每个方法的代码不超过30行,除非情况特殊。
关于公共方法书写风格的建议
关于公共方法调用的代码风格,应该遵循调用者最少知道的原则,调用者只需按照对应参数传入即可,后面的逻辑复杂性,原则上不必被调用者知道,且调用者应能够充分知晓正确与错误信息,且应保证其健壮性。
涉及到传入参数为数组的,应告知调用者该数组内部参数的详细说明,或在注释中给出对应示例。
页面下载使用专用文件服务器
涉及到web页面直接生成数据下载的,应尽量使用专用的文件服务器,而不是直接在页面进行下载,且下载应尽量使用异步生成文件。
例如用户在点击页面下载按钮之后跳入自己的下载页面,这个页面上有自己的文件下载的历史表格,有状态标记文件是否可以进行下载,当后台将文件生成好上传到文件服务器之后,会标记成可下载。
好处:记录下载历史、历史文件下载、下载性能优化、可以处理大文件。
坏处:需要多做一个页面和一张表,且需要等待文件生成上传至文件服务器的时间。
文件服务器可以使用对象云。