Sunday, April 14, 2013

Kul controller design

Controllers are a love/hate relationship for me. They're a great place to put code that brings the models to the views. They're also great for figuring out what to do with a given request. All of those things could probably be done implicitly, but the rules would be hard to understand, I think. Or would break the code up into many small distributed chunks which would be painful to read / follow / debug.

I hate controllers because they don't make sense. What is the life cycle of a controller object? In most frameworks, it's the lifetime of the request. It exists only so that you can do dynamic dispatch from the request parameters. You could put initialization code in the controller, but in most frameworks, the controller constructor can't take parameters, so you're limited on what initialization you can do. So most of the time the controller exists only to hold similar functions together, and is really a stateless object. Stateless objects seem like a bad design choice to me. (Stateless does not equal immutable. Immutable objects are good design - particularly in threaded environments.)

Controller is a class searching for a purpose, and I don't like that.

I had designed controllers into Kul, but I've put it off to the end of the initial implementation, because I really don't like the way they're implemented elsewhere. Now that I've finally come to that point, I can't bring myself to implement them wrong.

The key point here is that we need controller actions without wrapping them up into a class. So where do you put them? Can't put the function into the model or the view, that violates separation of concerns, along with being bloody stupid. I could attach them to the application, but how would you then keep the application from being a god object? Not to mention that I've got the same basic problem in the application class - no real point for it (that's another topic though).

Coming back to basic principles, there are really only a few objects in this: request, response, model, and view. Each of those has a definite lifetime, and can be easily identified. If those are your objects, where does the action method go?

It doesn't go any of those places, because it doesn't need to. A web server request (with some exceptions) really has no side effects. It translates a request into a response and there is no state to keep track of. It's a pure function, which doesn't need to be attached to an object.

Granted, databases and sessions and cookies all bring state into the picture. But the database is external to the app server, and sessions and cookies are a part of the request and response. Controller actions aren't actually pure functions. But they're damned close.

The best way I can think of in Ruby for namespacing controller actions is a module. It actually has a certain beauty to it - for an action called list in the controller products in the application store, you need to implement the following method: Store::Products::list - that makes sense to me. Since you're moving the actions out of classes, we don't have to implement things quite the same way any more. Implementors have more options for being able to place classes where they feel like it.

Now we have a different problem - we have to get the request information into the action, and get the response back out. It's simple enough to encapsulate that information into a request and response object, which happens under the covers in most implementations anyway (but not in Sinatra - that's another post). But now we're dictating the signature of the action function, which I really hate.

A simple plan is to determine at runtime the signature of the function and give it the appropriate parameters, a la javascript. Pass the first parameter as the request, the second as the response. The request would actually contain all of the data needed for the function, so any other parameters other than request would be syntactic sugar, really. It's a little brute force (and you're still dictating the action signature) but it's not bad.

The other plan I came up with is to apply the module to the request object. It would be slightly different for the implementors because they'd have to define module instance methods instead of straight module methods, but it wouldn't be that much different. Now your controller action would get executed in the context of the request, which would give you access to the same instance variables people expect from controllers, such as the params hash, the request session and cookies, response object, etc.

Come to think of it, you wouldn't want to pass in the response object. You'd dynamically construct it during the execution of the action. That would make it cleaner.

There's really not a huge amount of difference between this approach and the way it's being done traditionally, except that you're no longer hiding behind a controller class that isn't being used for anything.

The first approach makes testing easier - your method is closer to a pure function and that makes it easier to set up a mock for the request. On the other hand there's no reason that you couldn't take a similar approach for the testing of the second method - you just build your request and have the framework execute the test on it.

I'm really starting to like the action on request approach. Need to think on it some more.

No comments:

Post a Comment