OOPhoto - Modeling The Domain In Steps (Round I)
The latest OOPhoto application can be experienced here.
The latest OOPhoto code can be seen here.
So far, in OOPhoto, my latest attempt at learning object oriented programming in ColdFusion, I have the design for my application done; I have the prototype created and tested; and, I have my prototype interface annotated with the information that the interface alone could not provide. With all that done, it is time to start modeling the domain that will drive our programmatic back-end. When it comes to domain modeling, I'm a straight up n00b. I've never done this before. I'm not even sure what tools are available for such things. I know I could probably use Visio (if only I had it installed) for my UML, or some online tool, but I'm actually not gonna worry about that yet - why add more overhead to a process that is already ... over my head?
To keep things as simple as possible for as long as possible, I am going to model my domain using plain old text. And, I'm not going to do this all in one stroke. Below, I am going to be modeling my domain just a little bit at a time and taking time to discussing my thoughts at each step.
Note: I know that in ColdFusion Component instantiation is not most performant thing in the world. I know that advanced programmers, such as Peter Bell, create things like the Iterating Business Object to find a happy mid-way between true object oriented programming and slow CFC instantiation. I, however, am not going to worry about that at this time; I will be using collections of CFCs when I feel that it is appropriate. Once I get a better (any) handle on object oriented programming, then I will worry about creating optimizations like the IBO.
Note: I don't believe in using UUIDs as identifiers. I can't necessarily explain it, but I love auto-incrementing values as IDs. Therefore, assume that all IDs listed below are auto-incrementing values, unless otherwise stated.
Step 1: Modeling The Primary Domain Objects
I am going to start out with what I am referring to as the "Primary Domain Objects" in my application. This is not a technical term in any way, and happens to be something I just pulled out of the air. To me, the primary domain objects are the in-your-face, obvious "Nouns" of the application - the meat and potatoes, if you will. Furthermore, to start off with, I am only going to think about their properties - methods will come next.
PhotoGallery
--------------------------
- ID
- Title
- Description
- JumpCode
- Photos[ 0..n ]
- DateUpdated
- DateCreated
In my PhotoGallery properties, I have an array of Photo objects. I am illustrating this with array notation "[]". Furthermore, I am saying that the array of Photo objects may contain zero or more instances as expressed by the 0..n notation. I know this is not official UML or anything, but remember, I'm just a n00b, so be gentle. For the search results page, I am just going to use the small thumbnail of the first photo in the Photo array; therefore, the PhotoGallery object does not to keep any thumbnail information itself.
Photo
--------------------------
- ID
- PhotoAsset
- Comments[ 0..n ]
- PhotoGallery
- DateUpdated
- DateCreated
The Photo object has a PhotoGallery reference, which is a circular reference to its parent PhotoGallery object. Also, I have decided that to keep things as simple as possible, I am going to be storing my images in the database rather than on the file system. In order to stay as focused as I can on object oriented programming, I am cutting out the interaction with a third-party file system. Therefore, my binary image data is going to be stored in a PhotoAsset instance (below).
But what about the Sort index of a photo within the gallery? This is where, I think, we see an obvious difference between the domain model and the database implementation. In the database, we might need a "sort" column when we persist the photo data; however, the domain model really has no need for this kind of information since the photo sort will be dictated by the order of the Photo objects in the PhotoGallery::Photos[] array. Thoughts on that concept?
PhotoAsset
--------------------------
- ID
- OriginalBinaryData
- LargeBinaryData
- MediumBinaryData
- SmallBinaryData
- DateCreated
Like I said above, I don't want to deal with a file system at this point. As such, I am storing my binary data directly in the database (although I am not going to think about the database yet - don't worry Hal Helms, I thinking *just* about objects, I promise). When it comes to photos, there are four images being created for every image that gets uploaded: the original image, the detail page photo (listed as Large), the standard thumbnail (listed as Medium), and the tiny thumbnail (listed as Small). Each one of these will get created and the binary data will be stored directly in the PhotoAsset object as byte array.
Comment
--------------------------
- ID
- Comment
- Photo
- DateCreated
The Comment object has a Photo reference, which is a circular reference to its parent Photo object.
Step 2: Adding The Primary Domain Object Methods
Now that we have our primary domain objects and their properties modelled, let's go back and add methods. For good practice, all of the objects in my system are going to have an Init() method, which will be the constructor. I am also going to use generic Get() and Set() methods for my property accessors and mutators, thereby taking advantage of ColdFusion's dynamic typing. Also, when possible, I will return the reference to object being mutated so as to allow method chaining to take place (a feature I fell in love with when working with the sexiness known as jQuery).
PhotoGallery
--------------------------
- ID
- Title
- Description
- JumpCode
- Photos[ 0..n ]
- DateUpdated
- DateCreated
- - - - - - - - - - - - -
+ Init() :: PhotoGallery
+ Get() :: Any
+ Set() :: PhotoGallery
Photo
--------------------------
- ID
- PhotoAsset
- Comments[ 0..n ]
- PhotoGallery
- DateUpdated
- DateCreated
- - - - - - - - - - - - -
+ Init() :: Photo
+ Get() :: Any
+ Set() :: Photo
PhotoAsset
--------------------------
- ID
- OriginalBinaryData
- LargeBinaryData
- MediumBinaryData
- SmallBinaryData
- DateCreated
- - - - - - - - - - - - -
+ Init() :: PhotoAsset
+ Get() :: Any
+ Set() :: PhotoAsset
Comment
--------------------------
- ID
- Comment
- Photo
- DateCreated
- - - - - - - - - - - - -
+ Init() :: Comment
+ Get() :: Any
+ Set() :: Comment
Step 3: Creating Service Objects For Primary Object Access
At this point, we have the beginnings of our primary domain objects (I say "beginnings" because I am sure that these will have to change later). Now, we need ways to get at these objects. If you look at the prototype interface, there are several ways to search for information within in the system. Let's provide that functionality through "Service" objects.
Starting with the homepage, we immediately see three services - keyword search, jump code search, and recent photos. The first two are obviously PhotoGallery related, so let's create a PhotoGallery service object:
PhotoGalleryService
--------------------------
+ GetByJumpCode() :: PhotoGallery
+ SearchByKeywords() :: PhotoGallery[]
When you use a jump code, you are not really searching. A jump code is really just a way to directly access a given photo gallery; therefore, rather than use a "search" function for jump code, I went with "GetBy". But, speaking of "get by" functionality, I realize that we also need to access a photo gallery by ID when we look at the gallery detail page or the gallery edit page. So, let's add that functionality too:
PhotoGalleryService
--------------------------
+ GetByID() :: PhotoGallery
+ GetByJumpCode() :: PhotoGallery
+ SearchByKeywords() :: PhotoGallery[]
Now, going back to the homepage, what about that Recent Photos section? Is that a PhotoGallery service? Or a Photo service? We're not really listing any information that has to do with the galleries, just the photos and links to the photo detail pages. As such, I am going to put this in a PhotoService object:
PhotoService
--------------------------
+ GetRecentPhotos() :: Photo[]
Just as with the gallery service, we are also going to need to access a photo based on an ID when we look at the photo detail page:
PhotoService
--------------------------
+ GetByID() :: Photo
+ GetRecentPhotos() :: Photo[]
These services take care of only the most obvious functions that the application provides to the end-user. Going past the homepage of the application, we see things like Add Comment, Edit Gallery, Upload Photo; it is clear that many more services are required for this application to be functional. I wish I had time to go into it now, but unfortunately, I am going to have start doing some billable work.
As a teaser, here are some of the other features that I can see right off the top of my head:
- Object creation and persistence.
- Object validation.
- Error message reporting.
The object creation is going to be particularly interesting since several of our primary domain objects have circular references. This becomes very hairy in the case of the Photo object - it has a PhotoGallery parental reference. What does this mean when we come to a page where we are accessing the Photo by ID (think photo detail page)? It means we have to create a Photo object and a PhotoGallery object, which itself has a collection of Photo objects. The trickiness is that we want the primary Photo object to be the *same* Photo object as its counterpart stored in the Photo->PhotoGallery->Photos[] array, otherwise our data references might not make sense. Should be fun stuff!
I hope you have enjoyed my journey so far - more domain modeling to come soon!
Reader Comments
@Ben
Don't be afraid of the ColdFusion query type!!! I see this all the time when people start off, where they want to return arrays of objects.
The CF Query type is very powerful, wicked fast, scalable, and the best way to return large sets of records.
This is the reason, for instance, that the Transfer ORM returns a query from list() methods, and a CFC from get() methods.
Yes we can create Iterators (like the IBO), and sure they're useful, but go down that route only when absolutely necessary.
One of the major advantages to CF is cfquery and the query type. There's no reason to abandon that if you start going OO, else we're really nothing more than Java Lite. :(
@Elliott,
Don't get me wrong - I *love* the ColdFusion query object. I often tell people it is one of the most badass things in ColdFusion. The problem is that I don't even know OOP well enough yet to know where to leverage which ones.
For example, my gut feelings would say to store the "Comments" as a query inside of the Photo object since I never need to do anything with them at all except output them (and add them)... but is that a slippery slope?
However, in the search page, it does feel like a query would be the best bet... but again, I just want to understand the OOP principles and then I can come back and optimize to leverage more of the power of ColdFusion.
I don't want ColdFusion to be "Java Lite"... but I want my brain to understand Java Lite before I really come in and power it up.
Well, I'm more of a noob to OOP than you are so I'll phrase my comments as questions, that way they are easier to ignore. : )
Should the service layer be broader and more abstract? Maybe a single "Photography" service rather than a gallery and photo and comment services? These don't seem like a service layer, but more like abstract domain objects that are being extended by the concrete gallery, photo, and comment objects.
Also, perhaps since the get and set methods will be generic (not sure that is the right term) should they exist once in the single service layer rather than in each object?
Should the methods you have in your service objects actually be pushed deeper into the business (domain) objects? For example the getByID in the photo service actually be accessed as Photography().Photo().getByID().
Very curious to see how this turns out...
@Seth,
I can offer my opinions as a fellow n00b - take with a grain of salt:
Re: Photography service... I think you could offer this as a RemoteFacade for your application's public API object since it would greatly simplify the external interface of the application. However, from an internal view point, I think you still want a good separation of concerns.
Re: Generic get/set methods... I would create these once in a "Base" domain model object which the other model objects would extend. Therefore they all have it, but really, they are all just inheriting it from one object.
Re: Pushing GetByID() into the model... I shy away from this because it feels too much like pulling yourself up by your own boot straps. I like the idea of a service creating objects and populating them. That way, you can call other services needed to create composed objects and the model doesn't have to know about all that coupling.
Again, just my very uneducated opinion.
Ben,
A few general comments.
1) You are using 'get' an awful lot in your method names which is often a symptom of looking at your objects as nothing more than data. Object Orientation is more about modeling behaviour than about getting data. From looking at your initial spec, it looks like in some cases the more appropriate noun is load and in others it is find. It might be a point to consider what the true behaviour of your method is.
2) I am not a big fan of the getById method. Another way to do this would be to make a find() function that takes a filter object. The filter object would be a bean that takes parameters and provides intelligence on what is being used for criteria.
The advantage here is that when you implement a 'tags' functionality and you want to provide a method to get the photos for a specific tag, you can simply edit your filter object and whichever consumers of the filter object need to know about the tags. Thus keeping your service layer intact and preventing an explosion of methods everytime you wish to open up another functionality for finding a photo.
Here is an example of a filter object I happen to have on disk.
http://cfm.pastebin.com/f41d04cbb
3) Your generic functions 'get' and 'set' are a little indirect. Proper OO is about building a sensible contract between objects. Using a generic set and get really eliminates that contract and can make working in team environments much harder. Where does the property list go? Since the object can theoretically accept any piece of data, who knows what can be requested from that object? is it documented somewhere? Who maintains the documentation? Are they really doing it? etc...
Using methods gives an easy way to add behaviour to your objects. Lets look at a simple example:
http://cfm.pastebin.com/f6a759557
see the methods at the top? getFormattedSavingsPercent, calculateSavings, getFormattedDiscPrice and so on? This is much better implemented using methods, than the generic get or set. else there is a million places that reference the key to the generic get that can be hard to debug when something isn't working. Most implementations of generic get return an empty string when the key passed isn't found. While sensible, it turns the work back to the programmer to understand the data and know whether that is appropriate or not for the circumstances. Maybe easier done in development where the data is controlled, than in production once the data grows and represents real world situations.
Anyways, this is food for thought. There are many paths to greatness and there are many ways of feline depilation. :)
Dan Wilson
Dan has some interesting points, but the idea here is to learn for yourself and try things out. Don't be too concerned with what I, or Dan, or about anyone else says about your experiment. I mean, if someone sees something that is absolutely going down a bad path, we should mention it and let you consider it. But overall, part of learning OO (at least for me) was and still is trying things out and seeing what works and what doesn't.
I don't have too much of a problem with methods that have "get" in them. Sure, if loadX() or findY() makes sense, go right ahead. But as long as the method is, at its heart, about having the object DO something, I'm less concerned with the method name. getByID() or find() or findByID() all reveal the intent of the method. Use whatever works for you, as long as it is clear.
I do have to go with Dan on the aversion to generic get() and set() methods. This is a topic where some people love them and some people don't, and I definitely don't. The problem is that if one were to look at the API for, say, your Photo object, they would have absolutely no idea what properties to specify for the get() and set() calls. get('cthulhu')? set('foo', 42)? If you want to keep things dynamically typed that's fine, but it is so trivial to write, or more likely generate with a snippet or a bean creator, the getters and setters that I just don't see any reason not to do so.
Even though we want to avoid using getters and setters as much as we can, the bottom line is that something, somewhere has to know what it can set or get from the object. At minimum, to populate it, and probably to display it in a page or a form. From an API standpoint, set() tells me nothing, but setDateCreated() tells me everything. If one goes too far down this path, one could envision a single AnyObject that only has a get() and set() method and that can hold anything. Again, just my opinion, and others may weigh in since this topic makes the rounds on blogs and message lists with some frequency. But my advice would be to just write the getters and setters.
Overall I think what you've laid out looks very good. Keep it up!
Of course by the end of that comment I forgot my own advice from the beginning of it. If you really feel like trying out the generic get() and set() approach, by all means try it out. That way you can see for yourself if you prefer it or not!
Regarding the question early in the post about leaving any kind of "sort column" out of the photo object model, it seems right to me that you should leave it out as you're currently planning. That piece of data would only come into play when dealing with multiple objects, whether you're just returning an array of the objects based on the sort order or altering the sort order (which requires altering more than one single sort order value).
To be honest, I think generic get/set are just fine as long as they match up to cfproperty or documentation. As I've pointed out before they're used quite extensively in Objective-C and Cocoa (under the name KVC) which has been around for ages before modern Java, DAOs, and any of this other "OO" nonsense. ;)
[person valueForKey:@"name"];
[person setValue:@"Elliott" forKey:@"name"];
These of course either directly access instance members or call the send "name" or "setName:" messages to the objects.
Of course these throw exceptions if the key doesn't exist.
I'm personally a huge fan of generic get/set in the sense that it saves typing code, and also allows much more dynamic access later (without resorting to cfinvoke/ isCustomFunction combinations).
However, I think returning an empty string on an object is a bad idea. The way I implement such functions is that they throw exceptions if the property doesn't exist, as an object shouldn't be regarded as just a "struct" with arbitrary properties. Of course something like a facade is a different matter, in those cases I use get/set that throws an exception, or returns a default value passed as the argument.
sessionFacade = ...;
sessionFacade.get("SomeKey",defaultValue);
sessionFacade.get("SomeKey"); // exception!
An even more powerful aside to this is KVO which is Key Value Observing. There's two ways we can do this. If we always require users of the API to use get() and set() then when some internal property changes we can broadcast events about different properties changing. If, however, we don't use get/set then we need to sprinkle KVO event broadcasting all over the API. OnMethodMissing can kind of solve this, but it also breaks typed arguments and return types.
Both of these ideas have been around for a really really long time, and are tried and tested in OS X, NeXT, GNUStep, and WebObjects software everywhere.
(Note WebObjects is written in Java now and is still using KVC)
http://www.macresearch.org/cocoa_for_scientists_part_xi_the_value_in_keys
http://developer.apple.com/documentation/Cocoa/Conceptual/KeyValueCoding/KeyValueCoding.html
http://developer.apple.com/documentation/Cocoa/Conceptual/KeyValueObserving/KeyValueObserving.html
Anyway, without rehashing this age old argument over and over again, I want to put it out there that this is not some kind of cardinal sin of OO design, in fact, it's quite common in some places. You decide if you want to use it or not, but don't listen to anyone telling you that it's somehow inherently wrong. :)
Ben, sorry for the topic that may be drifting away from the real point of the blog entry. Feel free to tell me to stay on topic and I will, I swear! Just say the word and I can take this debate over to my blog. ;-)
Elliot, I don't really see how what you're talking about lines up with how CF does things though. There's no way to enforce a type using CFProperty; what if I call set('age', 'foo')? There are ways to try to detect this but the get() call starts getting very complicated. You'd have to get the metadata for the CFC, look at the properties, find the matching one, look at the type, and confirm that the type matches what was passed.
Also, the CFProperty tags do nothing in terms of API. Looking at a CFC's interface (for example, using cfdump), even with the CFProperty tags in place, I still have no idea what I can and can't pass to the generic methods. I'd have to physically open it up or do a getMetaData() call on it to figure anything out.
The same goes for custom logic. The generic set or get has no easy way to actually apply additional logic to it beyond just setting or retrieving the value. At that point you'd need to add a custom getter or setter, and have the generic method look for an existing one and call it instead of handling the call. Which makes the generic methods even more complicated, and you still have to write the custom method as well as the cfproperty tags.
Things seem to get out of hand very quickly. By the time one has written all the cfproperty tags as well as the logic to handle the generic get and set methods, they could have written the actual getters and setters and have moved on. And given how utterly trivial it is to generate getters and setters at a single keystroke or with a bean generator tool, the argument about saving typing just doesn't hold water with me. I can generate a bean, even one that has 100 properties, in just a few seconds.
As for dynamic access, writing an invokeMethod() method is just a few lines of code and you have all the dynamic access you'll need. I use this often.
regards,
Brian
...this is straying from the main point, so I'll keep this short. The prior comment about 'Java lite' caught my attention.
What are the actual benefits of Coldfusion over Java that we should embrace on these OO projects to not only avoid 'Java lite', but truly get the best of both languages? Once upon a time the benefits were clear to me, but lately there's been a lot of effort to use Coldfusion as if it were Java so it's getting murky in my mind. The "Query" point above makes sense as it's a really great feature.
Hopefully, when you complete this adventure in OO you'll have a much clearer understanding of what works from each camp.
Thanks for blogging this project. It's making me think. :-)
@Brian,
Feel free to let the conversation lead anywhere you guys want. I am never against learning, so by all means, discuss.
@All,
Let me take a sad sad moment to talk about documentation. Plain and simple, I don't have any. And, I don't work on large teams (rarely on a team larger than one person). As such, my documentation is always the code base itself. Every time that I want to use a ColdFusion component and I can't remember what its methods are, I have to crack it open and take a look inside.
Now, don't get me wrong - I am not saying this as an excuse one way or the other. I do want to learn the right way to do object oriented programming; I just wanted to point out, that this concept of creating a "contract" with the object really has not meaning in my world as the code base itself is my only tangible "contract".
To me, a standardized way of defining gettable and settable properties is enough to create consistency across my domain model objects.
Of course, this is all from someone who has little to no experience in the matter :) Which of course takes me back to Brian's point - do I want to iterate over each step of the modeling process? Or do I just want to get something complete down, and then update that based on advice.
Right now, I am thinking that on such a tiny project, if I over think each step, I am going to quickly fall into Analysis Paralysis and not be able to move forward (especially true since, as Dan says, there is more that one way to skin a cat).
@Dan,
I do like the idea of "behaviors" that you can add to objects (as you have in your paste bin). However, I do not see how the idea of generic getters and setters goes against this. If you look at the way Peter Bell has his IBO set up (as far as I can remember), it looks to see if there is a custom getter / setter and if there is, it uses that rather than just going to the properties list. The underlying execution is still encapsulated in the object itself, and can change at a moments notice without having the rest of the world knowing.
And, as far as this filter object goes... I can't really argue against it or for it. What I can say is that I would be afraid that you run into the scenario where your query is so dynamic that it becomes hard to read. For example, let's say that a given Filter required me to join to a separate table that no other filter does - now I have not only conditional ON or WHERE clauses, I also have conditional JOIN clauses. This is doable at first, but I feel the larger the system (that might require a filter object), the harder that query will be to read and update.
Again, this is all theory to me, but that it what my gut said when I read your comment.
@All,
When it comes to returning empty strings, I think I agree with you all that throwing an exception when a bad property is requested makes sense. No need to return an empty string as long as your object has default property values, which I think it should.
Overall, I need to embrace the Exception and learn to leverage it rather than do everything that I can to avoid it.
@Ben, to me the idea of throwing an error if an invalid property is requested is just one more strike against the idea. If you want to be able to handle the error and keep going, now you'd have to wrap all of your calls to set() or get() in try/catch blocks, right? Thoughts? Is that a concern to you or are you coming from the standpoint that this would be revealed in unit testing?
@Brian,
I think the idea is that in the final product, no invalid properties should be requested. I can't think of any scenario that I have run across in my procedural style programming where I would want to carry on if I asked, for example, a CFHTTP result for a property that wasn't valid.
When a project goes to production, I assume that all code will only be asking for valid properties... I think?
@Brian
This is no different than the runtime exception thrown when getFooBar() doesn't exist.
obj.get("FooBar")
obj.getFooBar()
both fail in the exact same way, at runtime with an exception.
Do you wrap every single call to a getter and setter with a try/catch in case that object doesn't have the method?
Both are also susceptible to the exact same typo bugs.
If we were to dynamically invoke a getter...
obj.invokeMethod("get" & arguments.property);
this will fail in the same manner as:
obj.get(arguments.property);
Generic get/set are really not that different than the explicit get/set in any usage context. Documentation, maybe, but in terms of usage, not really.
Clearly I don't wrap all method calls with try/catch blocks, but I would argue that calling a method that doesn't exist would be less likely than calling get() and passing in an invalid property name. But the difference isn't very big and this is what unit tests are for.
Something else to think about: If (or when) we get an IDE that supports true code completion, everyone with generic methods is probably going to think hard about switching back to explicit methods. I hope that Adobe will give us this in a CF IDE and if they do, that it isn't far away.
It's true that using a dynamic method invoker could also generate an exception if an invalid dynamic property name were passed into it, but the difference there would be that the error trapping would be localized to one place (the invokeMethod() method).
@Brian S.,
I was just working on some persistence notes and I keep coming back to this idea of storing the Sort in the Photo object. From a general programming standpoint, it doesn't really mean anything to have a Sort value since the objects will be in an Array. However, when I go to persist the object, I feel like, if that Sort value is not part of the object, how can I persist is properly?
Because the sort value is only meaningful in the parent array of Photo objects, I would have to pass the sort parameter into the persistence method along with the Photo object itself. Something like:
SERVICE.Commit( objPhoto, intSort )
Does this seem odd to you? It feels very strange to me.
I'm not an OO programmer either, so I'm just guessing as to the best approach.
If the idea is to think of objects as "sets of behaviors" rather than "sets of data", then the Sort aspect wouldn't be part of the Photo object because the Photo can't sort itself: the Photo has no sorting behavior. So the sorting would have to be handled by something else, probably either the PhotoGallery object or (more likely) the PhotoGalleryService.
How exactly you would want to go about doing it, though, is unclear to me. While that "service.commit" example you gave does seem a bit "forced" at first glance, it does seem to be one valid way of going about it.
I thought you were going to hold off on the sorting options for version 2? I mean, for this version, you could just return the array of photos in whatever order the database gave them to you (which would probably be roughly chronological order).
Round II: www.bennadel.com/index.cfm?dax=blog:1285.view
This stuff is mad hard :)
@Brian S.,
I am going to hold of on the manual sorting of photos for now; but, I still like the idea of having a sort column. Ordering things based on "ID" always feels a bit hacky to me for some reason. But, I guess it's really not any different that ordering based on date_created.
@Brian S.,
If you say Service.commit() seems a bit forced, what might you recommend. I am really struggling with this stuff :)
I just meant that I understood why you felt odd about the idea of passing a sort value along with the Photo object to your Service.commit() function (rather than having that sort value somewhere in the Photo object already).
It certainly seems to me that that method would work and I can't think of anything wrong with it, but again not being an OO programmer there may well be some existing design pattern that might be better.
@Brian S.,
Ok cool. Thanks for the feedback.
Just ran across your interesting OOP blog so I'm a little late to this discussion. Well, maybe really late;>)
"I don't believe in using UUIDs as identifiers." This brings up the question of how you prevent someone using the back button on their browser and creating multiple galleries, instead of updating the current gallery. I know how I would prevent this using a UUID, but not with an auto-incrementing PK.
Any thoughts on how to prevent "back button duplication"?
Doug
@Doug,
Never too late to have a too conversation :) When I have form that has a number of steps, I'll typically keep all that step-wise information in a form "object" in the user's session. Then, as the user proceeds through a form, I'll have a conditional check for something like:
<cfif formData.step3>
<cfset formData.currentStep = "step3" />
</cfif>
Here, I am checking to see if step3 is true (assuming step3 is the last step in the process). Then, if anyone tries to go back, the form processing logic will redirect them (internally) to the "confirmation" step.
@Doug,
Also, I believe that most browsers will prompt the user to re-submit form data if they hit the back button - it's rare to see a back button re-submit a form without warning... or is it? Now that I think of it, perhaps IE doesnt' prompt?