OOPhoto - Modeling The Domain In Steps (Round II)
The latest OOPhoto application can be experienced here.
The latest OOPhoto code can be seen here.
Yesterday, I got through modeling some of the more basic, obvious domain objects in OOPhoto, my latest attempt at learning object oriented programming in ColdFusion. I realized that domain modeling, even for such a simple application, is fairly complicated and will require some time. That is why I am breaking this process up into several, more manageable posts. Before we proceed, let's just recap what we came up with yesterday.
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
PhotoGalleryService
--------------------------
+ GetByID() :: PhotoGallery
+ GetByJumpCode() :: PhotoGallery
+ SearchByKeywords() :: PhotoGallery[]
PhotoService
--------------------------
+ GetByID() :: Photo
+ GetRecentPhotos() :: Photo[]
I got some great feedback yesterday from people like Dan Wilson, Brian Kotek, and Elliott Sprehn. While it was useful and some of it was a bit over my head, I like what Brian said about just letting me try stuff for myself and seeing what works and what doesn't. As such, I am gonna try to just push through the domain model to at least have something before I get caught too much in Analysis Paralysis without the ability to move forward. But that doesn't mean that I don't want feedback - by all means, please please please keep that coming.
Ok, so let's pick up where we left off. At the end of the last post, I had three additional points of functionality to cover that came right off the top of my head:
- Object creation and persistence.
- Object validation.
- Error message reporting.
Let's start with object creation and persistence. I know that in object oriented programming, objects are supposed to be "Smart." You are supposed to be able to ask them to do things and they are supposed to know how to do them. From what I have read, this often times refers to things such as object creation and persistence. I think this is known as the Active Record pattern? Basically, where an object has methods like load(), commit(), and delete() (or whatever CRUD terminology you like).
Here's what I don't like about that - persistence is only an issue because of the physical world in which we live - it is rarely a reflection of the programming needs. In an ideal world, we would have infinite RAM and no computer would ever need to be (or could be) shut down. In a world like that, the model would never need to do anything more than simply exist. Maybe I'm way off base there, but based on that reasoning, I don't think that persistence should be a concern of the primary model objects; I think it should, instead, be the concern of an external object - a service object.
So, what are the objects that we need to persist? The most obvious objects to me are those that have properties; if an object doesn't have any sort of "state" associates with it though instance-based properties, it has no need to be persisted. That leaves us with our primary domain objects:
- PhotoGallery
- Photo
- PhotoAsset
- Comment
When I see a list of objects like this, my immediately thought it just to create a Service object for each one of these types. We already have a service object for PhotoGallery and Photo - adding two more seems fairly straightforward. But then I think to myself, do we really need a service object for each? Think about the Comment object. It's so small and it will never be used outside of a Photo. It's not like you can access a comment directly via a URL or anything. You can't even delete a comment once it has been made. Maybe it should be handled by the PhotoService? After all, it really is tightly integrated with the Photo object.
To be honest, I simply don't know enough at this point to make decisions like that. As such, I am gonna go the default route of just creating a service object for each of the above primary domain objects. And, for the loading and persistence, each one will have the following methods:
Load() - Creates and populates a new object based on an optional ID.
Commit() - Saves the data either through insert or update. I like the idea of encapsulating the insert vs. update.
Delete() - Deletes the object.
So, let's update our service objects and make the two additional ones for PhotoAsset and Comment:
PhotoGalleryService
--------------------------
+ Commit() :: PhotoGallery
+ Delete() :: PhotoGallery
+ GetByID() :: PhotoGallery
+ GetByJumpCode() :: PhotoGallery
+ Load() :: PhotoGallery
+ SearchByKeywords() :: PhotoGallery[]
PhotoService
--------------------------
+ Commit() :: Photo
+ Delete() :: Photo
+ GetByID() :: Photo
+ GetRecentPhotos() :: Photo[]
+ Load() :: Photo
PhotoAssetService
--------------------------
+ Commit() :: PhotoAsset
+ Delete() :: PhotoAsset
+ Load() :: PhotoAsset
CommentService
--------------------------
+ Commit() :: Comment
+ Delete() :: Comment
+ Load() :: Comment
Now, just because each of our primary domain model objects has a service object that handles persistence, that doesn't mean that we can't be smart about it; we can still ask the PhotoGalleryService to persist a PhotoGallery instance and have it take care of calling all the necessary service objects to persist the composed Photo objects (and the Photo objects, their composed Comment objects, etc.). Just because we have the available methods for all kinds of persistence, as "API" programmers, we don't have to use them.
After I finished defining the service objects above, I had a thought - what is the difference between the Load() method and the GetByID() method? Both of them take an ID and return a populated object. Why bother having both? Then I realized why having both is essential - they do fundamentally different things. The Load() method creates a brand new object and pulls data out of the database. The GetByID() method, on the other hand, simply returns an object. Where the GetByID() gets the object in question is not a concern of ours. Maybe it turns around a calls something like the Load() method; but, maybe it turns around and checks an application cache to see if that object already exists. This job is fundamentally different than the job of pulling data out of the database, and therefore, both methods are necessary.
Before I go any further, I'd like to play out a little thought experiment; I'd like to take the action of persistence and write it out in pseudo code to see if any glaring holes pop out at me. I know that we are concerning ourselves with too much implementation at this point (and should be worrying more about object "interfaces"), but I think this is helpful as a sanity check. Persistence of complex objects is, well, complex and probably cannot be fully understood in the first pass. So let's take the most complicated object - the PhotoGallery - and persist it:
PhotoGalleryService.Commit( PhotoGallery )
Check to see if PhotoGallery is valid.
If PhotoGallery is not valid, throw exception.
Persist simple PhotoGallery properties.
For each Photo in Photos[] array, call:
PhotoService.Commit( Photos[ i ], intSort )
Check to see if Photo is valid (should be since PhotoGallery is valid).
If Photo is not valid, throw exception.
Persist simple Photo properties.
PhotoAssetService.Commit( PhotoAsset )
Check to see if PhotoAsset is valid (should be since Photo is valid).
If PhotoAsset is not valid, throw exception.
Persist PhotoAsset properties.
For each Comment in Comments[] array, call:
CommentService.Commit( Comment[ i ] )
Check to see if Comment is valid (should be since Photo is valid).
If Comment is not valid, throw exception.
Persist Comment properties.
The first thing that pops out at me, but something that I already know, is that we have no way to validate the domain objects before we persist them. I am going to address that issue next. Another, more subtle, point / question that jumps out at me is - what about transactions? Because we are persisting data in a piece-wise fashion, it means that the transaction cannot be controlled in the service objects, otherwise, we would get nested transactions, which I don't believe are valid (at this point). So, where does the transaction control go?
In order for a transaction to be useful, it has to wrap around all related persistence calls. But, at the same time, we want this to be in a service layer and not part of the controller. The controller should not be concerned with such things. Ideally we would want the Commit() method to be wrapped in a CFTransaction tag only when it is the top-level persistent mechanism and not when it is a nested persistence mechanism.
What this means is that we want PhotoGalleryService.Commit() to be transactional, but not its nested PhotoService.Commit() method; however, at the same time, if we want to ever persist a Photo as the top level item, we do want the PhotoService.Commit() method to be transactional. To create this functionality, I am going to create additional service objects (as needed) that will extend existing service objects but add transactional values. So, for example, we have:
PhotoGalleryService
... and I need that to be transactional most (all) of the time. As such, I will create another object:
TransactionalPhotoGalleryService
... which extends PhotoGalleryService, but overrides the persistence methods (only) like this (pseudo-code):
function Commit( PhotoGallery ){
START Transaction;
SUPER.Commit( PhotoGallery )
END Transaction;
}
Then, internally, all service objects would only ever call the non-transactional persistence versions since only the top-level one will need to have the transaction.
The only question at that point becomes, who decides to use the transactional vs. the non-transaction objects? I have to believe that that is the Controller. The Controller should always use the transactional versions of Service objects.
Hmmm, I really don't like that. From what others have told me, the Controller should have almost no job at all other than to take and hand off requests; and now, I am asking the Controller to decide something that may or may not lead to data corruption. I guess that's a huge red flag. In my previous post, Seth Feldkamp raised the question of having a larger Service layer, almost like a Facade rather than just small service objects. Perhaps this is the type of thing that could both handle the transactions and be exposed to the Controller layer.
Am I getting too far away from object oriented programming with this concern? I have to take a moment and acknowledge that OOP (object oriented programming) and MVC (model-view-controller) programming are not the same thing. One can exist without the other. The Controller is part of the MVC camp, not part of the OOP camp. As such, my stress over what the Controller can or cannot see is not an OOP concern - it is an MVC concern; it's a page flow architecture concern. Let's not lose sight of the goal - I am here to get better at OOP specifically, not MVC. As such, I am gonna slightly brush off the Controller concerns above and just keep moving forward.
So, for now, keeping with this idea of a transaction service object, we can add this object:
TransactionalPhotoGalleryService (ext. PhotoGalleryService)
--------------------------
+ Commit() :: PhotoGallery
+ Delete() :: PhotoGallery
Because this stuff is taking me a long time to think out, I am not making it as far as I would like in each post. Just about time to start working again. And so, as a final thought, I am reminded of Brian Kotek's comment from my very first OOPhoto post in which he described a strategy that would make this application's logic swappable for others:
I would recommend that you at least follow the service layer pattern to create a stable API to your model. That way it will be easier for me or others to plug in alternate implementations, as well as easy for someone to write alternate UI for it (AJAX or Flex for example). You may already have been planning to do this but I just thought I would mention it.
When I read this comment again, I am beginning to think that my "service objects" are not and should not be my "service layer". If I had another layer that sat above my service objects, not only would it create a more stable API as Brian is describing in his comment, but it would take care of my transactional concerns above. So what is this service layer? Is it a facade to the underlying service objects? Does it handle model validation pre-persistence? Is it the only thing that the Controller should be in contact with?
I have made some progress with this post, but unfortunately, I am closing with more questions than answers.
Want to use code from this post? Check out the license.
Reader Comments
NOW you see the madness that lurks beneath the shiny exterior of OOP. If you're ever read "The Call of Cthulhu" by H.P. Lovecraft, this is the point where a mountain-sized alien demigod lurches out of his cave. One man goes insane and dies instantly just from looking at it. ;-)
Handling transactions in the service layer is fine, that's what it is there for: to provide a stable API to the model and to handle behavior that requires orchestration across multiple different objects. Saving a bunch of objects is one of those places.
Yes, your red flag about the controller know having to know about transactions in the model is a good instinct.
I don't think you need two "service layers". One should be fine. Remember that the services should also be as dumb as possible. When folks drift towards a "fat" service layer, it usually indicates "dumb" domain objects, or the Anemic Domain Model antipattern. It usually ends up with a lot of procedural code in the services.
@Brian,
Why does OOP hate me so much :)
I had a thought - as I was coming up with that stuff above, I realized that I think so much from an HTML stand point that it might be clouding my view. I thought, maybe it would be more effective to design the application as if you were writing it for FLEX, Air, or other 3rd party system. While there is very little difference here, I think the mentality would be different enough to promote a different way of thinking about how to implement the code.
But, this begs the question, does the HTML controller and the FLEX controller talk to the same service layer?
Yes, the Model, including the Service layer, should be UI neutral. Meaning an HTML app and a Flex app would both call the same Service layer. However, for Flex apps, web service calls, and AJAX, a Remote Proxy is usually used, which take incoming calls and forwards them on to the underlying Service layer. It may expose only certain methods as "remote", and may apply additional processing to the call such as translation to JSON, converting data into alternate formats, etc.
Basically, the Model itself should have no need to know what is calling it.
@Brian,
Ok cool. So it would be something like:
FLEX -> RemoteFacade -> Service Layer -> .....
HTML -> Service Layer -> ....
Pretty much . . .
FLEX -> RemoteFacade -> Service Layer -> .....
HTML Request -> HTML Controller -> Service Layer -> ....
@Peter,
Ok cool. I just lumped my HTML request and controller together as that mentality is so ingrained.
So, it seems that it might be easier to come up with the API for the application first, and then the actual model second. Thoughts?
Best practice is to start with user stories, filling them in with specific scenarios that you can code against (coding by example).
Start with a story: as a ROLE I want FEATURE so that BENEFIT - e.g. as a REGISTERED USER I want to be able to upload a PHOTO so that I can SHARE IT WITH A FRIEND.
Then come up with a scenario - Bill wants to upload photo of g/f sharon. Uploads photo, enters description and all goes well.
Then personally I'd start with the first screen, get it to display right message and get the controller and the view screens flowing, mocking out the service calls. Then once you have the controller and the view kinda working I'd write a service class to allow the controller and view to work. That kind of outside in API development is almost always the most efficient as it allows you to develop the exact API you need rather than worrying about the perfect API. It's kinda like TDD only it's scenario driven development, doing the simplest thing that works and interative;y adding refinements so you never go very long without working code.
I just wanted to say that I have been reading this series on building your first OOP and I must say that Ben is on the right track. Don't get frustrated this is really good stuff you have been exploring and learning. You are making great progress and will continue to get better. A lot of the questions you have raised in these points are the same questions that I ran into at one point or another. Luckily you have lots of smart people helping you through them but your own exploration is key as well. It just takes some experience to get through. Evenually you might want to consider building a vertical slice of the app (like creating a new photo album but without worring about all the relationships) to see how things pan out.
Ben,
Check out Barney's Transaction Advice for ColdSpring.
http://www.barneyb.com/barneyblog/2006/10/22/transaction-advice/
It will let you make any methods in your service layer transactional and prevent nested transactions for you. In fact, you don't even need to place CFTRANSACTION tags in your code anywhere. It's pretty brilliant.
What you're doing with the TransactionalService flavors for each "Base" service is similar to what this is doing under the hood though.
The TransactionAdvice is useful but is probably going too far for what Ben is trying to do here, which is get a grasp of OO in general. Throwing ColdSpring and AOP into the mix is only going to complicate things at this point. In addition, one of the best ways to understand the need and use for something (like this Advice) is to build an app without it and then see where the pain points are. At that time, the use case for the Advice may make more sense since the problem being solved will be more evident.
@Brian: Agreed. I was just throwing it out there as something to keep in mind.
@All,
Again, thanks for the great feedback. I think I am gonna take a few minutes to create a nice graphic for my most recent understanding. I like seeing things visually and plus, my brain is a bit fried by this time :)
My new visual understanding of the "bigger picture" application:
www.bennadel.com/index.cfm?dax=blog:1286.view