PHP: Am I mixing up event-driven programming with signals-aware interfaces (Signal and Slots / Observer Pattern)?

后端 未结 2 1095
遥遥无期
遥遥无期 2021-01-30 11:50

I\'ve seen a lot of people saying that Symfony2, Zend Framework 2 and others are event-driven.

On the desktop world, by event-driven programming I understand that the a

2条回答
  •  醉梦人生
    2021-01-30 12:18

    Disclaimer: It's a long answer, but I think it's worth the read with all its references. And IMHO it leads to a conclusive answer.

    I've been struggling upon this subjects the last couple of days and, if I've read all correctly, the answer is:

    Event-driven !== Request-driven

    "[...] I find this the most interesting difference in event collaboration, to paraphrase Jon Udell: request driven software speaks when spoken to, event driven software speaks when it has something to say.

    A consequence of this is that the responsibility of managing state shifts. In request collaboration you strive to ensure that every piece of data has one home, and you look it up from that home if you want it. This home is responsible for the structure of data, how long it's stored, how to access it. In the event collaboration scenario the source of new data is welcome to forget the data the second it's passed to its Message Endpoint."

    Martin Fowler - Event Collaboration (Queries section)

    Based on that assertion, IIRC, modern PHP frameworks implements the Observer Pattern + Intercepting Filters + Singal and Slots, in order to trigger some events during the request cycle.

    But, despite the fact that it adopts some ideas of event-driven architectures, it doesn't seems to support that the whole framework is event-driven (i.e. Symfony2 is an event-driven framewrok).

    We are used to dividing programs into multiple components that collaborate together. (I'm using the vague 'component' word here deliberately since in this context I mean many things: including objects within a program and multiple processes communicating across a network.) The most common way of making them collaborate is a request/response style. If a customer object wants some data from a salesman object, it invokes a method on the salesman object to ask it for that data.

    Another style of collaboration is Event Collaboration. In this style you never have one component asking another to do anything, instead each component signals an event when anything changes. Other components listen to that event and react however they wish to. The well-known observer pattern is an example of Event Collaboration.

    Martin Fowler - Focus on events (section: Using events to collaborate)

    I think that PHP applications are more closely to be event-driven than request-driven only when the focus are on events. If those applications/frameworks are only using events for cross-cutting concerns (AOP), then it's not event-driven. In the same way you would not call it test-driven or domain-driven just because you have some domain objects and unit tests.

    Real world examples

    I've picked some examples to show why those frameworks are not fully event-driven. Despite AOP events, everything is request-driven:

    Note: Although, it can be adapted to be event-driven

    Zend Framework 2

    Let's examine the \Zend\Mvc\Application component:

    It implements the \Zend\EventManager\EventManagerAwareInterface and relies on the \Zend\Mvc\MvcEvent which describes the possible events:

    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';
    
        // [...]
    }
    

    The \Zend\Mvc\Application component itself is event-driven because it doesn't communicates directly with the other components, but instead, it just triggers events:

    /**
     * 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);
    }
    

    That's event-driven: You have no clue looking at the code of which router, dispatcher and view renderer will be used, you only know that those events will be triggered. You could hook almost any compatible component to listen and process the events. There's no direct communication between components.

    But, there's one important thing to note: This is the presentation layer (Controller + View). The domain layer indeed can be event-driven, but that's not the case of almost all applications you see out there. **There's a mix between event-driven and request-driven:

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

    The controller component is not event-driven. It communicates directly with the service component. Instead the services should subscribe to events risen by controllers, which is part of the presentation layer. (I'll point out the references about what would be an event-driven domain model at the end of this answer).

    Symfony 2

    Now let's examine the same on the Symfony2 Application/FrontController: \Symfony\Component\HttpKernel\HttpKernel

    It indeed has the main events during the request: 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);
    }
    

    But besides of being "event-capable" it communicates directly with the ControllerResolver component, hence it's not fully event-driven since the beginning of the request process, though it triggers some events and allows some components to be pluggable (which is not the case of the ControllerResolver that's injected as a constructor parameter).

    Instead, to be a fully event-driven component it should be as in ZF2 Application component:

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

    Prado

    I haven't enough time to investigate the source code, but at first it doesn't seems to be built in a SOLID way. Either way, what's a controller on MVC-alike frameworks, Prado calls it a TPage (Not sure yet):

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

    And it indeed communicates directly with components:

    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;
        }
        [...]
    }
    

    I understand that the TPage is an event listener and can be pluggable. But it doesn't make your domain model event-driven. So I think that, in some extent, it's more close to the ZF2 proposal.

    Event-driven examples

    To finish this long answer, here's what a full-blown event-driven application would have:

    Decoupling applications with Domains Events http://www.whitewashing.de/2012/08/25/decoupling_applications_with_domain_events.html

    Event sourcing http://martinfowler.com/eaaDev/EventSourcing.html

    Domain Event Pattern http://martinfowler.com/eaaDev/DomainEvent.html

    Event Collaboration http://martinfowler.com/eaaDev/EventCollaboration.html

    Event Interception http://martinfowler.com/bliki/EventInterception.html

    Message Endpoint http://www.enterpriseintegrationpatterns.com/MessageEndpoint.html

    ... and so on

提交回复
热议问题