设计原则Go设计模式(3)-设计原则是从相对高的维度来进行代码设计,设计的再好,代码编写不优雅,代码质量也难以得到保障。
即使当时写的很好,如何保证随着业务变化、其他同学的修改,代码仍然是优雅的呢?另外,怎样的代码称之为优雅,又让谁来评判呢?
所以代码编写至少涉及三个问题:
高质量代码的标准是什么?
如何编写高质量代码?
如何保证代码一直高质量?
当然,在代码编写方面,我仍然是个学生,大家要是觉得有价值可以参考一下,如果有错误的地方,也希望大家多多批评指正。
1高质量代码的标准
代码质量评价有很高的主观性。一般最常用到几个评判代码质量的标准有:可维护性、可读性、可扩展性、灵活性、简洁性、可复用性、可测试性。其中,可维护性、可读性、可扩展性又是提到最多的、最重要的三个评价标准。
要写出高质量代码,需要善用面向对象设计思想、设计原则、设计模式、编码规范、重构技巧等。
Go设计模式(3)-设计原则和Go设计模式(2)-面向对象分析与设计更多讲的是可维护性、可扩展性、灵活性、可复用性。而可读性、简洁性、可测试性则与基础的编码能力相关。
2如何编写高质量的代码
2.1编程规范
仅从编写看起来优雅的代码层面讲,起效最快的是符合编程规范。编程规范其实分两部分:
- 通用规范,各种语言都能用
- 指定语言规范,这是针对各种语言自己特性编写的,如Go、Java、PHP等,网上有很多可以拿来参考
这里我们仅讲一下通用编程规范。通用编程规范一般包含三个方面,命名与注释、代码风格、编程技巧。
命名与注释
命名与注释主要是为了增加可读性。但有部分程序员命名能力实在堪忧(说的就是我),好在找到了一个神器https://unbug.github.io/codelf/,可以查一下其他人是怎么命名的。
- 命名的关键是能准确达意
- 借助类的信息来简化属性、函数的命名,利用函数的信息来简化函数参数的命名
- 命名要可读、可搜索,不要使用生僻的、不好读的英文单词
- 接口的2中命名方式:在接口中带前缀"I";在接口的实现类中带后缀"Impl"。抽象类的2中命名方式:带上前缀“Abstract";不带前缀
- 注释的内容:做什么、为什么、怎么做。复杂的类和接口,还要写明”如何用“
- 类和函数一定要写注释,而且要写的尽可能全面详细
代码风格
- 函数的代码行数不要超过一屏幕的大小,比如50行
- 一行代码最好不要超过IDE的显示宽度
- 善用空行分割单元块
- 推荐两格缩进,节省空间。一定不要用tab键缩进
- 将大括号跟上一条语句同一行,可以节省代码行数,另起新的一行,结构清晰
- 在GoogleJava编程规范中,依赖类按照字母序从小到大排列。类中先写成员变量后写函数,成员变量之间或函数之间,先写静态成员变量或函数,后写普通变量或函数,并且按照作用域大小依次排列
编程技巧
- 将复杂的逻辑提炼拆分成函数和类
- 通过拆分成多个函数或将参数封装为对象的方式,来处理参数过多
- 函数中不要使用参数来做代码执行逻辑的控制
- 函数设计要职责单一
- 移除过深的嵌套层次
- 用字面常量取代魔法数
- 用解释性变量来解释复杂表达式
2.2代码可测试性
代码的可测试性是指针对代码编写单元测试的难易程度。容易编写单元测试就说明代码可测试性好,反之意味代码设计的并不是很合理。
提高代码可测试性有两个手段
- 依赖注入可以通过mock的方法将不可控的依赖变得可控
- 利用二次封装来解决某些代码行为不可控的情况
代码的可测试性也是检查代码是否高质量的一个手段。写完代码之后,一定记得编写单元测试。首先需要思考有哪些测试用例,然后开始编写单元测试,此时可能发现有部分单元测试无法编写。原因一般有如下几条:
- 未决行为:如代码和时间、随机数相关。一般使用二次封装来解决。
- 全局变量:全局变量会导致上一次运行的结果可能影响下一个运行。一般使用reset全局变量解决。可以看看能否不用全局变量。
- 静态方法:不好处理,非要测试只能mock。可以看看能否不使用静态方法。
- 复杂继承:父类需要mock某个依赖对象才能进行单元测试,那所有的子类、子类的子类都需要mock出这个对象。能做单元测试,就是比较麻烦。可以看看是否能改为组合。
- 高耦合代码:依赖十几个外部对象完成工作,需要mock十几个外部对象来进行单元测试。看看是否可以通过重构解耦。
3如何保证代码一直高质量
要想代码随着业务的变更、新人的更改一直保持高质量,除了不断的重构别无它法。这也分情况,如果是新项目,大家技术水平高、都有不断重构的意识、代码Review也会考虑代码质量问题,保持代码高质量相对简单一些。如果是老旧项目、当时活多时间紧,代码质量已经很差了,把这种代码提升到较高质量会难很多。
基于这两种情况,重构方案也有所不同。
3.1重构
对于第一种情况,使用编程规范中的方法进行优化。当然新的业务还是需要用到面向对象设计思想、设计原则、设计模式等。
对于第二种情况,就需要做大型重构了。代码是否需要做大型重构,一线开发人员最有发言权。如果觉得这个代码进行细微改动后不知道会引发什么、或者感觉改不动了、或者这个代码的开发严重影响了项目进度,那肯定是要重构了,不重构难道留着过年?当然,重构是个技术活,但有个前提,需要上下一心,大家统一认识,要有重构的决心。
大型重构主要靠解耦。代码松耦合、高内聚,是控制代码复杂度的有效手段。为了解耦,一般使用如下方案:
- 封装与抽象:有效地隐藏实现的复杂性,隔离实现的 易变性,给依赖的模块提供稳定且易用的抽象接口
- 引入中间层:简化模块或类之间的依赖关系
- 模块化:分而治之
- 使用设计思想和原则:单一职责原则、基于接口而非实现编程、依赖注入、多用组合少用继承、迪米特发展
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/
往期文章回顾:
技术
- Go设计模式(3)-设计原则
- Go设计模式(2)-面向对象分析与设计
- 支付接入常规问题
- HTTP2.0基础教程
- Go设计模式(1)-语法
- MySQL开发规范
- HTTPS配置实战
- Go通道实现原理
- Go定时器实现原理
- HTTPS连接过程
- 限流实现2
- 秒杀系统
- 分布式系统与一致性协议
- 微服务之服务框架和注册中心
- Beego框架使用
- 浅谈微服务
- TCP性能优化
- 限流实现1
- Redis实现分布式锁
- Golang源码BUG追查
- 事务原子性、一致性、持久性的实现原理
- CDN请求过程详解
- 常用缓存技巧
- 如何高效对接第三方支付
- Gin框架简洁版
- InnoDB锁与事务简析
- 算法总结
读书笔记
- 原则
- 资治通鉴
- 敏捷革命
- 如何锻炼自己的记忆力
- 简单的逻辑学-读后感
- 热风-读后感
- 论语-读后感
- 孙子兵法-读后感
思考
- 实践论
- 评价自己的标准
- 服务端团队假期值班方案
- 项目流程管理
- 对项目管理的一些看法
- 对产品经理的一些思考
- 关于程序员职业发展的思考
- 关于代码review的思考
- Markdown编辑器推荐-typora