TITLE
Catalyst Design Patterns: $c->forward versus Context Aware Model versus...?
SUMMARY
Musing about various ways to isolate and centralize application level logic in Catalyst
DETAILS
Catalyst promotes seperatation of concerns by making it easy to isolate your domain business logic, view logic and controller or 'glue' logic. To achieve this we often say that your controllers should be 'thin', by which I mean limited to the least amount of interface code between incoming HTTP requests and overall application context and your business domain logic and view. Examples of this interface code would include things like getting form post parameters and sending them to a model (Glue or proxy logic), validating said parameters (application level logic, or the minimal additional logic needed to make your business domain logic useful in a given application context) and sending model data to a view.
In practice we often find it difficult to figure out the best place to put certain types of application logic, which often don't make sense as methods in your domain model, and we find much redundency in our interfacing logic. Here's some approaches that I've seen taken, followed by my thoughts on the various pros and cons of the approach.
<Using <$c-
forward|detach|go|visit>.>> This common approach has at first glance a lot to offer since it achieves two goals, one it lets you centralize and reuse controller level logic and two said logic is context aware since actions that you forward two automatically get the current context. For example (assume for now that you are using Catalyst::Action::RenderView in a central end action, such as on your Root.pm controller>:
sub user : Local Args(1) {
my ($self, $ctx, $id) = @_;
my $user = $ctx->model('Schema::User')->find($id)) ||
$ctx->detach('user_not_found');
$ctx->forward('set_user_tracking');
$ctx->forward('send_returning_member_welcome_email');
}
In practice I find this approach tends to lead to messy and disorganized code particularly when stash stored state is involved (quite often you will find it hard to keep track of exactly what is in the stash at any given point). Also it suffers from the general problem that all Catalyst actions tend toward, in that actions in Catalyst are highly powered and have total access to the full application state, which leads to poor structural binding (code that is tightly bound to other code in fragile ways, unlike the loose coupling you should wish to see in glue logic). Both the fact that context is available the the fact you can forward to actions from forwarded actions tend to get messy.
One other issue here is that forward|go|detach|visit doesn't do as you might expect when used on a set of chained actions, since only the terminal action is called, not the entire chain. As a result I strongly suggest you restrict your use of this technique to private actions (if I had my way I'd make calling a non private action this way an error in core Catalyst). If someone has a rational use case for this on non private actions please let me know.
A smaller problem with the approach is that it is a Catalyst specific technique (forwarding to an action just seems strange when you first come to the frameword since it doesn't build on any existing programming paradigm that most Perl programmers are likely to know about). As a result it tends to be poorly understood and poorly used.
Lastly this approach tends to lead to code that is not well isolated and easy to test correctly. Typically you need a fully mocked context and a lot of other moving parts to even try.
That said, the approach when used carefully has the merit of allowing you to keep all your context requiring logic at the controller level. If you only use them for private actions, avoid stash inflation, keep each action function tightly scope, and avoid multiple forwards (or create a single controller action that does all the forwarding and routing) it can be a sane way to centralize and control some types of repeated controller level logic, particularly 'utility' logic, or odd bits that don't really neatly fit into a sanely designed model.
Creating Models that are context aware. We often say its a code smell when your models require the application context to work properly. However when creating logic that is scoped careful to assisting with interface or overall application level logic, allowing a model to accept context can be a good approach.
For example, it could make sense for logic around parameter validation to be able to directly read the body parameters from your context request, such as to avoid the need to constant repeat the controller logic that gets said parameters and sends them to validation.
Such models when correctly written can be easier to test in isolation, since after all they are just a plain old Perl class. Lastly this normality is in line directly with what Perl programmers might expect, and avoids the "What should we use forward|go|visit|detach for" problem that often arises in Catalyst development.
Downsides include that fact that you are making more classes and that might make it a bit harder on your programmers to trace the logical flow. Also you need to be careful to not let context leak upward into your business level logic. However it tends to be the approach I generally favor nowadays.
Other Approachs. Two other approaches that I've seen used to help keep your controllers minimal is to use Action Roles and Controller Roles. I've found using Action roles for complex logic to be somewhat ill advised, since they tend to be heavy handed (you can use them to wrap and modify the full execution of you action basically, and that's it). For example, I tried to create an action role to excapulate logic matching incoming arguments to a DBIC model, and in the end it was really a mess (just take a look at: Catalyst::ActionRole::BuildDBICResult and you will see what I mean.) As a result I believe that your use of Action roles should be nearly always limited to creating custom dispatcher matches.
Although Controller roles can be a great way to avoid redundent logic, in practice the fact that controllers in Catalyst are anemic and underpowered makes the approach a lot less useful then they otherwise might be. Creating them properly seems to not be easy, since we haven't really figured out what is useful about a controller beyond a handy namespace container for your actions. Until we have a solution for that, I find that I seldom create them for my work code personally, although I have seen that when people do, they nearly always either make the controller accept context (and create a new instance of the controller for every incoming request) or pass the context around to every method, which I personally detest as practice.
What is left undiscussed it the fact that the most common approach to View logic in Catalyst is to use the stash and let your templates manage all their own needs. This is a concern for a later date.
Hi John,
nice to read that others are also thinking about ways to get controllers thin while keeping Catalyst-specific things out of the model.
One of the last books I have read were "Domain Driven Design" by Eric Evans (http://en.wikipedia.org/wiki/Domain-driven_design). Transporting the Domain Logic into a Catalyst App I could imagine to insert a business model layer between Catalyst Controllers and Model classes. Models could be used for all infrastructure related things like Database, File Storage, Caching, Mail-Service and many more tasks.
Eric Evans suggests a lot of patterns to build into a domain model in order to cleanly separate concerns and keep the model clean, concise and maintainable.
I am currently trying to build some base classes for creating such a layer using a subset of Evan's suggested patterns. A central part of this layer is a configuration-driven class [accessed via $c->model('Domain')] based on Bread::Board. This way, one could group related queries into services and build entities acting as expanded (or proxies to) DBIC-Row objects with built-in logic that glues together DBIC and other models if needed. Currently I am torn about the granularity of the base-classes to offer. Evans cleanly separates Factories (for initially building objects), Repositories (for retrieving objects from DB) and Entities (the objects themselves) as well as Aggregates (extended Entities for public access). As DBIC offers many things the Java-counterparts do not seem to provide, one could omit Factories and Repositories, currently I am usure...
My first (still very pre-alpha) tries can be found here: https://github.com/wki/DDD
Best,
Wolfgang
Posted by: Wolfgang Kinkeldei | 04/05/2013 at 02:58 PM
@Wolfgang checkout our Implementing DDD by Vaughn Vernon when you're done.
As far as models that are context aware. You don't want your model to have a dependency on catalyst context object, rather you might, in the controller, derive from the context what you need into model terms and pass those pieces into your model. (you could do this with a factory that takes the context object and provides you with your model, it's basically another take on Dependency Injection )
The real trick is to understand the "Ports and Adapters" architecture
http://alistair.cockburn.us/Hexagonal+architecture
. This will ensure that both catalyst and your persistence are well abstracted.
Posted by: Caleb Cushing ( xenoterracide ) | 04/05/2013 at 07:09 PM
Thanks alot for the your advice. I just purchased the book and start reading now :-)
Posted by: Wolfgang Kinkeldei | 04/06/2013 at 05:31 AM
One thing I've been experimenting with as a mechanism for narrowing broad context objects down to the requirements of a particular model is through the use of type coercions. In general, a model that needs to be aware of context only needs a small fraction of that context so I'll make a class with that small interface, then define an associated type and a coercion from the Catalyst stash into this narrower context.
Things can still end up coupled, but there's now an explicit place (the definition of the new context class) that describes what context is being exposed to the model. It also helps with testing because, once I've tested the coercion does what's expected, I can just construct instances of the narrower context when writing tests. I generally find that the sooner I can get from a general data structure to a tightly specified object, the happier I am.
Posted by: Piers Cawley | 04/08/2013 at 05:19 AM