Well, I'm not sure there's a "typical" structure but the underlying
MVC pattern tries to ensure that:
* there's no logic in views
* the model is completely independent of the controller / views
* the controller is the traffic cop that knows about application flow
and the HTTP request / response
With that in mind, let's look at your questions:
> - Services handle list method queries themselves
Yes, the query belongs in the model (services are part of the model -
they know nothing about FW/1 and they know nothing about forms /
URLs).
FW/1 allows you to omit a controller method if all it would do is
delegate directly to a service method like this:
function doSomething( rc ) {
rc.data = someService.doSomething( argumentCollection = rc );
}
For a simple list operation, that's often the case so you can rely on
a service method:
function list() {
var result = entityLoad( 'stuff' );
return result;
}
This would put an array of stuff objects in rc.data - without any
other infrastructure.
> - For detail forms, services run the query themselves, create an
> instance of the corresponding transient model object, populate it from
> the query, and put the model object in the request collection for the
> view the use.
Yup, you could just have a service like this:
function edit( numeric id = 0 ) {
if ( id ) {
return entityLoad( 'stuff', id, true );
} else {
return entityNew( 'stuff' );
}
}
That would set rc.data to either a populated object based on the id or
an empty object (if id not provided or was zero).
> - For saves, services create an instance of the transient model
> object, populate it from submitted data, and ask it if it's valid. If
> not, they put the model instance and error collection into the request
> collection, and set the view to the edit form. If it is valid, the
> service does the save itself, using the populated model as the source
> of the data, and the controller's afterSave method redirects back to
> the list (or wherever).
Here's where the controller comes in. It knows about data from the
form / URL so it would coordinate the validation of that data. There
are several ways to handle this, depending on where you put your
validation logic. One possibility is:
// controller
function saveStart( rc ) {
... validate the data in rc ...
if ( invalid ) {
rc.message = 'Your data is not valid';
redirect( 'stuff.edit', 'all' );
}
}
function saveEnd( rc ) {
// data has been saved, redirect to confirmation page or whatever
rc.message = 'Your data has been saved';
variables.fw.redirect( 'stuff.list', 'message' );
}
// service
function save( numeric id = 0, string firstName, string lastName, ...
other args ... ) {
var object = 0;
if ( id ) {
object = entityLoad( 'stuff', id, true );
} else {
object = entityNew( 'stuff' );
}
// populate the object
// save it
entitySave( object );
}
If you're calling your model / services directly from your
controllers, you have more flexibility and you might do something
like:
// controller
function save( rc ) {
param name="rc.id" default="0";
var object = variables.stuffService.get( rc.id );
variables.fw.populate( object );
rc.errors = object.validate();
if ( arrayLen( rc.errors ) ) {
// invalid, redisplay form with errors
setView( 'stuff.edit' );
} else {
// valid, save it and redirect
variables.stuffService.save( object );
rc.message = 'Your data was saved';
variables.fw.redirect( 'stuff.list', 'message' );
}
}
The downside of that is it populates your object first - so your
object must be able to handle invalid data - and, if you're using ORM
(in CF9/Railo), you need to ensure your object won't get automatically
saved.
Another possibility is this - using validation as a service:
// controller
function save( rc ) {
rc.errors = variables.stuffService.validate( argumentCollection = rc );
if ( arrayLen( rc.errors ) ) {
// invalid, redisplay form with errors
setView( 'stuff.edit' );
} else {
// valid, save it and redirect
param name="rc.id" default="0";
var object = variables.stuffService.get( rc.id );
variables.fw.populate( object );
variables.stuffService.save( object );
rc.message = 'Your data was saved';
variables.fw.redirect( 'stuff.list', 'message' );
}
}
Or - if your service save() takes individual attributes instead, like
the first example above:
// controller
function save( rc ) {
rc.errors = variables.stuffService.validate( argumentCollection = rc );
if ( arrayLen( rc.errors ) ) {
// invalid, redisplay form with errors
setView( 'stuff.edit' );
} else {
// valid, save it and redirect
variables.stuffService.save( argumentCollection = rc );
rc.message = 'Your data was saved';
variables.fw.redirect( 'stuff.list', 'message' );
}
}
> - For deletes, services do the query themselves.
Yes, probably after the controller has validated that you can delete
this object.
Hopefully that helps answer your questions?
--
Sean A Corfield -- (904) 302-SEAN
Railo Technologies, Inc. -- http://getrailo.com/
An Architect's View -- http://corfield.org/
"If you're not annoying somebody, you're not really alive."
-- Margaret Atwood
The services (in FW/1) are simply a gateway to your model. Really they
are *part* of your model.
> Models are smart value objects that contain data and business
> rules for one object instance.
That's very confusing terminology - probably why you're struggling here.
FW/1 is MVC:
* Model - all of your business logic and data
* View - HTML display pages
* Controller - control flow / traffic cop
In FW/1, that's
* Model: services and everything they touch
* View: views and layouts
* Controller: FW/1 itself and your controllers
It's model (singular) not models. I've seen that plural notation
somewhere recently and it's very jarring. Your application has one
model, made up of all your (smart) domain objects and the
orchestration code (usually 'services' of some sort). Orchestration
code is needed for operations that don't naturally live in a single
domain object.
If you haven't already, I'd recommend reading my "Beans and DAOs and
Gateways, Oh My!" chapter in the Adobe ColdFusion Anthology (from
Apress) or the original article in the FAQU: Do More Code Less issue.
It covers several different ways of organizing model code in layers.
> Isn't this what suppressImplicitService disables in 1.2, and that will
> be gone completely in 2.0?
You can enable it in 2.0 but it will be off by default (in 1.2 it's on
by default but you can disable it).
> With that feature shut off, how else would
> would the service get called if the controller doesn't do it
> explicitly?
That's the point. The controller would need a call to service() - or
would need to call the underlying service directly.
> What's the rationale behind deprecating it?
It's been discussed a few times on the list so browse the archives. It
was discussed at length in the BOF at CFUnited and that's where my
final decision was made, based on feedback from the attendees (about
40 FW/1 users).
Basically most FW/1 users find it either unnecessary or confusing -
depending on how experienced they are.
I originally thought I was providing a useful shortcut but it has
generated far more discussion than any other feature in the framework.
I've never really been entirely happy with implicit service calls and,
overall, it adds very little value.
> - The controller doesn't instantiate the model instance directly; it
> asks the service for the instance, but then populates it itself. It
> makes sense to me that populating gets done by the controller, since
> the service doesn't know about form/url/rc, but it's interesting to me
> that it doesn't create the model instance. Is that pretty standard,
There is no One True Way. Ask a bunch of devs and you'll get a bunch
of different answers. Do what works best for you and just be prepared
to change your thinking if you run into problems.
> - Lower level question, but if you do a redirect with a message, that
> ends up in rc, right? How do you access that from a layout, so there's
> standard infrastructure to display it?
rc.message
> No ORM, yet. If the structure of your model was that it threw
> immediately on invalid field assignments, that'd seem to limit your
> flexibility a lot; you'd have to try/catch each assignment
> individually. Is that a common strategy?
Again, there is no One True Way. Different developers do different things.
I've done it different ways in different applications. If I'm using a
typed ORM, I tend to validate form data outside the object and only
update the object if validation passes. If I'm using untyped objects,
I tend to populate them first and validate them internally. In
general. But even that's not set in stone.
--
Sean A Corfield -- (904) 302-SEAN
Railo Technologies, Inc. -- http://getrailo.com/
An Architect's View -- http://corfield.org/
Ugh! I hadn't noticed that (I didn't write any of the user manager examples).
I'll open a ticket to clean that up.
> --
> FW/1 on RIAForge: http://fw1.riaforge.org/
>
> FW/1 on github: http://github.com/seancorfield/fw1
>
> FW/1 on Google Groups: http://groups.google.com/group/framework-one
Yup - and remember that there's "service" in the general model sense
and *service* in the FW/1 sense. The latter is more of a facade onto
your overall model. It's an optional convenience - see below.
> (FWIW, sounds like a directory structure to mirror this organization
> would be a 'model' directory containing 'services' and 'beans'
> directories, probably the object factory, and maybe its config.)
Yup. Spot on. That's probably how I'll lay things out in FW/1 2.0 for
the examples - especially examples that use DI/1 (since it will most
likely support a convention that anything in a beans/ folder is
non-singleton).
> This leads me to another question, apologies in advance if I'm behind
> the OO-think curve here: It seems that the default FW1 strategy is
> that services aren't called directly by controllers; service calls are
> queued with \service(), then run all together. What's the advantage of
> that sequencing model? It makes using the results of one service call
> in another more awkward, and I'm unclear on what you get in return.
It's intended to create a clear separation of styles: controllers
interact with the framework and know about URL/form data etc, services
are pure data-in / data-out - and controller logic wraps service
calls:
controllers/section.startItem() - calls service() to queue up foo.bar,
something.else
rc.data = services/section.item()
rc.foo = services/foo.bar()
rc.other = services/something.else()
controllers/section.endItem()
That's the typical structure in a controller anyway if you have explicit calls.
Also bear in mind that the elements of the rc are available as named
arguments to services and they are in a strict sequence so:
service( 'foo.bar', 'foo' );
service( 'something.else', 'other' );
will allow for foo.bar( data ) and something.else( data, foo ) for example.
> Still, I'm used to controllers calling one or more model
> methods, sometimes passing (partial) results of one to another (as
> individual arguments).
Then don't use the implicit calls :) Manage your services directly in
controllers/section.init(fw) and call them explicitly:
rc.foo = variables.fooService.bar( data );
(or use a bean factory to manage services).
Nothing is *forcing* you to rely on the implicit service call model.
It's there as a convenience for the folks who like it and for those
where it simplifies their controllers. The whole implicit / queued
service call thing has proved to be the most confusing / controversial
aspect of FW/1 because people focus on it and think they *must* use it
- and that's simply not the case. It's an _optional_ convention for
those who want it. This confusion and controversy is why it's not
going to be the default in FW/1 2.0.
> Is this a wrong-headed way to think about it? Although it's clear from
> some of your examples that controllers can have services injected
> which they can then call directly, clearly FW1 "wants" you to queue
> service calls instead.
No. FW/1 doesn't "want" you to do it in a particular way. It offers
the ability to use implicit calls and queued services and it offers
the ability to manage all the services yourself (manually or via a
bean factory) and use explicit calls.
Hope that helps?
Yup, so you're either going to need to take control of the services
and call them directly yourself in your controllers - the conventions
don't work for you there - or to treat the FW/1 services as a pure
facade onto your model (and have your 'real' services in the model/
folder, for example).
In that second case your something.else( data, foo ) method would turn
around and call realSomething.else( data, foo.someField );
> Queued service calls appeal less to me.
Well, that's the FW/1 way. When you call controller() you are telling
FW/1 to queue up calls to controller methods. When you call service()
you are telling FW/1 to queue up calls to services. This ensures the
request lifecycle is followed.
Again, if the conventions don't work for you, manage your services
directly and call methods explicitly in your controllers. That's why
FW/1 supports 'any' bean factory.
> FWIW, I actually prefer explicitly passing data into views too, rather
> than leaving it "laying around" in rc space.
I don't know of any CFML frameworks that provide that idiom
(interesting, tho' it is).
> It would break backwards compatibility, so it'd need
> to be a non-default option.
Views currently have access to any methods in Application.cfc and
framework.cfc directly - by design. So not only is it a big backward
compatibility issue, it goes against the simple design of FW/1.
--
Sean A Corfield -- (904) 302-SEAN
Railo Technologies, Inc. -- http://getrailo.com/
An Architect's View -- http://corfield.org/