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.