什么是CodeReview及CodeReview的目的
- CodeReview 不是针对某个人的批评会,也不是翻旧账的行为。CodeReview 是为了规范团队编码规范,提高整体代码质量,培养良好的编码习惯而采取的一种手段。
- 主要目的是推动团队成员遵守被按规范执行编码规范,发现代码工程中的异味代码和不遵守规范的代码,强调规范,提升个人意思。
- Coding 可以看成是一项工程,是多个人通力合作的产出。任何小的细节遗漏或没做好都可能导致线上故障。所有,要对每一行代码负责。
- 代码的可读性和易维护性是对每一个工程师的基本要求。写出来的代码是为了后续更容易的维护和功能的更迭,不仅仅只是能运行。
- Laravel 编码规范见文档
Talk is cheap show me the code.
Middleware codereview
文件 app/Exceptions/Handler.php
<?php/*** Report or log an exception.** @param \Exception $exception* @return void*/public function report(Exception $exception){ // 非配置文件代码中,不要直接使用 env 函数获取 .env 配置if (env('APP_ENV') !== 'local') {// 此处的 if 可以提升,合并到上一级,可读性更好if (app()->bound('sentry') && $this->shouldReport($exception)) {app('sentry')->captureException($exception);}}parent::report($exception);}
根据智众 Laravel 规范 以及 SRP 原则重构后如下。
<?php/*** Report or log an exception.** @param \Exception $exception* @return void*/public function report(Exception $exception){if ($this->shouldReportToSentry($exception)) {app('sentry')->captureException($exception);}parent::report($exception);}// 判断是否应该的函数最好以 is should can could 等开头// SRP 原则,最小责任。// 将判断是否应该被sentry捕获的异常单独提出来作为一个函数public function shouldReportToSentry(Exception $exception): bool{!\in_array(config('app.env'), ['production', 'staging', 'prod']) &&app()->bound('sentry') &&$this->shouldReport($exception);}
文件 app/Http/Middleware/CheckSimpleSign.php
<?phpnamespace App\Http\Middleware;use App\Models\ZOauthClients;use App\Traits\ResponseFormat;use App\Utils\Sign;use Carbon\Carbon;use Closure;use Illuminate\Support\Facades\Cache;class CheckSimpleSign{use ResponseFormat;protected $response;const MAX_TIMESTAMP_SKEW = 60;const CACHE_KEY="mmc:client_id:";/*** Handle an incoming request.** @param \Illuminate\Http\Request $request* @param \Closure $next* @return mixed*/public function handle($request, Closure $next){// try 的异常是什么异常?try{// 变量命名规范 参考 PSR2// 小写字母开头的驼峰写法$_client_id=$request->input('_client_id');$_timestamp=$request->input('_timestamp');$_sign=$request->input('_sign');//兼容未传验参参数无需校验if(!$_client_id && !$_timestamp && !$_sign){return $next($request);}if(!$_client_id || !$_timestamp || !$_sign){return $this->jsonError(421, '缺少参数',0);}// 无意义if(strtotime(date("Ymdhis",$_timestamp))!=$_timestamp){return $this->jsonError(421, '参数异常',0);}$version=$this->_client_version($_client_id);if(!$version){return $this->jsonError(421, '参数异常.',0);}$signParams=["_client_id"=>$_client_id,"_timestamp"=>$_timestamp,];$sign=Sign::getSign($signParams,$version);if($sign!=$_sign){return $this->jsonError(421, '签名异常.',0);}return $next($request);}catch (\Exception $e){return $this->jsonError(421, $e->getMessage(),0);}}// 函数名命名private function _client_version($client_id){$cache_key=self::CACHE_KEY.$client_id;$version=Cache::get($cache_key);if($version){return $version;}$client_info=ZOauthClients::getInfoByClientId($client_id);if(empty($client_info)){return "";}Cache::add($cache_key,$client_info['version'],self::MAX_TIMESTAMP_SKEW);return $client_info['version'];}}
Controller codereview
文件 app/Http/Controllers/Api/MMC/BloodSugarController.php
<?php//获取医院中心编号 部门暂不加入private function _getHospCd($hospId) {$sql = "select * from z_mmc_no_range where hosp_id = '$hospId' and mmc_min != '' limit 1";$ret = DBSelectOne($sql);if (isset($ret['mmc_min']) && $ret['mmc_min']) {return $ret['mmc_min'];}return false;}
操作DB,不要直接拼接 SQL 。要使用Laravel中ORM提供的写法。
大部分 Controller 没有分层。业务逻辑代码,大段堆积在 Controller 中。 Controller 职责过重。至少拆分一层 Service 层。Service 负责处理具体业务逻辑。Controller 仅仅负责分发客户端请求到Request验证参数,然后将具体处理业务逻辑分发到Service,获取并放回 Service 处理结果。对DB的操作建议写在Model中,如有必要,也可以写在Service中。
app/Http/Controllers/Api/Auth/WinAppAuthController.php
app/Http/Controllers/Api/Config/SysConfigController.php
app/Http/Controllers/Api/EyeGround/AuthController.php
app/Http/Controllers/Api/EyeGround/MmcEyesGroundController.php
app/Http/Controllers/Api/EyeGround/PatientController.php
app/Http/Controllers/Api/EyeGround/SystemNoticeController.php
app/Http/Controllers/Api/EyeGround/ViewEyesController.php
app/Http/Controllers/Api/EyeGround/VistelController.php
app/Http/Controllers/Api/MMC/BgController.php
app/Http/Controllers/Api/MMC/BloodSugarController.php
Utils 及 Service codereview
app/Utils/CommonUnits.php 工具类功能也要内聚。既然是 OOP 需要有 OO 的概念。需要具体拆分。
<?phpnamespace App\Utils;use Illuminate\Support\Carbon;class CommonUnits{/*** 根据生日获取年龄* @param string $birth 生日* @param string $nowDate 当前日期* @param int $type 1周岁 2虚岁* @return int|null*/public static function getAgeByBirth($birth,$nowDate="",$type=1){if(!$nowDate) $nowDate=Carbon::now()->format("Y-m-d");$birthTime = strtotime($birth);$nowTime = strtotime($nowDate);if($birthTime === false || $nowTime===null || $birthTime<0 || $nowTime<0){return null;}if($birthTime>$nowTime){return null;}list($birthYear, $birthMonth, $birthDay) = explode('-', $birth);list($currentYear, $currentMonth, $currentDay) = explode('-', $nowDate);if($type==1) {//周岁$age = $currentYear - $birthYear - 1;if ($currentMonth > $birthMonth || $currentMonth == $birthMonth && $currentDay >= $birthDay){$age++;}}else if($type==2){//虚岁$age = $currentYear - $birthYear;}return $age;}/*** @param $date* @param string $format* @return null|string*/public static function getDateByDateTime($date,$format="Y-m-d"){$dateTime=strtotime($date);if(!$dateTime || $dateTime<0){return null;}return Carbon::createFromFormat($format,$date)->format($format);}/*** 数组笛卡尔集组合* @param $arr* @return array|mixed* @example CommonUnits::combineDika([[1,2], [3,4,5]]);*/public static function combineDika($arr){$result = array_shift($arr);while ($arr2 = array_shift($arr)) {$arr1 = $result;$result = array();foreach ($arr1 as $v) {foreach ($arr2 as $v2) {if (!is_array($v)) $v = array($v);if (!is_array($v2)) $v2 = array($v2);$result[] = array_merge_recursive($v, $v2);}}}return $result;}/*** 数据脱敏* @param $string 需要脱敏值* @param int $start 开始* @param int $length 结束* @param string $re 脱敏替代符号* @return bool|string* 例子:* dataDesensitization('18811113683', 3, 4); //188****3683* dataDesensitization('乐杨俊', 0, -1); //**俊*/public static function dataDesensitization($string, $start = 0, $length = 0, $re = '*'){if (empty($string)){return false;}$strarr = array();$mb_strlen = mb_strlen($string);while ($mb_strlen) {//循环把字符串变为数组$strarr[] = mb_substr($string, 0, 1, 'utf8');$string = mb_substr($string, 1, $mb_strlen, 'utf8');$mb_strlen = mb_strlen($string);}$strlen = count($strarr);$begin = $start >= 0 ? $start : ($strlen - abs($start));$end = $last = $strlen - 1;if ($length > 0) {$end = $begin + $length - 1;} elseif ($length < 0) {$end -= abs($length);}for ($i = $begin; $i <= $end; $i++) {$strarr[$i] = $re;}if($start>0) {if ($begin >= $end || $begin >= $last || $end > $last) return false;}else if($start===0){if ($begin > $end || $begin >= $last || $end > $last) return false;}return implode('', $strarr);}
该类包含,根据生日获取年龄、数组笛卡尔集组合、数据脱敏、以及根据日期字符串获取date对象。
需要适当的对功能进行区分,来小心的构造类对象。不要所有不相干的功能都堆叠在一个类中。
日期相干的功能使用 Carbon\Carbon 库应该可以满足 90% 以上的需求,其他的也可以用 Carbon 中的方法来具体实现。
app/Services/YanDiNotMmcCompleteService.php
存在大段函数(170行)来实现一个功能。这样的函数大都可以拆分成更具体的职责函数来实现。大段函数,如果在功能更迭时需要改动,代码阅读成本以及测试成本很高,且容易引入bug。
<?phppublic function completion($yanDiInfo){Log::info("开始计算", $yanDiInfo);$userId = $yanDiInfo['user_id'];$hospId = $yanDiInfo['hosp_id'];$deptId = $yanDiInfo['dept_id'];$picture = json_decode($yanDiInfo['img_ids'], true);$left_num = 0;$right_num = 0;if (isset($picture['left'])) {$left_num = count($picture['left']);}if (isset($picture['right'])) {$right_num = count($picture['right']);}$isMmc = 0;$scores = ['pictureScore' => 0, //眼底图片得分'baseScore' => 0, //基本信息得分'historyScore' => 0,//病史信息得分'inspectScore' => 0,//实验室检查必填得分'inspectNotRequiredScore' => 0, //实验室检查选填得分'urineScore' => 0, // 实验室检查选填-尿常规得分];## 第一步 眼底图片上传 20分$scores['pictureScore'] = sizeof($picture) == 1 ? 10 : 20;## (此为糖网问卷,mmc问卷相关需要单独查询出来)$questionnaireAnswer = TwUserNewSurvey::query()->from('z_user as u')->selectRaw('s.*,u.name,u.sex,u.bday,u.cell,7 as company_id')->leftJoin('tw_user_new_survey as s', 's.user_id', '=', 'u.id')->where('u.id', $userId)->get()->toArray()[0] ?? [];//多选题组装$multipleArr = [];foreach ($questionnaireAnswer as $k => $v) {if ($questionnaireAnswer[$k] && $questionnaireAnswer[$k] !== '0.00') {// 处理每种情况数据if (array_key_exists($k, self::MULTIPLE_MAPPING)) {$res = json_decode($questionnaireAnswer[$k], true);if (is_array($res)) {foreach ($res as $val) {$multipleArr[$k . $val] = self::MULTIPLE_MAPPING[$k][$val] ?? '';}} else {$multipleArr[$k . $questionnaireAnswer[$k]] = self::MULTIPLE_MAPPING[$k][$questionnaireAnswer[$k]] ?? '';}}//处理问卷中字典if (array_key_exists($k, self::TW_ANSWER_MAPPING)) {$questionnaireAnswer[$k] = $this->twTransAnswer($questionnaireAnswer[$k], self::TW_ANSWER_MAPPING[$k]);}}else{// 排除 sex 0 男 1 女if ($k !== 'sex') {// 设置默认值为 ''$questionnaireAnswer[$k] = '';}}}## 遍历问卷答案计算得分foreach ($questionnaireAnswer as $k => $v) {## 基本信息if (array_key_exists($k, self::BASE_SCORE)) {if ($k === 'sex') {$scores['baseScore'] += self::BASE_SCORE[$k];continue;}if ($v && $v !== '0.00' && $v !== '0000-00-00') {$scores['baseScore'] += self::BASE_SCORE[$k];}}## 病史信息if (array_key_exists($k, self::HISTORY_SCORE)) {if ($v && $v !== '[]') {$scores['historyScore'] += self::HISTORY_SCORE[$k];}}## 实验室检查if (array_key_exists($k, self::INSPECT_REQUIRED_SCORE)) {if ($v && $v !== '[]' && $v !== '0.00') {$scores['inspectScore'] += self::INSPECT_REQUIRED_SCORE[$k];}}if (array_key_exists($k, self::INSPECT_NOT_REQUIRED_SCORE)) {if (in_array($k, ['pro', 'glu', 'leu', 'ery', 'nit']) && $v) {$scores['urineScore'] = 2;continue;}if ($v && $v !== '0.00') {$scores['inspectNotRequiredScore'] += self::INSPECT_NOT_REQUIRED_SCORE[$k];}}}Log::info("得分" . $userId .'----'. $scores['baseScore']);// 实验室检查选填得分,最多获得10分$scores['inspectNotRequiredScore'] = $scores['inspectNotRequiredScore'] + $scores['urineScore'];$scores['inspectNotRequiredScore'] = $scores['inspectNotRequiredScore'] > 10 ? 10 : $scores['inspectNotRequiredScore'];// 实验室检查总得分$scores['inspectTotalScore'] = $scores['inspectScore'] + $scores['inspectNotRequiredScore'];$completeTotal = $scores['pictureScore'] + $scores['baseScore'] + $scores['historyScore'] + $scores['inspectTotalScore'];$insertData = ['user_id' => $userId,'hosp_id' => $hospId,'dept_id' => $deptId,'is_mmc' => $isMmc,'complete_picture' => $scores['pictureScore'],'complete_base' => $scores['baseScore'],'complete_history' => $scores['historyScore'],'complete_inspect' => $scores['inspectTotalScore'],'complete_total' => $completeTotal,'complete_inspect_not_require' => $scores['inspectNotRequiredScore'],];YanDiComplete::query()->updateOrCreate(['user_id' => $userId, 'dept_id' => $deptId], $insertData);Log::info("sex". $questionnaireAnswer['sex']);$insertAnswerData = ['no' => date("Ymd"),'hosp_id' => $yanDiInfo['hosp_id'],'hosp_name' => $yanDiInfo['hosp_name'],'dept_name' => $yanDiInfo['dept_name'],'is_mmc' => $isMmc ? '是' : '否','user_id' => $userId,'name' => $questionnaireAnswer['name'] ?? '',//姓名'sex' => isset($questionnaireAnswer['sex']) ? ([0 => '男', 1 => '女'][$questionnaireAnswer['sex']] ?? '未知') : '未知',//性别'bday' => $questionnaireAnswer['bday'] ?? '',//'出生日期','cell' => $questionnaireAnswer['cell'] ?? '',//'手机号','has_cell' => isset($questionnaireAnswer['cell']) && $questionnaireAnswer['cell'] ? '是' : '否',//'是否手机号验证','height' => $questionnaireAnswer['height'] ?? '',//'身高','weight' => $questionnaireAnswer['weight'] ?? '',//'体重','sbp' => $questionnaireAnswer['sbp'] ?? '',//'收缩压','dbp' => $questionnaireAnswer['dbp'] ?? '',//'舒张压','education' => $questionnaireAnswer['education'] ?? '',//'教育','province' => $this->getProvinceName($questionnaireAnswer['province'] ?? ''),//'目前的长期居住地','left_num' => $left_num, // 左眼图片数量'right_num' => $right_num, // 右眼图片数量'is_tnb' => $questionnaireAnswer['is_tnb'] ?? '',//'糖尿病史','tnb_start_time' => $questionnaireAnswer['tnb_start_time'] ?? '', //糖尿病开始时间'is_high_blood' => $questionnaireAnswer['is_high_blood'] ?? '',//'高血压史','high_blood_start_time' => $questionnaireAnswer['high_blood_start_time'] ?? '',//'高血压开始时间','other_jzxjb_type' => $questionnaireAnswer['other_jzxjb_type'] ?? '','is_zfg' => $questionnaireAnswer['is_zfg'], //脂肪肝'is_jzxjb' => $questionnaireAnswer['is_jzxjb'], //甲状腺疾病'glu0' => $questionnaireAnswer['glu0'] ?? '',//'空腹血糖','hba1c' => $questionnaireAnswer['hba1c'] ?? '',//'糖化血红蛋白''cr' => $questionnaireAnswer['cr'] ?? '',//'血肌酐','ua' => $questionnaireAnswer['ua'] ?? '',//'血尿酸','tg' => $questionnaireAnswer['tg'] ?? '',//'甘油三酯TG','tc' => $questionnaireAnswer['tc'] ?? '',//'高密度脂蛋白胆固醇HDL-c','hdlc' => $questionnaireAnswer['hdlc'] ?? '',//'低密度脂蛋白胆固醇LDL-c','ldlc' => $questionnaireAnswer['ldlc'] ?? '',//'总胆固醇TC','record_at' => $yanDiInfo['created_at'],];$insertAnswerData = array_merge($insertAnswerData, $multipleArr);Log::info("aa", $insertAnswerData);//dd($insertAnswerData);YanDiAnswer::query()->where(['no' => date("Ymd"),'user_id' => $userId,'dept_id' => $deptId,])->delete();YanDiAnswer::query()->updateOrCreate(['no' => date("Ymd"),'user_id' => $userId,'dept_id' => $deptId,], $insertAnswerData);return true;}
重构,将子步骤抽取出来,单独成为函数,做到操作清晰,调用明确。
<?phppublic function calculateCompletionScore($eyesInfo){// 获取用户问卷填写详情$answers = $this->getUserAnswers($eyesInfo['user_id']);// 按照计算的方式组装处理 answer$answers = $this->rebuildAnswerStructure($answers);// 遍历计算完成度分数$scores = $this->calculateScoreOfAnswers($answers);...更新相关表结构}
除必要的注释外,被注释的不必要代码。一定不要带入生产环境。要删除干净,保持代码的洁净。
总结
- 代码分层要清晰,Controller 职责的接受 Request 并将验证的 Request 派送到 Service 层进行业务逻辑处理。Controller 一定不可以包含对业务逻辑的处理。具体业务逻辑的处理一定要在 Service 层编码。
- 代码职责问题。一个函数只允许有一个单一职责,如果代码函数超过 100行,一般都可以进行拆分和提炼出更小职责的函数。SRP 原则能有益于后续维护和需求变更。且如果有单元测试覆盖。能更好的的实践单元测试。利于单元测试的代码的编写。
- 一个项目的开发不是一个人的事情,是一个团队的事情。所以,养成良好的开发习惯和团队规范是必要的事情。这样才能持续写出易维护、运行稳定的代码。
- 具体的类的职责也不要超过类本身的职责。CommonUtils 就是一个错误的 OOP 抽象示范。
- 命名规范可以通过工具来保证。保持一致的命名规范和代码格式,是工程化的前提。
- 不允许直接拼接 SQL。使用 ORM 来进行数据库的操作。不允许在循环体中,进行数据库操作。会造成不可控的运行时间。
- 单例模式不一定使用于所有场景。不要盲目的 COPY 和应用。能静态声明(static)的方法可以尽量静态声明。
- 不要节省代码注释,你能读懂的,不代表所有人都能懂原来Code的意图。更多的注释能帮助更容易读懂代码。
