Photo from Chile

How I Use Transfer - Part IV - My Abstract Service Object

In the previous post in the series I discussed how I base most of my objects on Abstract Objects, which allows me to eliminate a lot of duplicate code. I then took a look at one method in my AbstractService object to demonstrate this. In this post I'm going to look at the rest of the methods in the AbstractService, so be prepared for a lot of code.

Let's start with the Init() method and the Configure() method:

view plain print about
1<cffunction name="Init" access="Public" returntype="any" output="false" hint="I build a new Service">
2    <cfargument name="AppConfig" type="any" required="true" />
3    <cfset variables.Instance = StructNew() />
4    <cfset variables.Instance.AppConfig = arguments.AppConfig />
5    <cfset Configure() />
6    <cfreturn this />
7</cffunction>
8
9<cffunction name="Configure" access="Public" returntype="void" output="false" hint="I am run by the Init() method">
10</cffunction>

I am using Coldspring to manage all of my singletons, which in my case are all of the Services, Gateways and some Utility Objects, so I provide an Init() method for all of my services, which is called automatically by Coldspring when it creates them.

The AppConfig object that is loaded into the Service during the Init() method contains a bunch of information about how the app is configured, some of which is required by some services. I wanted to avoid having to extend this Init() method in my concrete services, because I was worried that I might accidentally mess up the API, so I created a Configure() method as well. In the AbstractService the Configure() method is empty, so by default it does nothing. In order to have a service perform certain actions when it is created I need to create a Configure() method in the concrete Service Object. This does introduce a tiny bit of overhead, but I felt it was cleaner. Now I can have the only implementation of Init() in the AbstractService, and work with the Configure() method as needed.

We looked at the Get() method in the previous article, so that just leaves GetList, Update, Delete and some accessors that are used by Coldspring to compose the service. Let's walk through those, as they'll crop up when we look at the other methods:

view plain print about
1<cffunction name="getTransferClassName" access="public" output="false" returntype="any">
2    <cfreturn variables.Instance.TransferClassName />
3</cffunction>
4<cffunction name="setTransferClassName" returntype="void" access="public" output="false">
5    <cfargument name="TransferClassName" type="any" required="true" />
6    <cfset variables.Instance.TransferClassName = arguments.TransferClassName />
7</cffunction>
8
9<cffunction name="getEntityDesc" access="public" output="false" returntype="any">
10    <cfreturn variables.Instance.EntityDesc />
11</cffunction>
12<cffunction name="setEntityDesc" returntype="void" access="public" output="false">
13    <cfargument name="EntityDesc" type="any" required="true" />
14    <cfset variables.Instance.EntityDesc = arguments.EntityDesc />
15</cffunction>
16
17<cffunction name="getTheGateway" access="public" output="false" returntype="any">
18    <cfreturn variables.Instance.TheGateway />
19</cffunction>
20<cffunction name="setTheGateway" returntype="void" access="public" output="false">
21    <cfargument name="TheGateway" type="any" required="true" />
22    <cfset variables.Instance.TheGateway = arguments.TheGateway />
23</cffunction>
24
25<cffunction name="getTransfer" access="public" output="false" returntype="any">
26    <cfreturn variables.Instance.Transfer />
27</cffunction>
28<cffunction name="setTransfer" returntype="void" access="public" output="false">
29    <cfargument name="transfer" type="any" required="true" />
30    <cfset variables.Instance.Transfer = arguments.Transfer />
31</cffunction>
32
33<cffunction name="getTransientFactory" access="public" output="false" returntype="any">
34    <cfreturn variables.Instance.TransientFactory />
35</cffunction>
36<cffunction name="setTransientFactory" returntype="void" access="public" output="false">
37    <cfargument name="TransientFactory" type="any" required="true" />
38    <cfset variables.Instance.TransientFactory = arguments.TransientFactory />
39</cffunction>
40
41<cffunction name="getEmailService" access="public" output="false" returntype="any">
42    <cfreturn variables.Instance.EmailService />
43</cffunction>
44<cffunction name="setEmailService" returntype="void" access="public" output="false">
45    <cfargument name="EmailService" type="any" required="true" />
46    <cfset variables.Instance.EmailService = arguments.EmailService />
47</cffunction>
48
49<cffunction name="getTranslationService" access="public" output="false" returntype="any">
50    <cfreturn variables.Instance.TranslationService />
51</cffunction>
52<cffunction name="setTranslationService" returntype="void" access="public" output="false">
53    <cfargument name="TranslationService" type="any" required="true" />
54    <cfset variables.Instance.TranslationService = arguments.TranslationService />
55</cffunction>
56
57<cffunction name="getFileSystem" access="public" output="false" returntype="any">
58    <cfreturn variables.Instance.FileSystem />
59</cffunction>
60<cffunction name="setFileSystem" returntype="void" access="public" output="false">
61    <cfargument name="FileSystem" type="any" required="true" />
62    <cfset variables.Instance.FileSystem = arguments.FileSystem />
63</cffunction>
64
65<cffunction name="getAppConfig" access="public" output="false" returntype="any">
66    <cfreturn variables.Instance.AppConfig />
67</cffunction>

Let's briefly go over these values and objects that are composed into every service. Values for the first three are specified in the Coldspring config file, while the rest are autowired:

  • TransferClassName - This is the classname (Package.Name) that must be used to identify the main entity to Transfer. For example, for the UserService this would be user.user. We saw an example of the use of this property in the Get() method described in the previous article.
  • EntityDesc - This is a user-friendly description of the main entity (e.g, User). This is used in messages generated by the service.
  • TheGateway - This is a Gateway Object that provides queries for the main entity (e.g., UserGateway).
  • Transfer - This is the high level Transfer object from the TransferFactory.
  • TransientFactory - This is a factory that I wrote that creates Transient objects for me. As I only have a few transient objects that need to be created, I just have one TransientFactory which creates them all. If a service needs to create a transient it uses this factory.
  • EmailService - If a service needs to send an email, it can do so via the composed EmailService.
  • TranslationService - If a service needs to translate from one language to another (e.g., for user messages), it can do so via the composed TranslationService.
  • FileSystem - Interaction with the file system (e.g., file uploads, copying, etc.), is achieved via the composed FileSystem object.
  • AppConfig - I have chosen to access all of my private variables via getters. So rather than referring to variables.Instance.AppConfig, I just use getAppConfig(). I know there has been a lot of debate about this approach, and I believe that it's really a matter of personal preference, and this is what I prefer.

OK, so now we've seen what makes up the AbstractService, let's take a look at the remaining methods:

view plain print about
1<cffunction name="GetList" access="public" output="false" returntype="query" hint="Gets a default listing from the default gateway">
2    <cfargument name="args" type="struct" required="yes" hint="Pass in the attributes structure.">
3    <cfreturn getTheGateway().getList(argumentCollection=arguments.args) />
4</cffunction>

I am using this model with Fusebox, so all of the data supplied by the user (e.g., Form and URL variables) will be sitting in the attributes scope, so that's what I pass into this method. The args argument will contain any criteria specified for the listing. Then all I do is pass that criteria to the getList() method of the default Gateway. I don't override this method in any of my Service Objects, rather, the specific implementation is dealt with by different getList() methods in the Gateway Objects themselves.

The Delete() method is pretty simple, so let's look at that next:

view plain print about
1<cffunction name="Delete" access="public" output="false" returntype="struct" hint="Used to Delete a record">
2    <cfargument name="theId" type="any" required="yes" hint="The Id of the record to delete.">
3    
4    <cfset var ReturnStruct = StructNew() />
5    <cfset var theTO = get(arguments.theId) />
6    <cfif theTO.getIsPersisted()>
7        <cfset theTO.delete() />
8        <cfset ReturnStruct.sScreenMessage = "OK, the #getEntityDesc()# has been deleted.">
9    </cfif>
10    <cfreturn ReturnStruct>
11</cffunction>

I start by getting the Business Object requested, which I do by using the Get() method of the service which will return a Transfer Object to me. If a corresponding record exists in the database I then ask the object to delete itself and I set a message to display to the user. Note that this does not deal with any sort of cascading deletes. If I need that functionality I override this method in my concrete Service Object.

Note that I have recently refactored this method to push logic down into the Business Object. It used to look like this:

view plain print about
1<cffunction name="Delete" access="public" output="false" returntype="struct" hint="Used to Delete a record">
2    <cfargument name="theId" type="any" required="yes" hint="The Id of the record to delete.">
3
4    <cfset var ReturnStruct = StructNew() />
5    <cfset var theTO = get(arguments.theId) />
6    <cfif theTO.getIsPersisted()>
7        <cfset getTransfer().delete(theTO) />
8        <cfset ReturnStruct.sScreenMessage = "OK, the #getEntityDesc()# has been deleted.">
9    </cfif>
10    <cfreturn ReturnStruct>
11</cffunction>

I know that there has also been a lot of debate about which of the above two methods is preferable, and I personally prefer the first example. One of the advantages I see in this is that it allows me to minimize direct references to Transfer in the service layer.

That leaves just the Update() method. Honestly, I'm not that pleased with the design of this method. It works very well, doing exactly what I need it to do, but it definitely is much more procedural than I ultimately want it to be. I have a number of ideas about how I can improve it, but have not yet taken the time to refactor it. I guess due to time constraints this is one of those "if it ain't broke" situations.

view plain print about
1<cffunction name="Update" access="public" output="false" returntype="struct" hint="Used to add or update a record.">
2    <cfargument name="theId" type="any" required="yes" hint="The Id of the record.">
3    <cfargument name="args" type="struct" required="yes" hint="Pass in the attributes structure.">
4    <cfargument name="Context" type="any" required="no" default="" />
5    
6    <cfset var ReturnStruct = StructNew() />
7    <cfset var sAction = "updated" />
8    <cfset var theTO = Get(arguments.theId,true,arguments.args) />
9    <cfset var Upload = 0 />
10    <cfset var fld = 0 />
11    <cfset var varName = 0 />
12    <cfset ReturnStruct.sScreenMessage = "" />
13    <cfif NOT ArrayLen(arguments.args.Errors)>
14        <!--- datatype validations passed --->
15        <cfset theTO.validate(arguments.args,arguments.Context)>
16        <cfif NOT ArrayLen(arguments.args.Errors)>
17            <!--- Business rule validations passed --->
18            <cfif NOT Val(arguments.theId)>
19                <cfset theTO.onNewProcessing(arguments.args) />
20                <cfset sAction = "added">
21            </cfif>
22            <cfif StructKeyExists(arguments.args,"FieldNames") AND arguments.args.FieldNames CONTAINS "UploadFile">
23                <cfloop collection="#form#" item="fld">
24                    <cfif ListFirst(fld,"_") EQ "UploadFile" AND Len(form[fld])>
25                        <cfset Upload = getFileSystem().upload(getDestination(),fld) />
26                        <cfif Upload.getSuccess()>
27                            <cfset varName = ListLast(fld,"_") />
28                            <cfif StructKeyExists(theTO,"set" & varName)>
29                                <cfinvoke component="#theTO#" method="set#varName#">
30                                    <cfinvokeargument name="#varName#" value="#Upload.getServerFile()#" />
31                                </cfinvoke>
32                            </cfif>
33                        <cfelse>
34                            <cfset ArrayAppend(arguments.args.Warnings,"A file upload was unsuccessful.") />
35                        </cfif>
36                    </cfif>
37                </cfloop>
38            </cfif>
39            <cfset theTO.save() />
40            <cfset ReturnStruct.sScreenMessage = "OK, the #getEntityDesc()# has been #sAction#." />
41        </cfif>
42    </cfif>
43    <cfset ReturnStruct.theTO = theTO />
44    <cfreturn ReturnStruct>
45</cffunction>

I pass in the Id of the entity, as well as a structure that contains all of the values that should be populated into the object. I'm also passing in a context, which is used for validations inside the decorator. This is an idea that I picked up from Paul Marcotte.

Once again, I'm getting my Business Object using the service's Get() method, but this time I'm passing in a struct of values to use to populate the object. I'm also telling it (via the second argument) that I need a cloned object back. I'm doing that because I'm going to be putting these values into the Transfer Object before I've had a chance to validate them against business rules. I don't want Transfer's cache to reflect these new values until the object is saved, so I use a cloned object.

I have an array of errors, which is stored in attributes.Errors outside of the model. This therefore is passed in as arguments.args.Errors, and that array gets updated any time an error is encountered. During the population of the Transfer Object, which occurs inside of the Get() method, I check for any datatype errors, adding them to the array. Hence the check for ArrayLen(arguments.args.Errors) before proceeding.

If there were no datatype errors, I ask the Business Object to validate itself, passing in the context. This allows me to define different sets of validations in the Business Object for different scenarios. Once again, if arguments.args.Errors is empty I can continue.

Next I'm checking to see whether I'm adding a new record or updating an existing record. If the Id passed in is blank or zero, then, according to my business rules, it's a new record, so I ask the Business Object to do any processing that is required for new records. An example of this would be to set the status of a new Review to "Submitted". In my AbstractTransferDecorator this onNewProcessing() method is empty, so by default nothing happens. If I need a Business Object to perform some special processing on new records I add that method into the concrete decorator.

I originally tried to use the BeforeCreate event of Transfer's Event Model for this, but the processing that I wanted to do required data submitted by the user, and the Event Model does not provide a way to pass that in. That is why this method gets passed the args argument, which contains all of the data submitted by the user.

I often need to upload files when adding or updating a record, so I've included logic to do that as well. This is an area that definitely needs work from a design perspective. It works very much by convention. Any File input fields will be called "UploadFile_xxx", where xxx is the name of the property in which I want to store the file name. For example, if I have a Product and I need to upload two image files, the names of which should be stored in properties called ImageSmall and ImageLarge, my two File inputs would be called UploadFile_ImageSmall and UploadFile_ImageLarge.

For any FileUpload fields that I find, I call the upload() method of my composed FileSystem object, which will attempt to upload the file, and report back success or failure. If it was successful, I attempt to set properties in my Business Object as described in the paragraph above. If it is not successful I simply add a generic warning message to be displayed to the user. This error reporting is not ideal, but isn't really an issue for any of the apps with which I'm using this.

Finally I ask the Business Object to save itself and then create a message to display to the user.

And that's pretty much it for my AbstractService. I'd say that the majority of my concrete services inherit these methods from the AbstractService, so that saves me a lot of coding, and also encapsulates the logic nicely. For any situations where this generic logic does not meet my business requirements I simply extend (via super) or override the method in my concrete service.

This was a good exercise for me as going through all of this has raised some questions in my mind and given me a few ideas. I'll present them here, and would be happy to hear any feedback on them:

  1. How much of this logic could I actually push into the Business Object itself via the decorator? All of these examples deal with a single business object, so it seems possible, but often the cases where I override these methods require interaction with multiple business objects, which makes me think the service is the best place for them.
  2. I'm wondering about my adoption of the Configure() method. Does my reasoning make sense, or should I just be extending Init() where needed?
  3. I think I should move the logic for processing the file uploads into a separate object - perhaps the FileSystem object. I could just pass the full structure in and allow it to do the looping and extraction.
  4. I have some ideas for improving the way I do validations as well, many of which are thanks to Brian Kotek and Paul Marcotte. The internals of the validations are actually in the decorator, so we haven't seen them yet, but those changes will also impact the way the Update() method looks down the road - there will be less conditional logic.

Whew, that was a long post. If you're still reading, congratulations and thanks! In the next installment I'll take it easy and look at my AbstractGateway.

TweetBacks
Comments
Bob, first off, awesome posts, my friend. I am greatly enjoying following the series. I do have one question though:

The way I read Part III and Part IV, it seems as though you have a 1:1 ratio between Service and Business Object. Is that correct? If so, is there any particular reason (other than simplicity in inheritance with your AbstractService) why you do not handle related Business Objects within the same Service? [e.g. a UserService might handle Users as well as UserEmailAddress(es)]

Okay, so I guess it was more than one question. ;-)
# Posted By Matt Quackenbush | 7/7/08 6:12 PM
Matt, funny you should ask this, as another developer asked me the same question a couple of days ago, so obviously I wasn't as clear as I intended.

No, there is definitely not a 1:1 relationship. I did specify that in article #2, but I guess that was a lot of text ago ;-)

I create one service per subject area, much as your question suggests. So UserService might interact with a User object, an Address object, a userGroup object, etc. I'm guessing that the confusion comes from the fact that I'm passing one specific Transfer class into the service via Coldspring. That object is what I'm calling the "main" object. It is the one that can be potentially manipulated via the default methods in the AbstractService, but I'm not limited to that. I can, and do, create all kinds of additional methods in my concrete services that interact with other business objects.

The AbstractService isn't meant to be the be all and end all, it's just a starting point for my other services. I still end up having to write a lot of code in my concrete services.

I actually have a specialized service object that I haven't even discussed yet that deals with all of my "code table" objects. Things like UserGroup, OrderStatus, ProductCategory, etc. It is more abstracted so that it allows me to manipulate all of those object types via a single service. But that's a post I was going to save for much later.

Based on the discussion I alluded to above, and also prompted by your question, I'm actually in the process of composing a new post about Concrete Service Objects. I had intended to move right on to the Abstract Gateway, but it seems that I've not been totally successful in communicating how the Abstract Service is used yet.

Thanks again for the feedback and the push to be clearer.
# Posted By Bob Silverberg | 7/7/08 7:06 PM
Heh. My bad. Now that you mention it again, I do remember you mentioning that in Part II. That said, don't beat yourself up too much over clarity or perceived lack thereof. Some of us just don't remember things very well! ;-)
# Posted By Matt Quackenbush | 7/7/08 9:50 PM
Great series.

I could be mistaken but I think you made your own case for the problem of relying on conventions for logic when you interchanged UploadFile_xxx with FileUpload_xxx. ;)

Not trying to be nit-picky but thought it was funny
# Posted By Dustin | 2/20/09 5:21 PM
Good catch Dustin. I've fixed that typo ;-)
# Posted By Bob Silverberg | 3/4/09 4:25 PM
NB here...maybe I'm biting off more than I should chew. Up to now I've done pretty much procedural CF. I've installed ColdSpring, ModelGlue, and Transfer all at once.



I should define AbstractService in Coldspring as follows?

<alias alias="ormAdapter" name="ormAdapter.Transfer" />
<alias alias="ormService" name="ormService.Transfer" />

<bean id="AbstractService" class="logon.model.service.AbstractService">
<property name="Transfer">
<ref bean="ormAdapter" />
</property>
</bean>

Am I in the ball park?
# Posted By Joe | 4/22/09 2:08 PM
Sorry for these petty posts, but I can't seem to wire the transfer bean to AbstractGateway

Element INSTANCE.TRANSFER is undefined in VARIABLES.
# Posted By Joe | 4/22/09 6:52 PM
Hmm, well, these posts are actually pretty old, and I'm now doing things a bit differently. So I'd encourage you to look at some of the newer posts, specifically the ones that start with "How I Use Transfer Today".

Regarding your problems with the AbstractService, it seems that you are trying to instantiate an instance of the Abstract object, but it's not meant to be used that way. You are meant to create a concrete object that extends the abstract object and then instantiate that. If you've only read as far as this post you'll see that I discuss abstract vs. concrete objects in the next post in the series.
# Posted By Bob Silverberg | 4/22/09 8:05 PM
Oh, one more thing - looks like you're trying to use this with Model-Glue, which is possible, but not what I'm describing in these posts. You cannot pass the ormAdapter bean into an argument that expects Transfer as ormAdapter.Transfer is not Transfer. It's a wrapper for Transfer.
# Posted By Bob Silverberg | 4/22/09 8:09 PM
Thanks Bob, you were right...when I instantiated the concrete object some things started working.

The problem is that I still don't understand how to wire this thing up in coldspring.

Actually, the REAL problem is that I've been a procedural CF programmer for years and am trying to tackle ColdSpring, ModelGlue and Transfer all at once...but i digress.

I think I need to create a bean reference as so:

<bean id="TransferFactory" class="transfer.TransferFactory" />

and then wire up the concrete objects with the bean above bean. What do you think is the best practice for this?

Joe
# Posted By Joe | 5/3/09 7:20 PM
Joe, I would have to agree that you may continue to struggle because you're trying to take on so much at one time. I think it would benefit you to take the frameworks on one at a time, and don't move on to the next one until you've got a good understanding of how to use each one.

You may want to start with Transfer, as it can easily be used in isolation. Take a look at the tBlog sample application, which shows off most of the features of Transfer, and isn't using any other frameworks. When you think you understand what's happening in that sample app, try using Transfer in an existing project, to see if you really got it.

Otherwise you may end up with a "recipe book", in which you know which pieces to put where, but don't really understand what they're doing or why. That will make it very difficult for you to expand your knowledge.

I know this isn't the answer you're looking for, but it's the advice that I'd give to someone in your position.
# Posted By Bob Silverberg | 5/5/09 5:39 PM
Thanks Bob,

I am climbing this learning curve ever so slowly. I don't know if I've mentioned this, but your work is really helpful, and much appreciated.

I am using tBlog as my case study...I'm learn pretty quickly, but admittedly tend to rely on the "recipe book" mentality a bit too often.
# Posted By Joe | 5/6/09 2:17 AM