编程规范

下面是我对怎么样 写代码才能够 降低系统的复杂性,增加可读性的一些思考和建议,凡事不可绝对,存在即合理,所以我会用一些,尽量或者尽可能等词汇等,表明大部分情况下遵循这些建议应该是比较优的。

代码规范:

1.尽量使用抛异常的方式,返回错误码的方式,或者自定义的返回错误的方式

如果 调用函数的时候,函数内部发生了错误,上层如何知道内部发生了错误,发生了哪种错误? 以前尝试过调用函数的时候,函数返回 [errno => 2011021 , errmsg => ‘连接数据库超时’, data => [] ]这种格式的数据,如果errno 为0的时候代表正常,需要的数据在data中,否则错误原因和错误类型可以根据 errno, errmsg 确定。

比如

$data = getChannelList($id);
if ($data['errno'] !== 0 ) {

}

但是这样做的坏处是 整个代码里面,充满了errno的判断。而这些判断,就经验而论,更多的是一层层的传递给上层,直到最后返回给用户。

异常的方式给了错误一种默认处理方式,就是直接中断请求,直接结束,处理错误(展示或者记录日志),而不需要层层传递,简单粗暴,但是非常适合web开发,Web开发的每个请求处理,一般如流一般,层层检查处理,直到最后保存数据或者返回数据,中间有错误,如果有错误,告知用户错误原因,所以这种情况下,抛异常的中断的方式非常适合, 如果外界需要特殊处理异常,或者某函数异常不影响请求流, 可以在外层try catch,屏蔽错误。

2. function 内部 尽量 避免使用this,尽量避免 new 对象。

编程就应该就像堆积木一样简单,如果一个func ,其参数就是全部输入,return 的值就是全部的返回值,那花括号包裹的代码块就是一个整体,就是一块积木。
当阅读到这个func的时候,拿到了全部的传入参数,一行行走下去,总是能够理解这个func的作用的, 而不需要回溯到某个上层,看某个this->属性 是如何设置的,对此时的func的行为有着怎样的影响。

比如下面的Class Excel , 调用的时候先new 一个 对象初始化Data,然后中间经过一些逻辑,然后开始调用某功能函数A,当读到$this->data的时候,第一件事情,是不是先想下,$this->data 存储的值是什么?什么时间存储的,中间有没有调用setData,调用了几次, 哪里调用的,影响有多大?为了明确这些可能的疑问而回溯到上层代码,就增加了系统的复杂性。换言之, formatMultiRegions 与 __construct 以及 setData 交叠在一起,共同完成了 一件事情,而交叠的部分,可能与其他的逻辑也是混杂在一起,增加理解成本。

所以如果按照我建议的话,此处应该 改成 Excel_Better, 需要用到功能函数formatMultiRegions的时候, 传入参数Data,整个formatMultiRegions 的行为 就仅仅与data 有关,并不需要关心data经历过怎么样的处理变化,也并不需要回溯到 上层代码才能理解本函数,也就是 formatMultiRegions 就是一块被此时此刻才被用到的积木。
调用之前不知道其存在,调用之后不必在意其存在过,如流水一般,流过了,就不再需要关心,只是想要的数据向下传递。

反面栗子:

namespace app\models; 
use Yii; 
class Excel 
{     
    protected $data = null;     
    //初始化Excel对象     
    public function __construct(Array &$data)     {           
         $this--->phpExcel = new \PHPExcel();
        $this->data = $data;
    }   
    //某功能函数A
    public function formatMultiRegions()
    {   
        $data = $this->data;
        if ($data) { 
  
        }
        $mark = array_values(array_splice($data, 0, 1));
        return $mark;
    }   
    //某功能函数B
    public function formatPie()
    {   
        $data = $this->data;
        foreach ($data as $region=>$list) {

        }   
        return [$lines];
    }  
    // 修改data值
    private function setData($data)
    {   
        $this->data = $data;
    }   
}

推荐 demo :

namespace app\models; 
use Yii; 
class Excel_Better
{     
     //某功能函数A     
    public static function formatMultiRegions($data)     
    {            
       if ($data) {            
       }         
       $mark = array_values(array_splice($data, 0, 1));         
       return $mark;     
    }        
    //某功能函数B     
    public static function formatPie($data)     
    {            
        foreach ($data as $region=-->$list){

        }   
        return [$lines];
    }  
}

3,异常 集中处理

换言之,如果捕获到了异常,打算怎么办呢?如果没有特殊需求,不如不处理,在项目的某一处函数几种处理异常,根据不同的异常类型,根据不同的环境,选择不同的展示方式。
但是有些情况,比如超时重连,或者 异步通知重试等有特殊处理的需求的时候,建议捕获异常,并酌情处理。

4. 日志 尽量记录 完整的上下文,能够复现当时的场景 ,

一 . 更新数据库的日志至少需要记录四项 where条件,原来的数据,新的数据,更新的数据库返回值。

二 . 异常日志要记录 输入数据,错误代码行号,错误文件,错误原因,最好记录调用栈。、

三 . 对于一个记录用户请求的 日志来说,应该记录下,POST,GET 参数,用户的uid(如果有),处理该请求的机器,以及系统的返回值

5. 尽量避免if 的层层嵌套,遇到错误 直接返回或者处理,避免过长的代码段之间,互相牵扯

反面demo :

       if(!empty($id)) {
                $data = Channel::findIdentity($id);
                if(!empty($data)) {
                    $result = Channel::updateInfo($id, ['is_deleted' => 1]); 
                    if ($result) {
                        $logData = [
                            'user_id' => $user_id,
                            'to_user_id' => $data['user_id'],
                            'create_time' => time(),
                            'status' => OperatLog::STATUS_DEL,
                            'reason' => $delReason,
                            'type' => OperatLog::TYPE_CHANNEL,
                            'old_data' => json_encode($data),
                        ];   
                        OperatLog::addInfo($logData);
                        return ['errno' => 7400382, 'errmsg' => '删除成功'];
                    }    
                    return ['errno' => 7400384, 'errmsg' => '删除失败'];
                }    
                return ['errno' => 7400386, 'errmsg' => '当前修改数据不存在'];
            }    
            return ['errno' => 7400388, 'errmsg' => '当前请求参数有误'];

这个 其实是一个还能接受的代码段,某种意义上,还有点美感, 但是如果if 包裹的代码段 超过一屏, 则理解成本就会变得难以接受。

当我们看到一个if的时候, 我们需要 知道if 的 判断条件,开始和结束,是否有else 等,当我们看到 第二个 if 的时候,我们需要 知道 被第一个 通过的情况下进入的,然后同样需要 了解 if 的开始 结束等,然后第三个, 当我们读完第三个 if 退出的时候,需要了解第二个是否有else , if/else 块 之后又 是什么,当前段代码是在第几个if块 之后,等等。总之if的嵌套增大了代码的理解成本。

代码应该是 像 积木一样一块块累积起来的,不同的积木之间,应该尽量减少牵扯。
所以按照我的想法,代码应该写成如下:

         if(empty($id)) {
               return ['errno' => 7400388, 'errmsg' => '当前请求参数有误'];
         }
         $data = Channel::findIdentity($id);
         if(!empty($data)) {
             return ['errno' => 7400386, 'errmsg' => '当前修改数据不存在'];
         }
  
         $result = Channel::updateInfo($id, ['is_deleted' => 1]); 
         if (!$result) {
             return ['errno' => 7400384, 'errmsg' => '删除失败'];
         }
 
         $logData = [
               'user_id' => $user_id,
               'to_user_id' => $data['user_id'],
               'create_time' => time(),
               'status' => OperatLog::STATUS_DEL,
               'reason' => $delReason,
               'type' => OperatLog::TYPE_CHANNEL,
               'old_data' => json_encode($data),
         ];   
         OperatLog::addInfo($logData);
         return ['errno' => 7400382, 'errmsg' => '删除成功'];
     

6. 多表更新或者 删除,添加数据,多个sql语句,加上事务和回滚,原因相信无可争议,只是很多人未必注意。
虽然一般情况下不加事务也不会出问题,但是有了问题,就要修数据,修数据就可能引出各种问题。
个人 比较推崇的一个模板如下,try catch 只有 两个出口,要不一直走到commit,要不在catch里面rollback.

   $trans = Yii::$app->db->beginTransaction();
        try {
            $promotionAddData = array(
                //'region_manage' => $region_manage_id,
                'update_time' => time(),
                'update_user_id' => UserService::getUserId(),
                'is_deleted' => 1,
            );  
            $isUpdate = PromotionLeader::updateInfo($id, $promotionAddData);
            if (!$isUpdate) {
                throw new JdbException('更新数据库异常', 18400177);
            }   
            UserAccount::deleteRow($promotionLeaderInfo['user_id']);
            $trans->commit();
        } catch (\Exception $e) {
            $trans->rollback();
            throw $e; 
        }   
        return ['errno' => 200, 'errmsg' => '推广组长数据操作成功'];

7. 错误码尽可能 整个系统唯一,方便迅速定位错误。

当后端与FE通过json数据 交互的时候,一般来说,0或者200代表ok,其他代表错误就能满足需求,所以在需求层面上,错误码的定义无关紧要,所以一些系统返回400,500,这种仿 http的错误码。
出于排查错误的目的而言,错误码应该尽可能的唯一,当看到系统返回的内容的时候,大概就清楚这段文本大概是系统那段代码处理返回的。比如上面demo 程序中 18400177 ,这种错误码肯定系统内很难重复,生成规则是当前光标所在的列数 + 400(错误的性质,仿http错误码) + 当前行号,某些ide还可以加上当前文件总行数等,达到一个更加随机的错误码。
当然,还有其他的方式,比如在返回值里面,加冗余字段,标明返回数据在系统的位置信息,也可以。
这种全随机的方式,缺点在于很难对所有的错误进行一个统计的归类处理。

8. 尽量使用静态方法,减少new的数量。

将代码 主要 用来处理逻辑上,而非各种语言的语法上,与效率无关,只是为了代码的可读性,如下面的两个demo,既然new 可以去掉,就尽量减少代码行数,增加可读性,重心在逻辑,而非语法。

反面demo:

  function test() {
      $modelA = new ModelA;
      $userId =  $modelA->getInfo($_GET['id']);
      $modelB = new ModelB;
      return $modelB->getList($userId);
  }

推荐例程:

  function test() {
      $userId =  modelA::getInfo($_GET['id']);
      return $modelB::getList($userId);
  }

1 Comment on 编程规范

  1. 通篇看了你修改过的文章,吐槽几点:

    1. 文章标题与内容有偏差,文章的标题是编程规范,不过描述的内容更像心得和建议。正如第一段所说一样。另外,凡事不可绝对,存在即合理,这句话有什么都对,是非不分的意思,有点好好先生的感觉。
    2. 文章的层次感较差,有堆叠的嫌疑。就文章描述的内容而言,总体上都是处理系统复杂度的建议,对于如何降低一个复杂系统的复杂度有很多方面,有很多不同的维度和层次。就本文而言应该是限于coding的层面的,即便是这个层面仍然有很多个话题,本文所涉及的话题可以划归到三个方面:如何增加代码简洁度,如何让代码更加优雅,特别是降低排错成本,如何预防业务逻辑缺陷。这些其实都是可以独立成文的,因为其中有很多值得一叙的东西,是软件设计或者架构的基本话题。
    3. 层级表述问题。
    为什么一级话题使用阿拉伯数字,而在其下的二级内容中使用大写数字(一、二。。),一般都没有这么用的,如果文章有多个层级的话,第一层级一般都是大写数字,即便有多个层级,一般也没有在阿拉伯数字内嵌大写数字的
    多个层级的话可以这样表述:
    一、xxxxx
    1. xxxx
    2. xxxx
    或者是:
    1. xxxx
    1.1 xxxx
    1.2 xxxx

    4. 就具体的论题的具体观点呢,总体上我是持赞同观点的,不过还是想吐槽一下,哈哈。
    第一条,对于错误处理问题,必须使用异常的方式,对于程序中可能发生的异常和错误应该做充分的梳理、归类、整理。不使用异常处理错误的程序都停留在原始和半原始的阶段,都还停留在低级语言的层面上;
    第二条,function内部尽量避免使用this的问题,这里的function显然是method,这个应该第八条想表达的意思是一致的。同样的如果完全遵从了这些建议,那么面向对象语言的优势就会荡然无存了。当然既然说尽量了,你的意思是允许例外的,但是我要说的是相反,应该这样的情况非常少,刻意为之最终会事与愿违,这一点有机会我也专门写一下,哈哈。
    第三条,与第一条相似,不集中处理的我只能不说话了。。。
    第四条,日志的记录尽量记录完整的上下文是没问题的,但是这还是存在一个度的问题,特别是当我们的应用偏数据或者图片或者文件处理的情况,因为调用过程中以及输入输出和与数据存储交互时的数据非常大,如果完整记录上下文,会导致日志量过大,不但会将我们真正需要的关键信息淹没掉,部分时候还会导致磁盘或者网络IO大幅增加影响我们应用的性能。如果说不记日志是不足的话,那么不对记录什么日志以及记录那些信息以及如何记录仔细规划就贸然记录所有关键环节的输入输出则是有些过火了。
    第五条,表述的应该是,函数应该尽早返回,职责应该单一,小即美吧。
    第六条,这个应该取决于系统对数据一致性的要求程度,数据库事务是解决数据一致性的一种方式,事务是分级别的。除了事务之外,版本控制也是一种方式,还有乐观锁和悲观锁。这个需要针对具体业务要求和具体场景认真地规划的。
    第七条,错误码问题,这个呢,我还是坚决反对业务代码重新设定http状态,以及使用http的状态码来表意业务状态的,http状态码只应该由http服务器设定,以明确区分应用错误和http服务器错误,http状态码只适合表意http请求的状态,不适合表意我们的业务逻辑状态。
    第八条,在部分情况下这样做是可以的,刻意为之得不偿失。

    过了一点之后有点睡不着,在清华跑了一大圈,回来洗洗澡正好拜读一下你的大作。吐槽有点多,可不要伤了小心脏啊。哈哈。

Leave a comment

Your email address will not be published.

*