Architecting FB4.1

Architecting with Fusebox 4.1 is a very wide arena. There are lots of ways to do it, but I think I found one that is particularly well suited to the framework, especially when combined with CFCs for a backend. My presentation at the 2004 Fusebox Conference hinted at much of the principles, but I can say that after having written a couple more FB4.1 apps from scratch, that they make for a very nice little package.

There's a few main points:

  1. Separate 'pages' from 'actions'
  2. Separate the UI from the application proper
  3. Separate the markup from the presentation
  4. Reuse code everywhere it makes sense, and nowhere else

For point #1, a legit question might be "what's the different between a page and an action?". My definition is that a page renders content to the user, while an action performs an action and redirects to somewhere else. The definitions are mutually exclusive, so if you're doing an action, you HAVE to redirect to a page in order to render content; you can't do both on a single request. There are a few exceptions (for instance, marking a message 'read' – an action, when you open it – a page), but they are few and far between.

I opted to have all my fuseactions that generate HTML be named as you'd expect (addEntry, viewComments, etc.). All my fuseactions that do actions start with 'do' (doInsertEntry, doDeleteComment, etc.). In my presentation at FBCon04, I'd recommended separating the pages and actions into separate circuits. Upon further reflection, I'm retracting that recommendation in favor or just separating the pages and actions in your circuit.xml file. One block has all the pages, and another block has all the actions (commented appropriately, of course).

Point #2 is accomplished by the use of a 'service' CFC. It is a singleton CFC that is instantiated into the APPLICATION scope of the app, and it contains all the business operations and data recall for you app. Each business operation has a defined method name and set of parameters. How it's implemented (inlined SQL, a mess of BOs, DAOs, and gateways, magical pixie-powered light-based storage, whatever) is irrelevant, and that's exactly the idea. You should be able to change your presentation layer without changing your service object, and you should be able to reimplement your service object (maintaining the method signatures, of course) without changing your presentation layer.

Point #3 is the standard "use (X)HTML and CSS" line. Make sure you pages are designed from a semantic perspective. Build all the pages before you ever write a single line of CSS. Not going to be pretty, but they do need to be functional in that fashion. Then whip out your CSS magic wand and make it all pretty. This really has nothing to do with FB4.1, but it does have to do with good architecture, because the more divorced your markup is from your presentation (CSS, in this case), the easier it will be to make changes (visual or functional) down the road.

And at last, number 4. Two clauses, and the latter is the more important one. I've seen a lot of people (myself being one) who try to squeeze every last drop of reuse out of a bit of code. Reuse is good, but there are definite times when it's simply a foolish goal to strive for.

In this particular app, most of my display templates (roughly analogous to views in Mach-II) are in their own fuseactions. Again, I opted to not use a separate circuit, but rather name them starting with an underscope (i.e. _entryForm) set them as access="internal", and group them all in their own section in circuit.xml. They include everything needed to render the view, except for designated parameters for reuse (such as the XFA to submit a form to). That means they might include calls to the service object for a recordset, might set their own XFAs, might do basically anything, as long as it's bound directly to rendering that specific view file.

So, in a nutshell, what do I have? A single circuit, broken down into three sections (pages, views or pagelets, and actions). An abstracted service object that encapsulates all the business and persistent operations behind a known interface. Markup that is independant of it's presentation. An app that is both enormously flexible and easy to work with.

Future updates can apply to single sections of the app without affecting other sections if they don't need to, greatly reducing the subsequent testing needed. Poorly implemented sections (of which there are a couple because of time constraints), can be transparently reimplemented without affecting the app.

What I've described isn't quite MVC, at least in the traditional sense. It's subtly different, and while only time will tell, I think those differences result in a much better end result.

11 responses to “Architecting FB4.1”

  1. Greg

    Hey Barney,

    Can you expand on point #2, maybe an example on how you use the service cfc.

    btw I think I may have actually raced you back when I was swimming. Maybe at US open or something.

    anyways thanks for teh tidbits to think about.

  2. barneyb

    The service is just a collection of methods that implement the business operations of the application, in a framework agnostic way. That lets the backend (the real application) be independant of the UI being used, as well as support multiple concurrent UIs (such as an HTML version and a Flash version).

    For example, I've got a method for retrieving a comment, which is called like this:

    [set name="comment" value="#application.blogservice.getCommentById(attributes.id)#" /]

    Of particular import, note that the method doesn't know where the ID came from (attributes scope, in this case), nor the name of the variable it's return value is stored in (comments, in this case). Because of that, I can call the method in a different way (from a CFC facade, for example) without the method caring.

    That's one of my major gripes with "standard" FB4 MVC, because your fuse defines what the parameters and return variable must be named. Well, the Fusedoc does, but the point is that they are locked at architect-time. With a method call, I can pass it a value from a query, a page parameter, a hard coded value, or whatever, and I can also assign the result to a variable of my choosing. No variable collisions, no fuse-centric variable assignments, just pure logic in a container that's not only reusable throughout the framework, but throughout the app as a whole.

  3. Ulf

    Hi Barney,

    Are there any problems to expect because the service factory is in application scope and calls to it can not be locked in circuit.xml?

  4. Ed.T

    Barney,

    I saw your presentation at Fusebox 2004 and left with some ideas for improving my apps. But I'm still coming up short. Can I get some training wheels? I feel the need for two major improvements and you've got them: re-direct after post, and model independent implementation. I've been trying to get more comfortable with DAO's expecting the model to be more flexible through their use. Any materials or samples you can suggest to get these ideas through my thick skull?

    Thanks,

    Ed

  5. Barney

    Ulf,

    No, not really. Access to shared-scope CFCs does not require locking unless a race condition is created. However, only the CFC should know if such a condition exists (because that's an implementation detail of the CFC), so only the CFC itself can know if locking is required. Thus, any CFLOCKs that you need are internal to the service CFC, not external (in the calling code).

    In this app, the service object happens to carry a single instance variable (the datasource name) and the SQL access is all transactional, so there are no race conditions to worry about. That will undoubtedly change in the future, and at that point, I'll add CFLOCK calls as necessary. Because they are internal, my FB app won't even know the change happened; it is 100% isolated from implementation details.

  6. Barney

    Ed,

    Redirect after post is pretty simple. Any time a request is a form submissions, don't display a page, use CFLOCATION (or an equivalent) to go to another page for the display. Take the example of an "edit user" operation. First page is the form. It submits to a second page which runs an UPDATE query. Then, instead of displaying a "update complete" message, it uses CFLOCATION to forward to a third page, and that third page displays the "update complete" message.

    The only exception is errors in server-side validation. In that case, it's okay for the action page to detect the validation error, and then regenerate the form with the error message(s) at the top.

    For the independant UI and model, DAOs aren't going to get it for you. Those are an implementation detail of the model (a specific way of implementing persistance operations), and if your FB app knows about the DAOs, then it's not encapsulated. The service object provides an adapter between FB and your model. If you use DAOs, your service will deal with them. If you use inline SQL, your service will do it. The FB app doesn't know and doesn't care, as long as the service method does what it's supposed to do (addUser, getUserList, etc.).

    Breaking down your persistance code into DAOs that are then leveraged by your business logic is a good plan, but they are for separating business and persistance logic, not business and presentation logic. Both are desirable, but your business and persistance logic are usually much more tightly coupled (just the nature of the beast), so the benefits aren't as great as between business and presentation logic.

    I don't really have any good samples to share, unfortunately. The code from my presentation at the conference has an example of how service objects are used. Any book on application design should also talk about these principles, no matter the platform (CF, J2EE, .NET, FORTRAN).

  7. Ulf

    Hi Barney,

    just to get it right. If I need locking, I'll have to do it in this service component (which is in application scope).

    So I don't lock access to the service component (and use cflock scope="application"). I'll have to use named cflocks in a called method if I use business objects where race conditions can occur. Is that right?

    Ulf

  8. Barney

    Ulf,

    You got it. I usually set variables.my.lockkey to a UUID in the init() method and use that as my lock name. If I need multiple lock names, then I'll obviously have multiple keys. That helps avoid typos in your lock names, which can lead to amazingly subtle (and difficult to quash) bugs when your app starts running under load.

    Don't try to do too much with your service component regarding locking. Just as it encapsulates the locking behaviour from it's calling code, your other objects should encapsulate their locking behaviour from the service object. If you're allowing multiple service method calls to access a single object, then yes, the service needs to lock that. But if your business object has an internal race condition, the BO should do the locking, not the service, because the service can't know that race condition exists without knowing how the BO is implemented (and thereby breaking encapsulation).

  9. Ulf

    Hi Barney,

    Many thanks. You helped to come forth.

    Ulf

  10. Richard

    I may be a little late to this discussion but I though I'd add my tuppence worth anyway.

    Its regarding point #1 – split action and display pages, so that an action page has to redirect to a result page. I may have misunderstood so correct me if i'm wrong, but isn't this what fuses are designed for? You can still have one page request, ie fuseaction, but split it into separate action and display fuses? Each fuse is still independent and reusable, and this way you have one less fuseaction to worry about.

    It seems a bit of overkill to have a different fuseaction for each individual fuse.

  11. Barney

    Richard,

    Yeah, you've misunderstood a little. Action pages are like "process this form submission", while display pages are like "display this page". Each page may required multiple fuses (and fuseactions, in FB4.x), quite possibly even some of the same ones. But the two things should be separate requests.

    For example, say I click an 'add to cart' button for a product. The overall result of the click should be to add the product, and display the cart for the user. The click will first call an action page that'll add the item to the cart. Then the fusebox should redirect to another (display) page that renders the shopping cart. The two shouldn't happen in the same request.

    The end result is that the user experience hasn't changed (aside from perhaps a short extra delay for the redirect), but the user is protected from adverse consequences of hitting the refresh button. It also makes the code simpler, because each request is only doing a single thing. Simpler code is easier to maintain code, and that's always a good thing.