Permalink
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
521 lines (427 sloc) 13.8 KB

Refactoring From MVC to ADR

Starting With MVC

Directory Structure

The MVC directory structure for a naive blogging system might look like the following:

controllers/
    BlogController.php
models/
    BlogModel.php
views/
    blog/
        index.php
        create.php
        read.php
        update.php
        delete.php
        _comments.php

Some notes:

  • This system uses an ActiveRecord implementation for its models, even though ActiveRecord is classified primarily as a data source architecture pattern and only secondarily as a domain logic pattern. This is a common enough pattern, so we won't present code for it here.

  • The views are a series of PHP-based templates. This pattern is also common enough not to warrant explicit code presentation.

MVC Logic

The MVC BlogController class might look something like this:

<?php
class BlogController
{
    public function __construct(
        Request $request,
        Response $response,
        TemplateView $view,
        BlogModel $model
    ) {
        // ...
    }

    public function index()
    {
        // ...
    }

    public function create()
    {
        // is this a POST request?
        if ($this->request->isPost()) {

            // retain incoming data
            $data = $this->request->getPost('blog');

            // create a blog post instance
            $blog = $this->model->newInstance($data);

            // is the new instance valid?
            if ($blog->isValid()) {
                // yes, insert and redirect to editing
                $blog->save();
                $this->response->setHeader('Location', "/blog/edit/{$blog->id}");
            } else {
                // no, show the "create" form with the blog record
                $html = $this->view->render(
                    'create.php',
                    ['blog' => $blog],
                );
                $this->response->setContent($html);
            }
        } else {
            // not a POST request, show the "create" form with a new record
            $html = $this->view->render(
                'create.php',
                ['blog' => $this->model->newInstance()]
            );
            $this->response->setContent($html);
        }

        return $this->response;
    }

    public function read()
    {
        // ...
    }

    public function update()
    {
        // ...
    }

    public function delete()
    {
        // ...
    }
}

Some notes:

  • The Controller contains multiple action methods.

  • The Controller sets the response headers directly, even though it hands off content-building control to a Template View. Since the entire HTTP response is being presented, setting headers in the Controller represents a failure to separate presentation concerns properly.

  • The Controller performs business logic on the Model, rather than handing off the business logic to a domain layer. This represents a failure to accurately separate domain concerns.

Refactoring to ADR

Directory Structure

In comparison, an ADR directory structure refactored from the above MVC system might look like this:

resources/
    templates/
        blog/
            index.php
            create.php
            read.php
            update.php
            delete.php
            _comments.php
src/
    Domain/
        Blog/
            BlogModel.php
    Ui/
        Web/
            Blog/
                Index/
                    BlogIndexAction.php
                    BlogIndexResponder.php
                Create/
                    BlogCreateAction.php
                    BlogCreateResponder.php
                Read/
                    BlogReadAction.php
                    BlogReadResponder.php
                Update/
                    BlogUpdateAction.php
                    BlogUpdateResponder.php
                Delete/
                    BlogDeleteAction.php
                    BlogDeleteResponder.php

Some notes:

  • We have extracted each action method from the BlogController to its own Action class in a namespace dedicated to a web user interface.

  • Each Action has a corresponding Responder, into which all presentation work (i.e., response-building work) has been placed.

  • We have renamed views/ to templates/ and moved it to a different location.

  • While we might prefer to replace the ActiveRecord BlogModel class with a data mapper (BlogMapper) which returns persistence model objects (BlogRecord), that is beyond the scope of this exercise. We will leave the ActiveRecord implementation as it is, though we have moved it into a namespace dedicated to the domain layer.

ADR Logic

The refactoring goals are to:

  • separate presentation (response-building) logic from all other logic;
  • separate domain logic from all other logic;
  • remove all conditional logic from Actions (except ternaries for default input values).

These goals complement each other and feed back on each other; they might or might not be achieved in isolation.

The following is one possible order of refactorings. Another set of changes, or a similar set but in a different order, might achieve the same goals.

Separate Presentation

An initial refactoring of the above BlogController create() method to an Action and Responder pair might look like this:

<?php
class BlogCreateAction
{
    public function __construct(
        Request $request,
        BlogCreateResponder $responder,
        BlogModel $model
    ) {
        // ...
    }

    public function __invoke()
    {
        // is this a POST request?
        if ($this->request->isPost()) {

            // yes, retain incoming data
            $data = $this->request->getPost('blog');

            // create a blog post instance
            $blog = $this->model->newInstance($data);

            // is the new instance valid?
            if ($blog->isValid()) {
                // yes, insert it
                $blog->save();
            }

        } else {
            // not a POST request
            $blog = $this->model->newInstance();
        }

        // use the responder to build a response
        return $this->responder->response($blog);
    }
}
<?php
class BlogCreateResponder
{
    public function __construct(
        Response $response,
        TemplateView $view
    ) {
        // ...
    }

    public function response(BlogModel $blog)
    {
        // is there an ID on the blog instance?
        if ($blog->id) {
            // yes, which means it was saved already.
            // redirect to editing.
            $this->response->setHeader('Location', '/blog/edit/{$blog->id}');
        } else {
            // no, which means it has not been saved yet.
            // show the creation form with the current data.
            $html = $this->view->render(
                'create.php',
                ['blog' => $blog]
            );
            $this->response->setContent($html);
        }

        return $this->response;
    }
}

Note that we use the PHP magic method __invoke() as the main method for action invocation; this could be any other method name we wanted to standardize on.

At this point, we have successfully separated all presentation (response-building) work to the Responder.

Separate Domain Logic

The BlogCreateAction is still performing some business logic. We can refactor it to create a domain-layer BlogService to handle it for us.

<?php
class BlogService
{
    public function __construct(BlogModel $model)
    {
        // ...
    }

    public function newInstance()
    {
        return $this->model->newInstance();
    }

    public function create(array $data)
    {
        $blog = $this->model->newInstance($data);

        if ($blog->isValid()) {
            $blog->save();
        }

        return $blog;
    }
}
<?php
class BlogCreateAction
{
    public function __construct(
        Request $request,
        BlogCreateResponder $responder,
        BlogService $domain
    ) {
        // ...
    }

    public function __invoke()
    {
        if ($this->request->isPost()) {
            $data = $this->request->getPost('blog');
            $blog = $this->domain->create($data);
        } else {
            $blog = $this->domain->newInstance();
        }

        return $this->responder->response($blog);
    }
}

Note that a BlogService is now being injected into the BlogCreateAction as $domain, instead of a BlogModel as $model.

At this point, all presentation (response-building) work is being handled in Responder code, and all business logic is being handled in Domain code.

Remove Action Conditionals

The remaining logic in the BlogCreateAction proceeds along two different paths:

  • one, to present the form for adding a new blog entry;
  • and another, to save the new blog entry.

We can extract one of the paths to a BlogAddAction, like so:

<?php
class BlogAddAction
{
    public function __construct(
        Request $request,
        BlogCreateResponder $responder,
        BlogService $domain
    ) {
        // ...
    }

    public function __invoke()
    {
        $blog = $this->domain->newInstance();
        return $this->responder->response($blog);
    }
}

class BlogCreateAction
{
    public function __construct(
        Request $request,
        BlogCreateResponder $responder,
        BlogService $domain
    ) {
        // ...
    }

    public function __invoke()
    {
        $data = $this->request->getPost('blog');
        $blog = $this->domain->create($data);
        return $this->responder->response($blog);
    }
}

Some notes:

  • Both actions continue to use the same Responder and Domain classes.

  • We need to modify the web handler (probably a router) to dispatch to one BlogAddAction on GET, and BlogCreateAction on POST.

At this point we have fulfilled the ADR pattern of components and collaborations:

  • each Action only does three things: it collects input, calls the domain, then calls the responder;

  • the Domain code handles all business logic;

  • the Responder code handles all presentation logic.

Introducing a Domain Payload

N.b.: A Domain Payload can be complementary to ADR, but is not a required component of the pattern.

Currently, the Responder still has to inspect the Domain results to figure out how to present those results. However, the Domain already knows what the results mean. The Responder should not have to do extra work to divine the meaning of the Domain results; instead, the Domain should communicate that status explicitly.

To do so, the Domain can return its results wrapped in a Payload object along with a status value. The Responder can then work with the Payload instead of picking apart domain object values to find meaning.

First, we introduce a Payload:

<?php
class Payload
{
    public function __construct($status, array $result = [])
    {
        $this->status = $status;
        $this->result = $result;
    }

    public function getStatus()
    {
        return $this->status;
    }

    public function getResult()
    {
        return $this->result;
    }
}

Then, we modify the BlogService to return a Payload instead of unwrapped domain objects:

class BlogService
{
    public function __construct(BlogModel $model)
    {
        // ...
    }

    public function newInstance()
    {
        return new Payload(
            'NEW',
            ['blog' => $this->model->newInstance()]
        );
    }

    public function create(array $data)
    {
        $blog = $this->model->newInstance($data);

        if (! $blog->isValid()) {
            return new Payload('INVALID', [$blog => 'blog']);
        }

        $blog->save();
        return new Payload('SAVED', [$blog => 'blog']);
    }
}

Each Action calling the service now receives a Payload in return, and passes that Payload to the Responder:

<?php
class BlogAddAction
{
    public function __construct(
        Request $request,
        BlogCreateResponder $responder,
        BlogService $domain
    ) {
        // ...
    }

    public function __invoke()
    {
        $payload = $this->domain->newInstance();
        return $this->responder->response($payload);
    }
}
<?php
class BlogCreateAction
{
    public function __construct(
        Request $request,
        BlogCreateResponder $responder,
        BlogService $domain
    ) {
        // ...
    }

    public function __invoke()
    {
        $data = $this->request->getPost('blog');
        $payload = $this->domain->create($data);
        return $this->responder->response($payload);
    }
}

And finally, the Responder can use the Payload status to determine how to present the results:

<?php
class BlogCreateResponder
{
    public function __construct(
        Response $response,
        TemplateView $view
    ) {
        // ...
    }

    public function response(Payload $payload)
    {
        $blog = $payload->getResult()['blog'];

        switch ($payload->getStatus()) {
            case 'SAVED':
                $this->response->setHeader('Location', "/blog/edit/{$blog->id}")
                break;
            case 'INVALID':
            case 'NEW':
                $html = $this->view->render(
                    'create.php',
                    ['blog' => $blog]
                );
                $this->response->setContent($html);
                break;
            case default:
                $this->response->setStatus(500, 'Unknown Payload Status')
        }

        return $this->response;
    }
}

By using a Domain Payload, we can avoid having to inspect domain objects directly to determine how to present them, and instead check the status the domain passed back explicitly. This makes presentation logic easier to read and follow.