思考:减少DB查询次数,合理使用成员变量
高内聚,低耦合
是非常深入人心的设计思想,在做到高内聚低耦合的同时,我们也要考虑到值传递的问题:要避免在抽取函数,封装代码时不合理的值传递,避免在多个函数内部重复查询相同的DB。
举个栗子
需求描述
我们的项目是交友类APP,有划卡片喜欢、不喜欢、超级喜欢的动作,也有赠送礼物、邀请约会等动作
上述动作有各种判断,比如每天有喜欢的上限,又区分是不是会员;赠送礼物时又要判断是否开通了会员,扣费的话要不要使用会员价;邀请约会的时候要判断是不是好友等等;各种看起来平平无奇的场景融合在一起就让代码结构变得异常复杂了。程序设计变得异常重要。
需求分析
基于上述需求,我们定义了几个概念:动作、消费、记录;
把动作进行拆分:基础动作、付费动作、全局动作;把消费进行拆分:充值、消费、领券等;
根据拆分的概念实现逻辑代码,比如基础动作抽象为一个类,付费动作和全局动作继承这个类:付费的动作单独抽象为一个类,把付费相关的动作放到这里;全局动作指的是不管是不是会员,不换是不是消费,都能进行的动作,也抽象为一个单独的类。
(在项目开发第一版,我们是没做这种抽象的,在一个类里面实现了各种动作、消费、记录,随着项目的推进,变得非常臃肿混乱。)
上面讲的可能有些抽象,看下面的代码示例会清晰很多哦~
代码示例
为了行为紧凑,方便大家理解,我不直接粘贴我们的逻辑代码,而是把关键代码段拿出来分析。
整体结构
- 下述代码是上面提到的
全局动作类
,它继承了基础动作类
,所有的动作在基础动作类
中定义 - 规范了
输入参数
和输出参数
这些成员变量 - 构造方法传入当前用户id和对方id,所有的动作肯定是有双方的
- 规范了
setAction()
设置动作、getActionResult()
获得动作的执行结果,统一输入输出的格式和规范,不管在哪里调用,都遵守统一的规范
class UserUniversalAction extends BaseUserAction
{
//输入参数
protected $_userid; //当前用户ID
protected $_actionId; //动作ID
protected $_extra = []; //动作携带的额外参数
protected $_otherUserid; //对方用户ID
protected $_data = []; //中间存储
protected $_houseOpen = [];
protected $_userConsume = null;
protected $_now;
//输出参数
protected $_pass = true; //财务性约束,有没有消费通过
protected $_rechargeType = null; //消费的类型
protected $_propCount = 0;
protected $_consumeSuccess = false; //消费成功-默认false
protected $_canInvite = false; //
protected $_errorCode = 0; //非财务行约束,动作的关系执行条件
protected $_needAfterAction = true;
//protected $_out = []; //动作执行完成后,输出结果
public $hasRight = false;
public function __construct($userid, $otherUserid = '')
{
parent::__construct($userid, 0);
$this->_otherUserid = $otherUserid;
//初始化参数
$this->_houseOpen = HouseOpen::getCurrentOpen();
//消费初始化
$this->_userConsume = new UserConsume($this->_userid);
$this->_now = time();
}
//设置执行动作
public function setAction($actionId, $extra = [])
{
//设置动作参数
$this->_actionId = $actionId;
$this->_extra = $extra;
//前置动作,执行权限校验等
$this->_beforeAction();
//只有非财务性约束和财务性约束通过后,才能继续执行
if ($this->_errorCode == 0 && $this->_pass) {
//执行动作
$this->_actionExecute();
if ($this->_errorCode == 0) {
//后置动作
$this->_afterAction();
}
}
}
//获取动作执行结果
public function getActionResult()
{
return [
//财务性限制
'pass' => $this->_pass,
'rechargeType' => $this->_rechargeType,
'propCount' => $this->_propCount,
'canInvite' => $this->_canInvite,
//业务性限制
'errorCode' => $this->_errorCode,
'out' => $this->_data
];
}
设置执行动作详解
细心的同学可能已经发现了:我们又对setAction()
做了进一步的拆解,拆分为:
_beforeAction
:前置动作,执行权限校验等,比如只有开通了会员才允许超级喜欢,只有成为了好友才允许邀请约会。
_actionExecute
:执行动作,比如触发了超级喜欢,比如向对方发起了约会邀请。
_afterAction
:后置动作,比如发起约会时给对方发通知栏消息告知对方;比如约会结束之后发推送消息告知对方的约会感受。
这样做的好处是异常清晰,几乎所有的动作都可以理解为三步:动作前、动作中、动作后。
另外一个比较硬核的地方是传入的第二个参数 $extra = []
:
传入的第一个参数很好理解:$actionId
就是我们定义的动作id,我们根据动作id判断要执行哪些动作。
第二个参数$extra = []
,extra是扩展参数、可变参数的概念。就和我开篇提到的减少DB查询,合理使用成员变量 呼应上了:
把需要在多处使用到的参数传入,而不是每次都通过查询DB的方式获得。我们以参数形式传入的数据可以赋值给成员变量
//设置执行动作
public function setAction($actionId, $extra = [])
{
//设置动作参数
$this->_actionId = $actionId;
$this->_extra = $extra;
//前置动作,执行权限校验等
$this->_beforeAction();
//只有非财务性约束和财务性约束通过后,才能继续执行
if ($this->_errorCode == 0 && $this->_pass) {
//执行动作
$this->_actionExecute();
if ($this->_errorCode == 0) {
//后置动作
$this->_afterAction();
}
}
}
核心
下面的示例代码能让大家更好的理解如何合理的使用成员变量
老规矩先说需求:在约会结束时进行判断,如果线上语音约会时间小于1分钟则补偿给用户约会券(我们认为约会时间小于1分钟的就是体验不好的约会,不能让用户白花钱,要给予优惠券补偿)
如果是常规设计:我们需要至少查询3次DB,即:
- 触发结束约会时修改状态,进行一系列读写操作,返回给客户端最新的数据状态
- 在
_afterAppointmentFinish
中查询语音房是否是开放的状态(我们产品是有营业概念的,只有营业中可执行约会动作) - 在
_afterAppointmentFinish
中根据约会id,查询双方约会时长等信息
通过成员变量传参的方式,只需要1次查询DB,即:
- 触发结束约会时修改状态,进行一系列操作,返回给客户端最新的数据状态的同时,通过
$this->_data = $appointmentModel->toArray();
赋值给成员变量;_afterAppointmentFinish()
中通过$this->_data
取值就可以了。
protected function _actionExecute()
{
//执行权限校验
switch ($this->_actionId) {
.
.
.
case self::TYPE_ACTION_END:
$this->_doAppointmentEndActionExecute();
break;
}
}
protected function _afterAction()
{
//动作后置操作
switch ($this->_actionId) {
.
.
.
case self::TYPE_ACTION_END:
$this->_afterAppointmentFinish();
break;
default:
}
}
protected function _doAppointmentEndActionExecute()
{
$appointmentModel = AppointmentInfo::query()->selectRaw('id,userid,"inviteeUserid",status,endtime,"callStartTimestamp","callDuration","isConsume","appointmentOpenId"')
->where('id', $this->_extra['appointmentId'])->first();
.
.
.
$appointmentModel->endtime = time();
$appointmentModel->status = AppointmentInfo::TYPE_STATUS_END;
$appointmentModel->save();
$this->_data = $appointmentModel->toArray();
.
.
.
}
protected function _afterAppointmentFinish()
{
$houseOpen = $this->_houseOpen; //减少1次DB查询
if ($houseOpen['status'] != HouseOpen::HOUSE_STATUS_OPEN) {
return false;
}
//减少2次DB查询
if (isset($this->_data)
&& $this->_data['isConsume'] == AppointmentInfo::TYPE_IS_CONSUME
&& $this->_data['appointmentOpenId'] == $houseOpen['currentAppointmentOpenId']
&& $this->_data['callDuration'] < 60) { //约会时长不足1分钟,花了多少补偿多少精酿券
.
.
.
}
}
}
上面只是一个简单的栗子,随着项目推进,应用场景增多,合理使用成员变量会体现出更高的价值。
回顾
大家再回顾一下我开篇提到的输入参数,这些都是成员变量,其中 _extra ,_data ,_houseOpen
都是易于扩展的数组类型,我们可以通过合理的使用成员变量,减少冗余的DB查询,提高程序的运行效率。
//输入参数
protected $_userid; //当前用户ID
protected $_actionId; //动作ID
protected $_extra = []; //动作携带的额外参数
protected $_otherUserid; //对方用户ID
protected $_data = []; //中间存储
protected $_houseOpen = [];
protected $_userConsume = null;
protected $_now;
总结
要知道每次DB查询都是有网络耗时的;我们把数据存到成员变量,从内存中读取数据的耗时是可以忽略不计的。