好孩子的编码习惯

前言

我经常能听到一些对话
狗腿子A:哇 我刚刚去改**项目的代码,看的我有点怀疑人生
狗腿子B: 我现在项目的跟屎山一样
狗腿子C: 我隔壁那哥们每天写代码都特别随性,我有点按耐不住我的刀
.....
image.png
今天跟大家聊聊一些 我眼中 好孩子的编码习惯,而不是代码风格习惯 ,当然还是强烈建议大家代码风格跟psr-12psr-1靠齐。

psr-1基础编码规范 、psr-12编码规范托充
This specification extends, expands and replaces PSR-2, the coding style guide and requires adherence to PSR-1, the basic coding standard.

离职流程.png

推荐一本《代码整洁之道》,这本书我已经书都快翻烂了,墙裂推荐!!!

image.png

不过度的if嵌套判断

案例背景
有个函数需要判断用户是否参与活动
流程图1.png

案例代码

    if (用户 == VIP) {
        if (用户的过期时间 <= 1个月内) {
            if (用户没参加过任务) {
                return true;
            }
        } else {
            return false;
        }
    } else {
        return true
    }
    

面对这种多条件的判断可以试着用拦截法逆向思维
拦截法只要符合条件立马返回结果,不再嵌套的if。可以理解成横向判断变成纵向判断。

舒适感
从上往下看 > 从左往右看

逆向思维 大家上学的时候都了解过,与其漫天去找符合的条件还不如找不符合条件,这样的逻辑代码可以少很多。

    if (用户 != VIP) {
        return true;
    } 
    
    if (用户参加过任务) {
        return false;
    }
    
    if (用户的过期时间 <= 1个月内) {
        return true;    
    }
    
    return false;
    

不过度的try-catch嵌套

我遇到过很多项目都过度嵌套try-catch导致最上层的try-catch catch了寂寞。
image.png

案例代码

function insertUser($data)
{
    try {
        userIsInValid();
    } catch (Exception $exception) {
    }
}

function userIsInValid()
{
    try {
        //逻辑判断
    } catch (Exception $exception) {
        return true;
    }
    return true;
    
}

这样的代码没有问题,但是如果假设userIsInValid真的发生代码级的错误没法知道那里出问题,虽然不会破坏业务的健壮性。
可能有人说了在Excetion加个日志,但是如果嵌套的try-catch多了,排查日志也是一件很痛苦的事情。

1.尽可能业务最上层包裹异常 除非网络IO请求函数。
2.如果非要异常嵌套 需要定义每个异常的类型。
3.尽可能根据特定的异常进行catch 不建议直接catch Exception。
4.异常和日志是个cp,还是不要忘记了。
image.png

<?php

function insertUser($data)
{
    try {
        userIsInValid();
    } catch (Exception $exception) {
        // 日志
        // 业务处理
    } catch (HttpException $httpException) {
       // 日志
       // 业务处理
    }
}

function userIsInValid()
{
    //
    return true;
}

不要用if-else做错误类型判断

案例代码 (来源某个网民前段时间咨询)

<?php

.....
if ($code === 'NOTENOUGH') {
    packApiData(400014, 'Company have no enough money to pay', [], '企业余额不足');
} elseif ($code === 'AMOUNT_LIMIT') {
    packApiData(400015, 'Amount limit', [], '金额超限或被微信风控拦截');
} elseif ($code === 'OPENID_ERROR') {
    packApiData(400016, 'Appid and Openid does not match', [], 'Openid格式错误或不属于此公众号');
} elseif ($code === 'SEND_FAILED') {
    // 付款错误,要查单来看最终结果
    if ($orderInfo[1]['status'] == 'SUCCESS') {
        // 还是成功给了,扣回余额
        
        packApiData(200, 'success', [$orderInfo[1]]);
    } else {
        packApiData(400017, 'Weixin pay failed', [], '微信支付付款失败');
    }
} elseif ($code === 'SYSTEMERROR') {
    packApiData(400018, 'Weixin pay server error', [], '微信支付服务器错误');
} elseif ($code === 'NAME_MISMATCH') {
    packApiData(400019, 'Real name mismatch', [], '微信用户的真名校验失败');
} elseif ($code === 'FREQ_LIMIT') {
    packApiData(400020, 'Api request frequently', [], '微信支付接口调用过于频繁,请稍候再请求');
} elseif ($code === 'MONEY_LIMIT') {
    packApiData(400021, 'Company have reached total payment limit', [], '已经达到今日付款总额上限');
} elseif ($code === 'V2_ACCOUNT_SIMPLE_BAN') {
    packApiData(400022, 'This payment account has no real name', [], '用户的微信支付账户未实名');
} elseif ($code === 'SENDNUM_LIMIT') {
    packApiData(400023, 'The number of times the user paid today exceeded the limit', [], '该用户今日收款次数超过限制');
}

这样的代码可能写起来特别舒服,但是后期进行业务的增加改写和时间的沉淀,容易变成让人害怕的屎山代码。

image.png

我们用mapping错误码来调整下

function packApiDataByOrderError($code)
{
    $errorCodeMappins = [
        "NOTENOUGH" => [
            "code" => 400014,
            "wx_message" => "Company have no enough money to pay",
            "error_message" => "企业余额不足"
        ],

        "AMOUNT_LIMIT" => [
            "code" => 400015,
            "wx_message" => "Amount limit",
            "error_message" => "金额超限或被微信风控拦截"
        ],

        .....
    ];

    if (array_key_exists($code, $errorCodeMappins)) {
        packApiData(
            $errorCodeMappins[$code]['code'],
            $errorCodeMappins[$code]['wx_message'],
            [],
            $errorCodeMappins[$code]['error_message']
        );
    }

    packApiData(
        999999,
        "undefined message",
        [],
        "未知错误"
    );
}

建议errorCodeMappins不要放在函数内,可以放在类顶部或者专门枚举类。
通过errorCode 可以避免调整主流程代码,能够保证主流程的代码比较精简也能对不同的code进行错误的定义

if ($code == "SEND_FAILED") {
    // 付款错误,要查单来看最终结果
    if ($orderInfo[1]['status'] == 'SUCCESS') {
        // 还是成功给了,扣回余额
        PDOQuery($dbcon, 'UPDATE user SET money=money-? WHERE open_id=?', [$payAmount, $openId], [PDO::PARAM_INT, PDO::PARAM_STR]);
        packApiData(200, 'success', [$orderInfo[1]]);
    } else {
        packApiData(400017, 'Weixin pay failed', [], '微信支付付款失败');
    }
}

packApiDataByOrderError($code);

在合适的场景使用设计模式

上述可能只能针对错误码进行改造,如果万一我们需要不同的错误进行逻辑处理还怎么办。这时候可以考虑用设计模式 (比如用以多态取代条件表达式)

设计模式固好但不要过度使用,不然整个项目更难维护,你要坚信未来的你队友不知道是什么样的生物

image.png

$callbackCodeMappings = [
    "SEND_FAILED" => OrderSendFailed::class,
];

if (array_key_exists($code, $callbackCodeMappings)) {
    $class = new $callbackCodeMappings[$code];
    $class->handle();
}


interface OrderStateImp
{
    public function handle($context);
}

class OrderSendFailed implements  OrderStateImp
{
    public function handle($context)
    {

    }
}

$callbackCodeMappings同样建议配置专门枚举文件内。
给出得代码比较粗糙,其实可以更加健壮性的做一些判断

统一处理浮点运算结果

由于php是弱对象语言,所以面对一堆情况总能出现,这个订单数据怎么不对了,接口有问题。

$int = 0.58; var_dump(intval($int * 100));
output:57

在浮点数里面 58是被视为57.999999999999999999999……9999无限接近58
再intval强制转换乘整型的时候就默认采用截取法取整

所以最好养成一个好习惯每次在计算浮点数的时候用
BC Math

$int = 0.58;
intval(strval($int * 100))

或者使用BC MATH

bcmul(0.58, 100, 0);

image.png

鼓励用全局错误码来控制错误

写接口的我们对以下的json格式特别熟悉

{
    "success": true,
    "error_code": 0,
    "message": "",
    "results": []
}

对以下的代码也已经熟悉

if (***) {
    $this->error(999,"****", []);
}

这样的结果的错误码容易重复没有统一管理,事实上唯一错误码应该有以下帮助。
1.前端可以根据错误码做逻辑处理
2.根据错误码能直接快速定位到错误代码

建议

<?php

namespace App\ErrorCode;

class UserErrorCode
{
    const USER_DISABLE_ERROR = [
        "error_code" => 1050001,
        "message" => "用户已被停用"
    ];
}

$this->error(UserErrorCode::USER_DISABLE_ERROR);

错误码建议

1-2位 - 项目码 | 3-4位 - 模块码 | 5-7位具体业务错误码

可靠的命名规范

不可靠的命名总会让人误导。
比如变量命名为userArrayList 我以为是个数组列表变量,事实上这个特么是个对象列表。

1.做有意义的区分
比如 singleUserItemuserItem有啥区别
比如 getUserListgetUsers有啥区别
image.png

2.可以通过搜索翻译能知道的变量含义
不要把变量贴入搜索翻译会出现七七八八的东西
3.如果真的不知道该怎么翻试试用拼音把别硬凹了
比如之前做百度的一个接口对接
变量命名为hundredDegree而不是baidu
image.png
其他的可以参照《代码简洁之道》

擅用middleware

middleware可以理解成观察者模式,我们开发的接口总会遇到很多同样操作,比如
1.身份检测
2.权限判断
3.请求参数filter调整
4.记录接口信息
5.接口限流
我见过挨个接口去实现、也见过初始化一个ControllerBase的类,实现这些,子类的Controller去继承这些。
其实我们可以抽离成middleware去实现
image.png

好处可以根据不同接口对middleware进行组合选择,而不是对代码进行各特殊化处理.

函数的单一职责

最最最最后也是最重要的,代码的恶心大多数来源于函数的职责不清晰,有全都塞在一起的、东一块西一块的。
其实关于单一职责有很多文章在描述,如何去检验或者去写符合标准的单一职责。
画流程图
如果你能把业务的流程图画的特别清晰,那么你的函数的职责也就定下来了。
image.png

<?php

// 兑换逻辑
function doExchange()
{
    if (checkIsLock()) {
        
    }
    lock();
    if (!checkUserIsExchange()) {
        
    }
    costUserPoint();
    exchangeGoods();
}
// 判断是否悲观锁
function checkIsLock(){}
// 上悲观锁
function lock(){}
// 判断用户是否可以兑换
function checkUserIsExchange(){}
// 扣除积分
function costUserPoint(){}
// 兑换商品
function exchangeGoods(){}

最后

上述为洪光光心中的好孩子的习惯,也有可能是你眼中坏孩子的习惯。如果你认为是坏孩子的习惯或者认为还有其他好孩子的习惯欢迎评论撕逼讨论。
毕竟
image.png

留个彩蛋 看看大家怎么实现
写一个函数returnScoreResult,请根据输入的分数,返回对应的成绩的等级。
1.如果分数小于0或者大于100 返回 【无效分数】
2.如果分数>=0,<60 返回 【不及格】
3.如果分数>=60,<70 返回【及格】
4.如果分数>=70,<80 返回 【一般】
5.如果分数>=80, <90 返回 【良好】
5.如果分数>=90, <100 返回 【优秀】
6.如果分数=100 返回【满分】

阅读 4.6k

推荐阅读

一个搬砖仔的情感空间

30 人关注
4 篇文章
专栏主页