由Yii Event事件领悟到的开闭原则
最近在了解Yii 2的触发事件相关的知识,以下摘抄自yiichina话题
事件的触发
事件通过Component::trigger()来触发,触发本质就是执行事件handler的过程。
$order->trigger(Order::EVENT_CREATED);
源码如下:
public function trigger($name, Event $event = null){
this->ensureBehaviors();
if(!empty($this->_events[$name])) {// 执行handler必须传递一个Event实例,用来传递数据if($event ===null) { $event =newEvent(); }// 指定是谁触发的这个事件,默认就是trigger调用者自身if($event->sender ===null) { $event->sender =$this; } $event->handled =false;// 默认事件没有被处理$event->name = $name;// 事件名称foreach($this->_events[$name]as$handler) { $event->data = $handler[1];// 最关键的地方:所有handler都是通过call_user_func来执行的call_user_func($handler[0], $event);// 如果在$handler[0]中,$event->handled被置为true,表示事件已经被处理好了,后续handler不用再执行了if($event->handled) {return; } } }// 执行类级别的事件处理器Event::trigger($this, $name, $event);}
在触发事件时,我们经常会通过Event对象来传递数据:
假如有个场景是在文章下面评论送积分,那么评论后就会触发送积分的事件user_after_publish,另外我们知道送积分还有很多别的场合,不同场合送的分数不一样。因此我们积分需要有个类PointEvent来表示:
use yii\base\Event;classPointEventextendsEvent{//要赠送的积分数量public $points =0;// 事件处理结果消息public $msg = '';// 其他方法}
发表评论之前,先绑定“送积分”事件handler:
// 绑定handler$user = Yii::$app->user->identity;$user->on(User::EVENT_AFTER_PUBLISH, [$obj,'afterPublish']);....// 在某个时间点触发// 实例化一个PointEvent,points 指定为 10$event =newPointEvent();$event->points =10;$user->trigger(User::EVENT_AFTER_PUBLISH, $event);
事件处理器要为发表评论的用户积分+10:
publicfunctionafterPublish($event){ $user = $event->sender; $points = $event->points; $user->points += $points; $user->save();}
说到这里,可能有的小伙伴会问,用户发表评论获得积分这么个功能为啥要通过这种方式来实现,为啥不用下面“简单”的方式来实现呢:
publicfunctionpublishComment(){ $param = Yii::$app->request->post(); $user = Yii::$app->user->identity;// 新建评论$comment =newComment(); $comment->load($param,'data'); $comment->save();// 更新用户积分$user->points += intval($param['points']); $user->save(); }
这样做,就是把新建评论和更新用户放在一个方法里面。但是这样做将会有个隐形的问题,如果用户评论之后,不光要积分+10,还要告知文章作者怎么办?这里只能在publishComment()后面继续添加代码了,如果哪天又要发送邮件啥的,还得继续往里面添加代码。这违反了“面对扩展开放,面对修改关闭”的编程原则,久而久之这块代码就会变得非常臃肿,很难再维护,分出去再写几个方法也无济于事。
分析这原因,就是从功能上说,评论和用户获得积分其实是松耦合的关系——你可以给积分也可以不给积分。但是在publishComment()方法你把这两个功能捆绑的死死的,丝毫分不开——这就是一种糟糕的设计。
相反,用事件就能很好解决这个问题。我们说,事件其实是一个流程上的某个特定的点,这里是流程就是用户发表评论,EVENT_AFTER_PUBLISH是发表成功后的一个节点,程序走到这里,触发一下。有处理器就执行,没有就继续往后执行。我们绑定了一个送积分的处理器 ,也可以绑定推送消息的处理器。这么看待问题,就已经解耦了两种功能。在实现上,将发表评论看做流程本身,而送积分,推送提醒看做是“附加”的,在需要的时候绑定下,不需要就不绑定。
下面是主流程:
publicfunctionpublishComment(){$user = Yii::$app->user->identity;// 新建之前的时间点:触发EVENT_BEFORE_PUBLISH$user->trigger(User::EVENT_BEFORE_PUBLISH);// 新建评论$comment =newComment(); $comment->load($param,'data'); $comment->save();// 新建之后的时间点:触发下 EVENT_AFTER_PUBLISH$user->trigger(User::EVENT_AFTER_PUBLISH, $event);}
有的场合是需要送积分:
$user->on(User::EVENT_AFTER_PUBLISH, [Points,'givePoints']);
有的场合是要发邮件提醒:
$user->on(User::EVENT_AFTER_PUBLISH, [Email,'notifyPoserOwner']);
还有的场合是需要判断用户有没有发表评论的权限:
$user->on(User::EVENT_BEFORE_PUBLISH, [Authorization,'checkAuth']);...publicfunctioncheckAuth($event){ $user = $event->sender;if(!$user->isAdmin) {thrownewAccessDeniedException('你没有权限发表评论'); }}
事件处理器Points::givePoints,Email::notifyPoserOwner,Authorization::checkAuth 分布在不同的类中,并没有出现在上面publishComment里面,而且可增可减,完全视需要而定。因此真正满足了“开闭原则”的要求。
小编心得:开闭原则——对扩展开放,对修改关闭。这是代码组织当中的一条非常重要的原则。