Go设计模式(4)-代码编写(设计模式go语言实现)

设计原则Go设计模式(3)-设计原则是从相对高的维度来进行代码设计,设计的再好,代码编写不优雅,代码质量也难以得到保障。

即使当时写的很好,如何保证随着业务变化、其他同学的修改,代码仍然是优雅的呢?另外,怎样的代码称之为优雅,又让谁来评判呢?

所以代码编写至少涉及三个问题:

高质量代码的标准是什么?

如何编写高质量代码?

如何保证代码一直高质量?

当然,在代码编写方面,我仍然是个学生,大家要是觉得有价值可以参考一下,如果有错误的地方,也希望大家多多批评指正。

1高质量代码的标准

代码质量评价有很高的主观性。一般最常用到几个评判代码质量的标准有:可维护性、可读性、可扩展性、灵活性、简洁性、可复用性、可测试性。其中,可维护性、可读性、可扩展性又是提到最多的、最重要的三个评价标准。

要写出高质量代码,需要善用面向对象设计思想、设计原则、设计模式、编码规范、重构技巧等。

Go设计模式(3)-设计原则和Go设计模式(2)-面向对象分析与设计更多讲的是可维护性、可扩展性、灵活性、可复用性。而可读性、简洁性、可测试性则与基础的编码能力相关。

2如何编写高质量的代码

2.1编程规范

仅从编写看起来优雅的代码层面讲,起效最快的是符合编程规范。编程规范其实分两部分:

  1. 通用规范,各种语言都能用
  2. 指定语言规范,这是针对各种语言自己特性编写的,如Go、Java、PHP等,网上有很多可以拿来参考

这里我们仅讲一下通用编程规范。通用编程规范一般包含三个方面,命名与注释、代码风格、编程技巧。

命名与注释

命名与注释主要是为了增加可读性。但有部分程序员命名能力实在堪忧(说的就是我),好在找到了一个神器https://unbug.github.io/codelf/,可以查一下其他人是怎么命名的。

  1. 命名的关键是能准确达意
  2. 借助类的信息来简化属性、函数的命名,利用函数的信息来简化函数参数的命名
  3. 命名要可读、可搜索,不要使用生僻的、不好读的英文单词
  4. 接口的2中命名方式:在接口中带前缀"I";在接口的实现类中带后缀"Impl"。抽象类的2中命名方式:带上前缀“Abstract";不带前缀
  5. 注释的内容:做什么、为什么、怎么做。复杂的类和接口,还要写明”如何用“
  6. 类和函数一定要写注释,而且要写的尽可能全面详细

代码风格

  1. 函数的代码行数不要超过一屏幕的大小,比如50行
  2. 一行代码最好不要超过IDE的显示宽度
  3. 善用空行分割单元块
  4. 推荐两格缩进,节省空间。一定不要用tab键缩进
  5. 将大括号跟上一条语句同一行,可以节省代码行数,另起新的一行,结构清晰
  6. 在GoogleJava编程规范中,依赖类按照字母序从小到大排列。类中先写成员变量后写函数,成员变量之间或函数之间,先写静态成员变量或函数,后写普通变量或函数,并且按照作用域大小依次排列

编程技巧

  1. 将复杂的逻辑提炼拆分成函数和类
  2. 通过拆分成多个函数或将参数封装为对象的方式,来处理参数过多
  3. 函数中不要使用参数来做代码执行逻辑的控制
  4. 函数设计要职责单一
  5. 移除过深的嵌套层次
  6. 用字面常量取代魔法数
  7. 用解释性变量来解释复杂表达式

2.2代码可测试性

代码的可测试性是指针对代码编写单元测试的难易程度。容易编写单元测试就说明代码可测试性好,反之意味代码设计的并不是很合理。

提高代码可测试性有两个手段

  1. 依赖注入可以通过mock的方法将不可控的依赖变得可控
  2. 利用二次封装来解决某些代码行为不可控的情况

代码的可测试性也是检查代码是否高质量的一个手段。写完代码之后,一定记得编写单元测试。首先需要思考有哪些测试用例,然后开始编写单元测试,此时可能发现有部分单元测试无法编写。原因一般有如下几条:

  1. 未决行为:如代码和时间、随机数相关。一般使用二次封装来解决。
  2. 全局变量:全局变量会导致上一次运行的结果可能影响下一个运行。一般使用reset全局变量解决。可以看看能否不用全局变量。
  3. 静态方法:不好处理,非要测试只能mock。可以看看能否不使用静态方法。
  4. 复杂继承:父类需要mock某个依赖对象才能进行单元测试,那所有的子类、子类的子类都需要mock出这个对象。能做单元测试,就是比较麻烦。可以看看是否能改为组合。
  5. 高耦合代码:依赖十几个外部对象完成工作,需要mock十几个外部对象来进行单元测试。看看是否可以通过重构解耦。

3如何保证代码一直高质量

要想代码随着业务的变更、新人的更改一直保持高质量,除了不断的重构别无它法。这也分情况,如果是新项目,大家技术水平高、都有不断重构的意识、代码Review也会考虑代码质量问题,保持代码高质量相对简单一些。如果是老旧项目、当时活多时间紧,代码质量已经很差了,把这种代码提升到较高质量会难很多。

基于这两种情况,重构方案也有所不同。

3.1重构

对于第一种情况,使用编程规范中的方法进行优化。当然新的业务还是需要用到面向对象设计思想、设计原则、设计模式等。

对于第二种情况,就需要做大型重构了。代码是否需要做大型重构,一线开发人员最有发言权。如果觉得这个代码进行细微改动后不知道会引发什么、或者感觉改不动了、或者这个代码的开发严重影响了项目进度,那肯定是要重构了,不重构难道留着过年?当然,重构是个技术活,但有个前提,需要上下一心,大家统一认识,要有重构的决心。

大型重构主要靠解耦。代码松耦合、高内聚,是控制代码复杂度的有效手段。为了解耦,一般使用如下方案:

  1. 封装与抽象:有效地隐藏实现的复杂性,隔离实现的 易变性,给依赖的模块提供稳定且易用的抽象接口
  2. 引入中间层:简化模块或类之间的依赖关系
  3. 模块化:分而治之
  4. 使用设计思想和原则:单一职责原则、基于接口而非实现编程、依赖注入、多用组合少用继承、迪米特发展

3.2单元测试

重构有风险,重构需谨慎。很多时候大家不敢做重构,主要怕重构引起大问题,影响业务。那如何能保证重构不引起问题呢?理论上没法百分之百保证不会产生问题,但是我们可以通过一些手段来规避风险,其中最有效的就是单元测试。

写单元测试难度不大,所以部分程序员不想写。另外有时候项目会很紧急,没有写单元测试的时间。所以关键问题是团队需要建立对单元测试正确的认识,如果没有单元测试,代码不准上线,通过种种手段,保证单元测试的编写。

其实单元测试有很多好处。

写单元测试的过程本身就是代码Review和重构的过程,能有效地发现代码中的bug和代码设计上的问题。

另外重构的过程中,如果单元测试仍然能够跑通,说明重构质量是可以的。

4实例

上面聊了这么多理论,总得看点代码才行。正好前两天家里人要去医院,所以简单写了一个抢号的功能。当时写的比较急,属于能用就行,趁这次写文章,就拿这个代码来做优化吧。

这个代码是PHP的,毕竟脚本写起来更快一些(PHP是最好的语言)。代码编写和语言关系不大,就优化PHP版的吧,如果大家有兴趣,可以写Go版的。

这个功能只支持一家医院,我希望优化完后,可以快速支持多家医院的抢号。另外默认都是通过微信进行预约。我们只是用这个用例来锻炼,希望大家不要用来做不好的事情。

我们先看一下代码以前的样子:

<?php
$tm          = Msectime();
$docName     = 'docName';
$patientName = '患者名';
$wxID        = '微信ID';
$regDay      = '预约时间';


//返回当前的毫秒时间戳
function Msectime()
{
    list($msec, $sec) = explode(' ', microtime());
    $msectime         = (float) sprintf('%.0f', (floatval($msec) + floatval($sec)) * 1000);
    return $msectime;
}


//http Get请求
function SendRequest($url)
{
    $curl = curl_init();
    curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
    curl_setopt($curl, CURLOPT_TIMEOUT, 5);
    curl_setopt($curl, CURLOPT_URL, $url);


    $res    = curl_exec($curl);
    $resobj = json_decode($res, true);
    curl_close($curl);
    return $resobj;
}
//http Post请求
function SendPostRequest($url,$postData)
{
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
    curl_setopt($ch, CURLOPT_POST, 1);
    curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);
    $output = curl_exec($ch);
    $resobj = json_decode($output, true);
    curl_close($ch);
    return $resobj;
}






//1.获取个人id
$uid = -1;
$uInfoUrl = "//hospital?act=userinfo_oid&uid=$wxID&tm=$tm&oid=$wxID";
$uInfoS   = SendRequest($uInfoUrl);
$uInfo    = json_decode($uInfoS['data'], true);
$uid      = $uInfo['id'];


//2.获取病人id
$sListUrl = "//hospital?act=member&uid=$uid&oid=$wxID";
$sListS   = SendRequest($sListUrl);
$sList    = json_decode($sListS['data'], true);
$sickid   = -1;
foreach ($sList as $item) {
    if ($item['name'] == $patientName) {
        $sickid = $item['id'];
        break;
    }
}
//3.获取医生可用时间段的id
$url       = "//hospital/ajax.ashx?act=bespeak_v1&deptid=417&clsid=2&tm=$tm";
$response  = SendRequest($url);
$docList   = json_decode($response['data'], true);
$bespeakid = -1;
$aorp      = 0; //0上午 1下午
foreach ($docList as $item) {
    if ($item['name'] == $docName && $item['bdate'] == $regDay) {
        if ($item['pm'] != 0 && $item['pm'] != '约满') { //下午有号
            $aorp      = 1;
            $bespeakid = (int)($item['id']);
            var_dump($item);
            break;
        }else if ($item['am'] != 0 && $item['am'] != '约满') { //上午有号
            $aorp      = 0;
            $bespeakid = (int)($item['id']);
            var_dump($item);
            break;
        }
    }
}


//4.注册
if($uid == -1 || $sickid == -1 || $bespeakid == -1){
    var_dump($uid,$sickid,$bespeakid,'failed');
    exit;
}


$regUrl = "//hospital?act=bespeak";
$postData = array(
    'oid' => $wxID,
    'uid' => $uid,
    'sickid' => $sickid,
    'bespeakid' => $bespeakid,
    'aorp' => $aorp,
);
$res = SendPostRequest($regUrl,$postData);
if($res['result'] == 'ok'){
    var_dump('succcess',$postData);
}else{
    var_dump('failed',$postData,$res);
}

通过代码可以看出该功能主要包含以下函数:获得当前时间、发送请求、获取该微信号对应的用户id信息、获取病人id信息、获取医生可用时间段、注册功能。而且初步判断整个使用微信注册流程就包含如上步骤。

代码有如下问题:使用面向过程、命名待优化、缺乏注释、复杂逻辑没有拆分为函数和类、有重复逻辑、不支持多个医院、请求链接与参数配置散乱。

我们可以做如下设计:获取当前时间放到工具类里;发送请求放到网络类里;和医院进行交互的操作可以放到一个类里,但是解析结果功能需要能够替换,毕竟每家医院返回结果可能不一致;创建工厂类用于选择不同医院;

修改后代码为:

<?php


//工具类
class Utils
{
    //返回当前的毫秒时间戳
    public function msectime()
{
        list($msec, $sec) = explode(' ', microtime());
        $msectime         = (float) sprintf('%.0f', (floatval($msec) + floatval($sec)) * 1000);
        return $msectime;
    }
    //拼接URL
    public function buildQuery($url,$arr)
{
        return $url."?".http_build_query($arr);
    }
}


//Http客户端
class HttpClient
{
    //http Get请求
    function sendGetRequest($url)
{
        $curl = curl_init();
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
        curl_setopt($curl, CURLOPT_TIMEOUT, 5);
        curl_setopt($curl, CURLOPT_URL, $url);
        $output    = curl_exec($curl);
        curl_close($curl);
        return $output;
    }
    //http Post请求
    function sendPostRequest($url,$postData)
{
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
        curl_setopt($ch, CURLOPT_POST, 1);
        curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);
        $output = curl_exec($ch);
        curl_close($ch);
        return $output;
    }
}


//和医院系统交互类
class HospitalOperation
{
    public $httpClient;
    public $hospital;


    function __construct($hospital)
{
        $this->httpClient = new HttpClient();
        $this->hospital = $hospital;
    }


    //1.获取个人id
    public function getUserId()
{
        var_dump("开始执行getUserId");
        $url = $this->hospital->getUinfoUrl();
        $uInfo   = $this->httpClient->sendGetRequest($url);
        if(empty($uInfo)){
            var_dump("执行getUserId失败");
            return -1;
        }
        $uid = $this->hospital->getUid($uInfo);
        var_dump("getUserId成功,uid为$uid");
        return $uid;
    }


    //2.获取病人id
    public function getPatientId()
{
        var_dump("开始执行getPatientId");
        $url = $this->hospital->getPatientUrl();var_dump($url);
        $listInfo   = $this->httpClient->sendGetRequest($url);
        if(empty($listInfo)){
            var_dump("执行getPatientId失败");
            return -1;
        }
        $patientId = $this->hospital->getPatientId($listInfo);
        var_dump("getPatientId成功,patientId为$patientId");
        return $patientId;
    }


    //3.获取医生可用时间段的id
    public function getBresPeakId()
{
        var_dump("开始执行getBresPeakId");
        $url       = $this->hospital->getBresPeakUrl();
        $bresInfo  = $this->httpClient->sendGetRequest($url);
        if(empty($bresInfo)){
            var_dump("执行getBresPeakId失败");
            return -1;
        }
        $bresPeakId = $this->hospital->getBresPeakId($bresInfo);
        if($bresPeakId == -1){
            var_dump("getBresPeakId失败,没有合适的时间");
        }else{
            var_dump("getBresPeakId成功,bresPeakId为$bresPeakId");
        }
        return $bresPeakId;
    }


    //4.注册
    public function registe()
{
        var_dump("开始执行registe");
        $url = $this->hospital->getRegistUrl();
        $postData = $this->hospital->getPostData();
        foreach($postData as $checkPoint){
            if($checkPoint == -1){
                var_dump('check postdata failed',$postData);
                return -1;
            }
        }
        $regInfo = $this->httpClient->sendPostRequest($url,$postData);
        $res = $this->hospital->getRegistRes($regInfo);
        if($res === true){
            var_dump('registe succcess',$postData);
        }else{
            var_dump('registe failed',$postData,$res);
        }
        return $res;
    }


    //5.执行整个流程
    function run(){
        $res = $this->getUserId();
        if($res == -1){
            exit;
        }
        $res = $this->getPatientId();
        if($res == -1){
            exit;
        }
        $res = $this->getBresPeakId();
        if($res == -1){
            exit;
        }
        $res = $this->registe();
        if($res == -1){
            exit;
        }
    }
}


//返回数据解码
class Decode
{
    public function decodeFunc($data)
{
        $data = json_decode($data, true);
        return $data;
    }
}


//医院类
class Hospital
{
    protected $wxId; //微信ID
    protected $regDepId;//预约诊室
    protected $docName;//医生姓名
    protected $patientName;//患者姓名
    protected $regDay;//看病日期


    protected $uInfoUrl; //根据微信号获取个人信息URL
    protected $patientListUrl; //获取患者列表URL
    protected $bresPeakUrl; //医生出诊时间URL
    protected $regUrl; //注册URL


    protected $uId; //微信ID对应的用户ID
    protected $postData;//注册需要提交的数据


    public $utils;//工具类
    public $decode;//接口返回数据解析方式


    function __construct($wxId,$regDepId,$docName,$patientName,$regDay,$uInfoUrl,$patientListUrl,$bresPeakUrl,$regUrl,$decode)
{
        $this->wxId = $wxId;
        $this->regDepId = $regDepId;
        $this->docName = $docName;
        $this->patientName = $patientName;
        $this->regDay = $regDay;


        $this->uInfoUrl = $uInfoUrl;
        $this->patientListUrl = $patientListUrl;
        $this->bresPeakUrl = $bresPeakUrl;
        $this->regUrl = $regUrl;
        $this->decode = $decode;
        $this->utils = new Utils();
        $this->initPostData();
    }


    public function setPostDataItem($key,$value)
{
        $this->postData[$key] = $value;
    }
    public function getPostData()
{
        return $this->postData;
    }


    public function initPostData(){}


    public function getUinfoUrl(){}
    /**
     * 获得微信对应的用户id,-1表示获取失败
     */
    public function getUid($res):int {}


    public function getPatientUrl(){}
    /**
     * 获得患者Id,-1表示获取失败
     */
    public function getPatientId($res):int{}


    public function getBresPeakUrl(){}
    /**
     * 获得医生问诊时段Id,-1表示获取失败
     */
    public function getBresPeakId($res):int{}


    public function getRegistUrl(){}
    /**
     * 获得注册结果,-1表示注册失败
     */
    public function getRegistRes($res):int{}
}




/**
 * Class HospitalA
 * 继承自Hospital类,主要用于
 * 1. 生成各个接口的URL
 * 2. 解析返回数据,获取想要的结果
 * 3. 最终生成注册的数据postData
 */
class HospitalA extends Hospital
{
    public function initPostData()
{
        $this->postData = array(
            'oid' => $this->wxId,
            'uid' => -1,
            'sickid' => -1,
            'bespeakid' => -1,
            'aorp' => -1,
        );
    }


    public function getUinfoUrl()
{
        $arr = array(
            'act'=>'userinfo_oid',
            'uid'=>$this->wxId,
            'tm'=>$this->utils->msectime(),
            'oid'=>$this->wxId,
        );
        $url = $this->utils->buildQuery($this->uInfoUrl,$arr);
        return $url;
    }


    public function getUid($res):int
{
        $res = $this->decode->decodeFunc($res);
        if($res['result'] != 'ok'){
            return -1;
        }
        $uInfo = $this->decode->decodeFunc($res['data']);
        $this->setPostDataItem('uid',$uInfo['id']);
        $this->uId = $uInfo['id'];
        return $uInfo['id'];
    }


    public function getPatientUrl()
{
        $arr = array(
            'act'=>'member',
            'uid'=>$this->uId,
            'oid'=>$this->wxId,
        );
        $url = $this->utils->buildQuery($this->patientListUrl,$arr);
        return $url;
    }


    public function getPatientId($res):int
{
        $res = $this->decode->decodeFunc($res);
        if($res['result'] != 'ok'){
            return -1;
        }
        $list = $this->decode->decodeFunc($res['data']);
        foreach ($list as $item) {
            if ($item['name'] == $this->patientName) {
                $this->setPostDataItem('sickid',$item['id']);
                return $item['id'];
            }
        }
        return -1;
    }


    public function getBresPeakUrl()
{
        $arr = array(
            'act'=>'bespeak_v1',
            'deptid'=>$this->regDepId,
            'clsid' => 2,
            'tm'=>$this->utils->msectime(),
        );
        $url = $this->utils->buildQuery($this->bresPeakUrl,$arr);
        return $url;
    }


    public function getBresPeakId($res):int
{
        $res = $this->decode->decodeFunc($res);
        if($res['result'] != 'ok'){
            return -1;
        }
        $docList = $this->decode->decodeFunc($res['data']);
        $bespeakid = -1;
        $aorp      = 0; //0上午 1下午
        $flag = 0;
        foreach ($docList as $item) {
            if ($item['name'] == $this->docName && $item['bdate'] == $this->regDay) {
                if ($item['pm'] != 0 && $item['pm'] != '约满') { //下午有号
                    $aorp      = 1;
                    $flag = 1;
                }else if ($item['am'] != 0 && $item['am'] != '约满') { //上午有号
                    $aorp      = 0;
                    $flag = 1;
                }
                if($flag == 1){
                    $bespeakid = (int)($item['id']);
                    var_dump('选择医生为',$item);
                    break;
                }
            }
        }
        $this->setPostDataItem('bespeakid',$bespeakid);
        $this->setPostDataItem('aorp',$aorp);
        return $bespeakid;
    }


    public function getRegistUrl()
{
        $arr = array(
            'act'=>'bespeak',
        );
        $url = $this->utils->buildQuery($this->regUrl,$arr);
        return $url;
    }


    public function getRegistRes($res):int
{
        $res = $this->decode->decodeFunc($res);
        if($res['result'] != 'ok'){
            return -1;
        }
        return 1;
    }


}


function main(){
    $decode = new Decode();
    $docName     = '**';
    $patientName = '***';
    $wxId        = '***';
    $regDepId    = 0;
    $regDay      = '2021-03-22';
    $uInfoUrl = '***';
    $patientListUrl = '***';
    $bresPeakUrl = '***';
    $regUrl = '***';
    $hospitalName = 'A';
    switch ($hospitalName){
        case 'A': $hospital = new HospitalA($wxId,$regDepId,$docName,$patientName,$regDay,$uInfoUrl,$patientListUrl,$bresPeakUrl,$regUrl,$decode);
    }


    $oper = new HospitalOperation($hospital);
    $oper->run();
}


/**
 * 使用:
 * 对接新的医院,可继承Hospital类,实现类中的函数。确保最终postData中包含所需数据,用于注册
 * 如果解析方式不一样,或者有医院的返回数据编码进行了更改,可继承Decode类,实现新的Decode类,进行替换
 * 对于新医院,在main中根据医院名称,使用工厂方法生成对应的医院对象
 * 可以将URL等参数做配置化,可减少初始化传入的数据
 */
main();

改写后的代码还有一定的优化空间,如错误返回、函数返回限制、父类和子类的关系限制、代码可测试性检查等。大家有时间可以尝试再优化一下。

总结

写出高质量的代码需要付出的精力比随便写要多得多,但随着不断的练习,速度和质量都会很快的提升。需要先有这个意识、然后掌握一定方法、然后不断实践,最终慢慢成功。

最后

大家如果喜欢我的文章,可以关注我的公众号(程序员麻辣烫)

我的个人博客为:https://shidawuhen.github.io/

往期文章回顾:

技术

  1. Go设计模式(3)-设计原则
  2. Go设计模式(2)-面向对象分析与设计
  3. 支付接入常规问题
  4. HTTP2.0基础教程
  5. Go设计模式(1)-语法
  6. MySQL开发规范
  7. HTTPS配置实战
  8. Go通道实现原理
  9. Go定时器实现原理
  10. HTTPS连接过程
  11. 限流实现2
  12. 秒杀系统
  13. 分布式系统与一致性协议
  14. 微服务之服务框架和注册中心
  15. Beego框架使用
  16. 浅谈微服务
  17. TCP性能优化
  18. 限流实现1
  19. Redis实现分布式锁
  20. Golang源码BUG追查
  21. 事务原子性、一致性、持久性的实现原理
  22. CDN请求过程详解
  23. 常用缓存技巧
  24. 如何高效对接第三方支付
  25. Gin框架简洁版
  26. InnoDB锁与事务简析
  27. 算法总结

读书笔记

  1. 原则
  2. 资治通鉴
  3. 敏捷革命
  4. 如何锻炼自己的记忆力
  5. 简单的逻辑学-读后感
  6. 热风-读后感
  7. 论语-读后感
  8. 孙子兵法-读后感

思考

  1. 实践论
  2. 评价自己的标准
  3. 服务端团队假期值班方案
  4. 项目流程管理
  5. 对项目管理的一些看法
  6. 对产品经理的一些思考
  7. 关于程序员职业发展的思考
  8. 关于代码review的思考
  9. Markdown编辑器推荐-typora

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注