19

我看到很多人说 Symfony2、Zend Framework 2 等都是事件驱动的。

在桌面世界中,通过事件驱动编程,我了解到应用程序将在其状态发生变化时通知其观察者。

由于 PHP 应用程序是无状态的,因此无法做到这一点。IE 在用户使用界面时让观察者绑定到视图观察变化。相反,它需要一个新的请求进程来更新视图。所以,这不是一个事件,而是一个全新的请求

另一方面有一个类似的概念:事件驱动架构。

在这里你可以阅读:

http://en.wikipedia.org/wiki/Event-driven_programming

http://en.wikipedia.org/wiki/Event-driven_architecture

这里是另一个:

http://en.wikipedia.org/wiki/Signal_programming

信号是对发生事件的进程的通知。信号有时被描述为软件中断。信号类似于硬件中断,因为它们会中断程序的正常执行流程。在大多数情况下,无法准确预测信号何时到达。

  • Stackoverflow [singals] 标签说明

而且,我以前所说的事件驱动,似乎和Qt引入的Signals and Slots Pattern(观察者模式实现)有更多的关系

例如,普拉多框架声称是事件驱动的:

http://www.pradosoft.com/demos/quickstart/?page=Fundamentals.Applications(应用程序生命周期部分)

http://www.pradosoft.com/docs/manual/System/TApplication.html#methodonEndRequest

IIRC,这不是一个事件驱动的应用程序,而只是实现observable Interface. 我的意思是,考虑到桌面应用程序使用事件的方式和无状态应用程序使用事件的方式(作为插件):第一次使用整个应用程序的事件,包括视图,最后一次仅用于服务器端操作。

一种与面向方面的编程(带有信号和槽)更相关,另一种与横切关注点/AOP 无关。换句话说,它与应用程序状态更相关。

那么,这些术语之间的关系实际上是什么以及它们之间的区别是什么?

  1. 事件驱动编程
  2. 事件驱动架构
  3. 信号和槽模式

这些术语只是通用模式吗?因此,所有实现观察者模式的东西都可以被认为是事件驱动的吗?

更新

Zend 框架 2

我在上面链接的关于 AOP 的文章(http://mwop.net/blog/251-Aspects,-Filters,-and-Signals,-Oh,-My!.html)由 Matthew Weier O'Phinney 撰写(采埃孚领导者)。IIRC,它没有提到“事件驱动”,只是信号和插槽。

Symfony 2

Symfony2EventDispatcher组件描述没有提到“事件驱动”应用程序: http ://symfony.com/doc/current/components/event_dispatcher/introduction.html 它只包含对“事件”的引用(确实,由信号和插槽处理)。

两个框架似乎都在 Signal 和 Slots 中使用 Intercepting Filter Pattern 来处理请求过程中的同步事件。

4

2 回答 2

16

免责声明:这是一个很长的答案,但我认为它的所有参考资料都值得一读。恕我直言,它导致了一个确凿的答案。

过去几天我一直在努力解决这个问题,如果我没看错的话,答案是:

事件驱动!== 请求驱动

“[...] 我发现这是事件协作中最有趣的区别,套用 Jon Udell 的话说:请求驱动的软件在说话时说话,事件驱动的软件在有话要说时说话。

这样做的结果是管理状态的责任发生了变化。在请求协作中,您努力确保每条数据都有一个归宿,如果需要,您可以从该归宿进行查找。这个家负责数据的结构,存储多长时间,如何访问它。在事件协作场景中,欢迎新数据源在传递到其消息端点的第二秒忘记数据。”

Martin Fowler -活动协作(查询部分)

基于该断言,IIRC,现代 PHP 框架实现了观察者模式 + 拦截过滤器 + 信号和插槽,以便在请求周期中触发一些事件。

但是,尽管它采用了一些事件驱动架构的思想,但似乎并不支持整个框架是事件驱动的(即 Symfony2 是一个事件驱动的框架)。

我们习惯于将程序分成多个协作的组件。(我在这里故意使用模糊的“组件”一词,因为在这种情况下,我的意思很多:包括程序中的对象和通过网络进行通信的多个进程。)使它们协作的最常见方式是请求/响应样式. 如果客户对象想要来自销售员对象的一些数据,它会调用销售员对象上的一个方法来向它询问该数据。

另一种协作方式是事件协作。在这种风格 中,您永远不会有一个组件要求另一个组件做任何事情,而是当任何事情发生变化时,每个组件都会发出一个事件。其他组件侦听该事件并根据他们的意愿做出反应。著名的观察者模式是事件协作的一个例子。

Martin Fowler -关注事件(部分:使用事件进行协作)

我认为 PHP 应用程序更接近于事件驱动而不是请求驱动,只有当焦点放在事件上时。如果这些应用程序/框架仅将事件用于横切关注点 (AOP),那么它就不是事件驱动的。同样,您不会仅仅因为您有一些域对象和单元测试而将其称为测试驱动或域驱动。

现实世界的例子

我选择了一些例子来说明为什么这些框架不是完全事件驱动的。尽管有 AOP 事件,但一切都是请求驱动的

注意:虽然,它可以适应事件驱动

Zend 框架 2

让我们检查一下\Zend\Mvc\Application组件:

它实现了\Zend\EventManager\EventManagerAwareInterface并依赖于描述可能事件的\Zend\Mvc\MvcEvent :

class MvcEvent extends Event
{
    /**#@+
     * Mvc events triggered by eventmanager
     */
    const EVENT_BOOTSTRAP      = 'bootstrap';
    const EVENT_DISPATCH       = 'dispatch';
    const EVENT_DISPATCH_ERROR = 'dispatch.error';
    const EVENT_FINISH         = 'finish';
    const EVENT_RENDER         = 'render';
    const EVENT_ROUTE          = 'route';

    // [...]
}

\Zend\Mvc\Application组件本身是事件驱动的,因为它不直接与其他组件通信,而是仅触发事件:

/**
 * Run the application
 *
 * @triggers route(MvcEvent)
 *           Routes the request, and sets the RouteMatch object in the event.
 * @triggers dispatch(MvcEvent)
 *           Dispatches a request, using the discovered RouteMatch and
 *           provided request.
 * @triggers dispatch.error(MvcEvent)
 *           On errors (controller not found, action not supported, etc.),
 *           populates the event with information about the error type,
 *           discovered controller, and controller class (if known).
 *           Typically, a handler should return a populated Response object
 *           that can be returned immediately.
 * @return ResponseInterface
 */
public function run()
{
    $events = $this->getEventManager();
    $event  = $this->getMvcEvent();

    // Define callback used to determine whether or not to short-circuit
    $shortCircuit = function ($r) use ($event) {
        if ($r instanceof ResponseInterface) {
            return true;
        }
        if ($event->getError()) {
            return true;
        }
        return false;
    };

    // Trigger route event
    $result = $events->trigger(MvcEvent::EVENT_ROUTE, $event, $shortCircuit);
    if ($result->stopped()) {
        $response = $result->last();
        if ($response instanceof ResponseInterface) {
            $event->setTarget($this);
            $events->trigger(MvcEvent::EVENT_FINISH, $event);
            return $response;
        }
        if ($event->getError()) {
            return $this->completeRequest($event);
        }
        return $event->getResponse();
    }
    if ($event->getError()) {
        return $this->completeRequest($event);
    }

    // Trigger dispatch event
    $result = $events->trigger(MvcEvent::EVENT_DISPATCH, $event, $shortCircuit);

    // Complete response
    $response = $result->last();
    if ($response instanceof ResponseInterface) {
        $event->setTarget($this);
        $events->trigger(MvcEvent::EVENT_FINISH, $event);
        return $response;
    }

    $response = $this->getResponse();
    $event->setResponse($response);

    return $this->completeRequest($event);
}

那是事件驱动的:你不知道将使用哪个路由器、调度程序和视图渲染器的代码,你只知道这些事件将被触发。您可以挂钩几乎任何兼容的组件来侦听和处理事件。组件之间没有直接的通信。

但是,需要注意一件重要的事情:这是表示层(控制器 + 视图)。领域层确实可以是事件驱动的,但并非您看到的几乎所有应用程序都是如此。**事件驱动和请求驱动混合在一起:

// albums controller
public function indexAction()
{
    return new ViewModel(array(
        'albums' => $this->albumsService->getAlbumsFromArtist('Joy Division'),
    ));
}

控制器组件不是事件驱动的。它直接与服务组件通信。相反,服务应该订阅由控制器引发的事件,控制器是表示层的一部分。(我将在此答案的末尾指出有关什么是事件驱动的域模型的参考资料)。

Symfony 2

现在让我们在 Symfony2 Application/FrontController 上检查相同的内容:\Symfony\Component\HttpKernel\HttpKernel

它确实有请求期间的主要事件:Symfony\Component\HttpKernel\KernelEvents

/**
 * Handles a request to convert it to a response.
 *
 * Exceptions are not caught.
 *
 * @param Request $request A Request instance
 * @param integer $type    The type of the request (one of HttpKernelInterface::MASTER_REQUEST or HttpKernelInterface::SUB_REQUEST)
 *
 * @return Response A Response instance
 *
 * @throws \LogicException If one of the listener does not behave as expected
 * @throws NotFoundHttpException When controller cannot be found
 */
private function handleRaw(Request $request, $type = self::MASTER_REQUEST)
{
    // request
    $event = new GetResponseEvent($this, $request, $type);
    $this->dispatcher->dispatch(KernelEvents::REQUEST, $event);

    if ($event->hasResponse()) {
        return $this->filterResponse($event->getResponse(), $request, $type);
    }

    // load controller
    if (false === $controller = $this->resolver->getController($request)) {
        throw new NotFoundHttpException(sprintf('Unable to find the controller for path "%s". Maybe you forgot to add the matching route in your routing configuration?', $request->getPathInfo()));
    }

    $event = new FilterControllerEvent($this, $controller, $request, $type);
    $this->dispatcher->dispatch(KernelEvents::CONTROLLER, $event);
    $controller = $event->getController();

    // controller arguments
    $arguments = $this->resolver->getArguments($request, $controller);

    // call controller
    $response = call_user_func_array($controller, $arguments);

    // view
    if (!$response instanceof Response) {
        $event = new GetResponseForControllerResultEvent($this, $request, $type, $response);
        $this->dispatcher->dispatch(KernelEvents::VIEW, $event);

        if ($event->hasResponse()) {
            $response = $event->getResponse();
        }

        if (!$response instanceof Response) {
            $msg = sprintf('The controller must return a response (%s given).', $this->varToString($response));

            // the user may have forgotten to return something
            if (null === $response) {
                $msg .= ' Did you forget to add a return statement somewhere in your controller?';
            }
            throw new \LogicException($msg);
        }
    }

    return $this->filterResponse($response, $request, $type);
}

但是除了“事件能力”之外,它还直接与 ControllerResolver 组件通信,因此从请求过程开始它就不是完全事件驱动的,尽管它会触发一些事件并允许一些组件是可插入的(事实并非如此)作为构造函数参数注入的 ControllerResolver )。

相反,要成为一个完全事件驱动的组件,它应该像 ZF2 应用程序组件一样:

    // Trigger dispatch event
    $result = $events->trigger(MvcEvent::EVENT_DISPATCH, $event, $shortCircuit);

普拉多

我没有足够的时间研究源代码,但起初它似乎并没有以SOLID方式构建。无论哪种方式,什么是类似 MVC 的框架上的控制器,Prado 将其称为 TPage(尚不确定):

http://www.pradosoft.com/demos/blog-tutorial/?page=Day3.CreateNewUser

它确实直接与组件通信:

class NewUser extends TPage
{
    /**
     * Checks whether the username exists in the database.
     * This method responds to the OnServerValidate event of username's custom validator.
     * @param mixed event sender
     * @param mixed event parameter
     */
    public function checkUsername($sender,$param)
    {
        // valid if the username is not found in the database
        $param->IsValid=UserRecord::finder()->findByPk($this->Username->Text)===null;
    }
    [...]
}

我知道这TPage是一个事件监听器,可以插入。但它不会使您的域模型成为事件驱动的。所以我认为,在某种程度上,它更接近于 ZF2 的提议。

事件驱动的例子

为了完成这个冗长的答案,下面是一个成熟的事件驱动应用程序应该具备的:

使用域事件解耦应用程序 http://www.whitewashing.de/2012/08/25/decoupling_applications_with_domain_events.html

事件采购 http://martinfowler.com/eaaDev/EventSourcing.html

领域事件模式 http://martinfowler.com/eaaDev/DomainEvent.html

活动协作 http://martinfowler.com/eaaDev/EventCollaboration.html

事件拦截 http://martinfowler.com/bliki/EventInterception.html

消息端点 http://www.enterpriseintegrationpatterns.com/MessageEndpoint.html

... 等等

于 2012-09-02T23:55:10.537 回答
9

PHP 不是无状态的,HTTP 是。简单地说,我们基本上在无状态技术之上构建了一个层,我们可以在该层上实现有状态设计。总而言之,PHP 和您选择的数据存储拥有所需的所有工具,可以通过会话的标记化构建基于事件驱动模式的应用程序设计。

以一种高度概括的方式,您可以将 HTTP 视为用于 Web 的,就像 BIOS 用于桌面计算一样。事实上,只要稍微深入一点,您就可以很容易地看到 Web 隐含的事件驱动性质。你说“这不是一个事件,而是一个全新的请求”,我回过头来说,“一个全新的请求一个事件”,我的意思是在设计模式的意义上。它具有与用户与应用程序交互相关的具体语义含义。

本质上,通过 MVC 和 Front Controller 等模式(以及通过 HTTP cookie 和 PHP 会话的机制),我们只需恢复会话状态,然后响应事件,相应地修改该状态。

我喜欢思考 REST 的本质:具象状态转移……但我要补充一点,我们不应该忘记仅在 UI 事件发生时才转移状态的隐含含义。因此,我们维护与 HTTP 签订的合同,我们仅在模型的“表示状态”(即文档、JSON 等)中“说话”,但这只是我们的方言。其他系统选择在画布坐标、信号数据库等中说话。

编辑/更多想法

所以我一直在思考它,我认为有一个概念可以说明在通过 HTTP 讨论 PHP 领域中的这些模式时有点模棱两可:确定性。具体来说,一旦接收到请求,PHP 执行的路径是确定的,这正是为什么在 PHP 中考虑“事件驱动”架构非常困难的原因。我的想法是,我们应该考虑比 PHP 更高的一个级别,即与用户交互的更大“会话”。

在桌面计算中,我们使用运行循环和有状态的上下文来“等待”事件。但是,我会争辩说,网络实际上是对这种架构的改进(在大多数情况下),但最终是相同的模式当事件发生时,我们引导我们的状态,然后处理该事件,而不是运行循环和无限持续时间的状态。我们不只是将该状态保存在内存中并等待下一个事件,而是存档该状态并关闭资源。在某种意义上它可以被认为效率较低(我们需要在每个“事件”处加载状态),但它也可以被称为更高效,因为内存中永远不会无缘无故地空闲状态。我们只加载实际被消费/操作的状态

因此,通过这种方式,将通过 HTTP 的 PHP 视为宏观层面的事件驱动,同时确保任何给定的执行确实是确定性的,而不是真正的事件驱动。但是,我们实现了前端控制器和 MVC 模式,以便我们可以为应用程序开发人员提供熟悉的偶数驱动挂钩结构。当您使用一个体面的框架时,您只需说“我想知道用户何时注册,并且该用户应该可供我修改”。这是事件驱动的开发。你不应该担心框架已经引导环境(几乎)唯一的目的是调用你的钩子(相对于更传统的概念,即环境已经存在并且你只是通知事件)。这就是以事件驱动的方式开发 PHP 的意义所在。控制器确定(基于请求)哪个事件正在发生,并使用它设计使用的任何机制(即观察者模式、钩子架构等)来允许您的代码处理事件、响应事件或任何命名法最适合您的特定框架的语义。

于 2012-09-02T05:11:31.443 回答