Observer 观察者模式

Observer

含义

定义对象之间的一对多依赖关系,以便当一个对象更改状态时,它的所有依赖关系将被自动通知和更新

适用性

当希望对目标对象的特定事件做出反应时,将使用观察者模式。什么时候目标对象分发生变化,就通知观察者。其实,PHP 已经有了观察者模式的内置接口:SplSubjectSplObserver

抽象结构

  • SplSubject 是一个抽象类或者接口。其内部可以定义一个数组保存附加的观察者们. SplSubject 包含三种方法。 attach 方法将观察者添加到目标对象,detach 方法分离观察者。 notify 方法通常包含循环,遍历所有附加的观察者并调用其更新方法。
  • SplObserver 也是一个抽象类或者接口。SplObserver 内有一个 update 方法,由于 SplSubject 触发目标对象的更新。
  • RealSubjectSplSubject 接口的实现。 包含存储附加观察者的对象实例(数组/集合等),实际上是标准里 SplObjectStorage 类的实例,可以看做是一个整洁的助手类(helper class),该类提供了一种对象到数据的映射,具体参见手册。
  • RealObserverSplObserver 接口的实现。它的 update 方法传递一个 SplSubject 实例对象作为参数,任何时候目标对象发生变动,它就会执行相应的业务逻辑

示例

在这个例子中,将使用 PHP 内置的 SplObserverSplSubject 来显示一个相当通用的观察者模式,之后,探索一下 Laravel Eloquent 模型内部结构,以及如何使用事件来处理附加的观察者。

SPL Obeserver

使用通用的 SPL 接口,只需定义两个类,为了演示方面,不作命名空间区分


<?php


class RealSubject implements \SplSubject
{
    private $observers;

    public function __construct($name)
    {
        $this->name = $name;
        $this->observers = new \SplObjectStorage();
    }

    public function attach(\SplObserver $observer)
    {
        // TODO: Implement attach() method.
        $this->observers->attach($observer);
    }

    public function detach(\SplObserver $observer)
    {
        // TODO: Implement detach() method.
        $this->observers->detach($observer);
    }

    public function notify()
    {
        // TODO: Implement notify() method.
        foreach ($this->observers as $observer) {
            $observer->update($this);
        }
    }
}

class RealObserver implements \SplObserver
{
    private $name;
    public function __construct($name)
    {
        $this->name = $name;
    }


    public function update(\SplSubject $subject)
    {
        // TODO: Implement update() method.
        print "{$this->name} was notified by {$this->name}" . PHP_EOL;
    }
}

$subject1 = new RealSubject('subject1');
$observer1 = new RealObserver('observer1');
$observer2 = new RealObserver('observer2');
$observer3 = new RealObserver('observer3');

$subject1->attach($observer1);
$subject1->attach($observer2);
$subject1->attach($observer3);
$subject1->notify();



测试结果:

observer1 was notified by subject1
observer2 was notified by subject1
observer3 was notified by subject1

RealSubjectRealObserver,都是通用的实现方式,但是有其很大局限性,这些通用的方法是约束的,你不能在 update 方法里添加任何除了 SplSubject 类的类型提示(type hint),你可能也并不总是希望通知的方式是公开的,而希望是内部流程发生时,观察者才得到通知,正是使用通用 SPL 接口的限制,这里你无法更改。那怎么办呢?

抽烟的故事

我们都知道人是复杂的,可以是观察者也可以是被观察者,或者说他既是 目标对象(Subject) 又是 观察者(Observer),下面通过一个故事来模拟下:

大学通宵打 LOL 或 Dota 的时候,什么不能少?香烟!没错,尤其后半夜闹烟慌了,没烟抽了,这时候你只剩唯一一根,刚一点着,室友闻到了,挖槽,不给我来一口,这时候你咋办?是我就赶紧跑出去自己偷偷吸起来,哈哈,开玩笑。下面用代码模拟下这个情景,这里 “你” 可以看作目标对象,“室友” 看作观察者,我们期望的输出应该是这样:

--- 你 点着了一根 芙蓉王 ---

小明说:    “擦,我闻到了芙蓉王的味道”

小贱说:    “擦,我闻到了芙蓉王的味道”

小王说:    “擦,我闻到了芙蓉王的味道”

你说:      “握草,一帮畜生,我先来一口”

--- 你 点着了一个根 中华 ---

你说: “麻蛋,幸亏还剩一根中华,哈哈”

接下来用代码实现:


<?php

// 先定义两个接口,点烟的人,就是你,闻到香烟的室友
interface CigaretteSmeller
{
    public function smell(CigaretteSmoker $smoker, $cigarette);
}

interface CigaretteSmoker
{
    public function nearBy(CigaretteSmeller $smeller);
    public function noLongerNearBy(CigaretteSmeller $smeller);
    public function lightUp($cigarette);
}

// 给故事里的人都起个名字,定义一个说话的方法
class Person implements CigaretteSmoker, CigaretteSmeller
{
    public function __construct($name)
    {
        $this->name = $name;
        $this->observers = new \SplObjectStorage();
    }

    public function says($phrase)
    {
        print "{$this->name} 说: \t\"" . $phrase . "\"" . PHP_EOL;
    }
    
    // 实现接口中的 nearBy 方法
    public function nearBy(CigaretteSmeller $smeller)
    {
        $smellers = func_get_args();
        foreach ($smellers as $smeller) {
            $this->observers->attach($smeller);
        }
    }
    
    // 实现接口中的 noLongerNearBy 方法
    public function noLongerNearBy(CigaretteSmeller $smeller)
    {
        $smellers = func_get_args();
        foreach ($smellers as $smeller) {
            $this->observers->detach($smeller);
        }
    }
    
    // 实现接口中的 lightUp 方法,你一点着香烟附近的室友就靠过来
    public function lightUp($cigarette)
    {
        print "--- {$this->name} lightUp {$cigarette} ---" . PHP_EOL;
        foreach ($this->observers as $observer) {
            $observer->smell($this, $cigarette);
        }
    }
    // 实现接口中的 smell 方法
    public function smell(CigaretteSmoker $smoker, $cigarette)
    {
        $this->says("擦,我闻到了{$cigarette}的味道");
    }

}

$you = new Person('你');
$wang = new Person('小王');
$li = new Person('小李');
$jian = new Person('小贱');

$you->nearBy($wang, $li, $jian);
$you->lightUp('芙蓉王');
$you->says('握草,一帮畜生,我先来一口');

$you->noLongerNearBy($wang, $li, $jian);
$you->lightUp('芙蓉王');
$you->says("麻蛋,幸亏还剩一根中华,哈哈");

哈哈。是不是稍微理解了 Observers 模式的应用呢。这里与前面通用的 SPL 示例不同,这段代码反映了一定的业务逻辑,两者都使用到了观察者模式。接下来我们看看 ObserverEloquent 里是如何应用的。

Eloquent Observer:开箱即用

所有的 Eloquent 模型都内置了观察者模式。我们创建一个 Car 模型,汽车会有些基本属性:制造商,车辆识别码(vin),年代,描述等。

app/Car.php

namespace App;
use Illuminate\Datebase\Eloquent\Model;
class Car extends Model
{
    
}

怎么在这个 model 上设置观察者呢,很简单,使用 observe 方法。

app/test1.php

\App\Car::observe(new Observers\ObserveEverything);

创建了一个通用的观察者,名为 ObserveEverything,它包含了所有可以在 Eloquent 模型上使用的那些开箱即用的方法。简单起见,每个方法打印一个语句,以便了解何时被调用的。方法列表如下:

app/Observers/ObserveEverything.php

class ObserveEverything
{
    public function creating($model)
    {
        print "creating model" . PHP_EOL;
    }       
    public function created($model)
    {
        print "created model" . PHP_EOL;    
    }
    public function updating($model)
    {
        print "updating model" . PHP_EOL;
    }
    public function updated($model)
    {
        print "updated model" . PHP_EOL;
    }
    public function saving($model)
    {
        print "saving model" . PHP_EOL;
    }
    public function saved($model)
    {
        print "saved model" . PHP_EOL;  
    }
    public function deleting($model)
    {
        print "deleting model" . PHP_EOL;
    }
    public function deleted($model)
    {
        print "deleted model" . PHP_EOL;
    }
    public function restoring($model)
    {
        print "restoring model" . PHP_EOL;
    }
    public function restored($model)
    {
        print "restored model" . PHP_EOL;
    }
}

从每个方法名当中你或许已经知道何时被调用的。不过我还是稍微解释下:

  • 当 model 首次在数据库被创建之前,creating 方法被触发。可以通过新建 model 的 save 方法或静态 create 方法来触发。注意,当新的 model 已经被构建或检索时,不会触发该方法。如果方法返回 false,则 model 不会被创建。
  • 当 model 已经在数据库创建完成后,created 方法被触发。
  • 当已经存在的 model 在被更新之前,updating 方法会被触发。同理,返回 false ,model 不会被更新。
  • 当已经存在的 model 被更新之后,updated 方法被触发。
  • 当 model 被创建或者更新之前,saving 方法会被触发。同理,返回 false model 不会被保存。
  • 当 model 被创建或者更新之后,saved 方法被触发
  • 当 model 被删除之前,deleting 方法被触发。同理,返回 false,model 不会被删除。
  • 当 model 被删除之后 deleted 方法被调用。
  • 当 model 被还原之前 restoring 方法被调用,还原适用于应用了软删除(soft deletes)的模型。它将删除当前记录的 deleted_at 列。同理,返回 false,model 不会被删除。
  • 当 model 被还原之后 restored 方法被调用。

接下来,添加该 Obeserver 到 Car model 上。

app/test1.php

Car::observe(new Observers\ObserveEverything);

当 Car 模型发生一系列改变时将触发 ObserveEverything 观察者的调用。

app/test1.php

$car1 = Car::find(1);
$car->vin = str_random(32)
print "Saving car #1 to database" . "<br>";
$car1->save();

输出:

Saving car #1 to database
saving model
updating model
updated model
saved model

app/test1.php

$car2 = new \App\Car();
   $car2->description = "cool car description";
   $car2->vin = str_random(32);
   $car2->manufacturer = "Honda";
   $car2->year = '2012';
   print "Creating a new car";
   $car2->save();

输出:

Creating new car
saving model
creating model
created model
saved model

app/test1.php

print "Deleting a car that you just made";
$car2->delete();
 

输出:

Deleting that new car you just made
deleting model
deleted model

app/test1.php

print "Restoring that car you just deleted";
$car2->restore();

输出:

Restoring that car you just deleted
restoring model
saving model
updating model
updated model
saved model
restored model

使用观察者阻止更新

上面我们重现了这些开箱即用的事件模式。里面的每个方法都与 Eloquent model 事件相挂钩。前面我们说了在相应事件返回 false,则对应 model 将不会执行,这意味着我们就可以在这里做些事情,来阻止 save,create,delete,restore 等。来看一个例子,假设说所有的汽车 vin 码必须包含字母 h 才能保存,那么不包含字母 h 的模型就不会写入数据库。

app/test1.php

Car::observe(new Observers\VinObserver);
$car1 = Car::find(1);

// attempt #1 with no h
$car1->vin = "asdfasdfasdf";
$car1->save() && print "attempt #1 saved\n";
// attempt #2 contains h
$car1->vin = "hasdfasdfasdf";
$car1->save() && print "attempt #2 saved\n";

输出:

model vin does not contain letter 'h', canceling update...
attempt #2 saved

来看一下怎么实现
app/Observers/VinObserver.php

namespace App\Observers;
class VinObserver
{
    public function updating($model)
    {
        $origin = $model->getOriginal("vin");
        if ($model->vin === $original) {
            return true; // ignore unchanged vin
        }
        if (! str_contains($model->vin, 'h')) {
            print "model vin does not contain letter 'h', canceling updating vi \n";
            return false;
        }
    }
}

忽略所有未改变 vin 码的 model。并且不包含字母 h 的 vin 返回 false,这样就可以阻止更新。来看 laravel 底层是如何处理的。

vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php

/**
     * 执行模型更新操作
     *
     * @param  \Illuminate\Database\Eloquent\Builder  $query
     * @return bool
     */
    protected function performUpdate(Builder $query)
    {
        // 如果更新时间返回 false,我们将取消更新操作,以便开发人员可以将验证系统
        // 挂接到其模型中,并在模型未通过验证时取消此操作。否则,我们就更新
        if ($this->fireModelEvent('updating') === false) {
            return false;
        }

        // 首先,为了开发者方便,我们需要创建一个新的查询实例,并接触我们维护的
        // 模型上的创建和更新时间戳。然后继续保存模型实例。
      
        if ($this->timestamps) {
            $this->updateTimestamps();
        }

        // 一旦我们执行了更新操作,就将触发该模型实例的 update 事件。这将允许开发者
        // 有机会在模型更新后挂钩处理的事情,
     
        $dirty = $this->getDirty();

        if (count($dirty) > 0) {
            $this->setKeysForSaveQuery($query)->update($dirty);

            $this->fireModelEvent('updated', false);
        }

        return true;
    }

当执行更新操作时,其中一件事就是检查脏字段(dirty fields)。如果模型没有发生改变,就不要更新,接下来是 fireModelEvent 方法,作用就是触发模型事件,如果返回 false,就不执行更新操作,继续看看 fireModelEvent 方法内部。

vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php


 /**
     * 触发模型上给定的事件
     *
     * @param  string  $event
     * @param  bool  $halt
     * @return mixed
     */
    protected function fireModelEvent($event, $halt = true)
    {
        if (! isset(static::$dispatcher)) {
            return true;
        }

        // 我们把类的名称附加到事件中,以区别于被触发的其他模型事件,从而
        // 允许我们单独监听每个模型事件,而不是捕获所有模型的事件。
        
        $event = "eloquent.{$event}: ".static::class;

        $method = $halt ? 'until' : 'fire';

        return static::$dispatcher->$method($event, $this);
    }
    

这个方法内部调用了调度器(dispatcher)的 until 或者 fire 方法并返回结果。你附加到模型上的所有观察者都放在调度器中。这就是为什么你在这个 fireModelEvent 方法中没有看到观察者的内容。那么这个静态调度器到底是个什么东西呢? Eloquent 模型使用了共享的调度器,尤其是 $app['events'] 单例。该事件调度器是消息总线,它是 Illuminate\Events\Dispatcher 的一个实例。当应用程序启动数据服务提供者时,事件调度器将被注入到 Eloquent 模型中。

vendor/laravel/framework/src/Illuminate/Database/DatabaseServiceProvider.php

    /**
     * 启动应用程序事件
     *
     * @return void
     */
    public function boot()
    {
        Model::setConnectionResolver($this->app['db']);

        Model::setEventDispatcher($this->app['events']);
    }

这里,我们已经发现模型事件如何被触发以及如何阻止模型的更新操作。但是还有一点,假设所有注册的模型事件都被放在事件调度器中处理,你不知道内部如何处理的,接下来看一看观察者是如何附加到模型上的。

vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php

之前版本注册模型观察者等一系列方法放在 model.php,里,后面 laravel 将这部分功能以 trait 的方式重构了,并命名为 HasEvents。所以路径是
Illuminate\Database\Eloquent\Concerns\HasEvents.php

    /**
     * 注册模型的观察者
     *
     * @param  object|string  $class
     * @param  int  $priority
     * @return void
     */
    public static function observe($class, $priority = 0)
    {
        $instance = new static;

        $className = is_string($class) ? $class : get_class($class);

        // 当注册模型观察者时,我们将遍历可能的事件并确定此观察者是否具有该方法
        // 如果有,我们将把它挂钩到模型的事件系统中,以便监听。
        foreach ($instance->getObservableEvents() as $event) {
            if (method_exists($class, $event)) {
                static::registerModelEvent($event, $className.'@'.$event, $priority);
            }
        }
    }

当在模型上调用观察者方法时,它会遍历可能出现的事件,然后使用事件和类名称做参数调用 registerModelEvent 方法。getObservableEvents 方法返回一个字符串数组。字符串便是前面提及的那些事件(updating, updated, creating, created 等等),它还包括模型属性数组 $observables 里存放的其他任何可被观察(监听)的事件。根据源码得知,该方法具有以下参数:

static::registerModelEvent('updating', 'Observers\VinObserver@updating');

那么问题来了,registerModelEvent 方法究竟做了什么?

    /**
     * 使用调度器注册模型事件
     *
     * @param  string  $event
     * @param  \Closure|string  $callback
     * @param  int  $priority
     * @return void
     */
    protected static function registerModelEvent($event, $callback, $priority = 0)
    {
        if (isset(static::$dispatcher)) {
            $name = static::class;

            static::$dispatcher->listen("eloquent.{$event}: {$name}", $callback, $priority);
        }
    }

共享的调度器被告知去监听 App\Car 的更新事件,任何时候在调度器上触发该事件,对应回调也将触发。这就是观察者如何附加到模型上的。注意,每个模型都没有得到自己的观察者数组。这将会比附加观察者数组到每一个模型实例上使用更少的内存空间。这也意味着我们所有的 Car 模型都有相同的观察者,这是符合预期的。如果你就想给 Car 的其中一个实例创建观察者,而其他实例并不创建,那么就需要做一些不同的处理。现在,我们已经明白了观察者是如何附加到 Eloquent 模型上并被触发。

我们已经得知 laravel 提供的一些列标准事件可以帮助我们实现好多功能。然后。现实情况是复杂的,对应的 model 也是复杂的,你可能需要好多不同的标准事件以外的事件。比如你有一个不同状态的 content 模型,对应状态可能是草稿,待审核,已发布等。如果你想根据不同状态执行不能的 model 行为,那么你就需要自定义观察者附加到模型上,去监听对应事件。

前面已经提及到模型属性数组 $observable。这里就是存放我们自定义的事件。该数组允许你去监听其他事件。比如:

<?php

use Illuminate\Database\Eloquent\Model;

class MyContentModel extends Model
{
  /**
   * Additional observable events.
   */
  protected $observables = [
    'sentforreview',  // 即将发布进入待审核状态
    'rejected',       // 撤销,驳回,意味着内容可能因为审核失误而发布了,马上下架
  ];
}

本质上,它使用了 Eloquent 内部的 getObservableEvents 方法,前面已提及,该方法会合并自定义的事件到标准事件数组中。

Illuminate\Database\Eloquent\Concerns\HasEvents

    /**
     * 获取能监听到的事件名
     *
     * @return array
     */
    public function getObservableEvents()
    {
        return array_merge(
            [
                'creating', 'created', 'updating', 'updated',
                'deleting', 'deleted', 'saving', 'saved',
                'restoring', 'restored',
            ],
            $this->observables  // <--- merge in custom events
        );
    }

紧接着,生成对应观察者实现:

<?php

namespace App\Observers;

class MyContentObserver
{

  public function sentforreview(MyContent $myContent)
  {
    // Your logic here.
  }

  public function rejected(MyContent $myContent)
  {
    // Your logic here.
  }

}

这一步我们就完成了自义定的事件等待触发了,那么什么时候触发,怎么触发?

<?php

use Illuminate\Database\Eloquent\Model;

class MyContentModel extends Model
{
  public function sendForReview()
  {
     // 对模型作相应的处理然后触发事件
     $this->fireModelEvent('sentforreview', false);
  }

  public function reject()
  {
     // 对模型作相应的处理然后触发事件
     $this->fireModelEvent('rejected', false);
  }
}

到这里我们就拥有了一个单一的观察者来处理自定义事件。在最新版本 5.4 里,laravel 提供了一种 map 方式,允许我们将 Eloquent 模型的生命周期的多个事件映射到专门的事件系统中的事件类(EventServiceProvider),关于 laravel 的事件系统,请参考文档。这将使我们的 model 更轻量。基本示例:

<?php

namespace App;

use App\Events\UserSaved;
use App\Events\UserDeleted;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    use Notifiable;

    /**
     * 模型事件映射。
     *
     * @var array
     */
    protected $events = [
        'saved' => UserSaved::class,
        'deleted' => UserDeleted::class,
    ];
}


关于这个特性,请参考
https://laracasts.com/series/whats-new-in-laravel-5-4/episodes/10

总结

事件驱动架构(Event-driven architecture)是一种围绕应用程序状态而设计的软件架构模式。观察者模式可用于这种类型的软件架构设计。 还有其他类似的模式。比如:

  • 中介者模式(mediator pattern)
  • 命令总线模式(command bus pattern)
  • 订阅发布模式((subscribe/publish pattern)laravel 内置的事件系统就是用的这种模式

说实话,这些内在区别有啥不同,我也搞不清,总之别太迷恋设计模式。。。。

参考书籍:《Design Patterns in PHP and Laravel》

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容

  • Observer 模式应该可以说是应用最多、影响最广的设计模式之一,在系统开发架构设计中有着很重要的地位和意义。O...
    wild_horse阅读 639评论 0 0
  • Eloquent: 起步 简介 Laravel 的 Eloquent ORM 提供了一种漂亮简洁的关系映射的模型来...
    Dearmadman阅读 11,892评论 3 16
  • 今天中午下了一场暴雨,听着雨声,电台声进入了沉沉的梦乡。梦乡啊! 这真是我这大概七年来的第一次做梦,之所以记得这么...
    沿海地带阅读 665评论 0 3
  • (看着老田笑着递过来的满盈盈的一杯啤酒,侯勇觉得他脸上的美人痣今天格外夺目) 周一,日常下井,这次还是老地方。本想...
    翱翔的窝窝头阅读 345评论 0 0
  • 人类一思考,上帝就发笑。比如,人为什么活着?这是个哲学问题。有些人会说,提出这个问题的人就是吃饱了撑的,瞎扯...
    一叶一沙阅读 243评论 0 2