How I Use Transfer - Part VI - My Abstract Gateway Object
Posted At : July 14, 2008 6:35 AM | Posted By : Bob Silverberg
Related Categories: OO Design, How I Use Transfer, ColdFusion, Transfer
In the past few posts in the series I discussed, at length, how I have implemented my service layer, which consists of an Abstract Service Object and Concrete Service Objects. I decided to take it easy this time around and just look at my AbstractGateway Object, as it's pretty simple in comparison.
As with the AbstractService, this object is not meant to be instantiated on its own, but rather acts as a base object which my concrete Gateway Objects extend. There are only three public methods in my AbstractGateway:
- GetList(), which returns a listing for the default entity
- GetActiveList(), which returns a listing for the default entity, but only includes Active records
- ReInitActiveList(), which is used to reinitialize the cached query for the Active List (see below for details)
Here's the Init() method:
2 <cfset variables.Instance = StructNew() />
3 <cfreturn this />
4 </cffunction>
All I do in the Init() method is set up a struct to hold all of my private variables.
GetList() is currently set up to use Transfer's list() method to return a list of all records for the default entity:
2 <cfreturn getTransfer().list(getTransferClassName(),getDescColumn()) />
3 </cffunction>
TransferClassName and DescColumn are specified in the Coldspring config file for each Gateway bean. For example, for the UserGateway TransferClassName is user.user and DescColumn is UserName. I do in fact often override this method, but for the times that I don't need to I'm glad that I have a default method defined in this object.
Most of my Business Objects have a property called ActiveFlag, which is used for logical deletions. I rarely delete anything from the database, rather I set ActiveFlag to false and then it is up to the application to treat those items as deleted. So I often want to generate a list of entities which only includes active records (e.g., to populate a select box), in which case I ask the Gateway to GetActiveList(). Because this is something that I often need, I cache this query in the Gateway itself:
2 <cfif NOT StructKeyExists(variables.Instance,"qryActiveList")>
3 <cfset ReInitActiveList() />
4 </cfif>
5 <cfreturn variables.Instance.qryActiveList>
6 </cffunction>
7
8 <cffunction name="ReInitActiveList" access="public" output="false" returntype="void" hint="ReInitializes the data in the cached query">
9 <cfset variables.Instance.qryActiveList = GetActiveListData() />
10 </cffunction>
11
12 <cffunction name="GetActiveListData" access="private" output="false" returntype="query" hint="Returns a query with all active records in the default table">
13 <cfreturn GetTransfer().listByProperty(getTransferClassName(), "ActiveFlag", 1, getDescColumn()) />
14 </cffunction>
When I call GetActiveList(), it checks to see if the query has already been cached, and if not it requests that it be done via the ReInitActiveList() method. Then it simply returns the cached query. ReInitActiveList() can also be called by the service layer after a database change, to ensure that the cached query remains up to date. I've encapsulated the actual running of the query into its own method, GetActiveListData(), so that I can vary the implementation of ReInitActiveList() and GetActiveListData() independently. For example, in some concrete gateways I may override both methods, while in others I may only override one of those methods. In the AbstractGateway, GetActiveListData() uses Transfer's listByProperty() method to retrieve only Active records, and sorts them by the DescColumn.
And that's pretty much it for the AbstractGateway. Most of the interesting gateway code is in the concrete gateways, an example of which I'll describe in the next post.
The only thing that I might change about this object is to use java.lang.ref.SoftReference for the cache, instead of just caching the query in the gateway (which in turn is cached in the application scope). Transfer's object cache uses soft references, and I have read a couple of interesting articles about this. I'm not sure that this would be an appropriate use of soft references as I don't really want these queries to fall out of the cache. I only cache queries for certain gateways, and the size of those queries is quite limited, so for now I think it's best to leave it as is. If I do go that route I'll be sure to document what I've done in a blog post.
One final note, during the writing of this series I've been evaluating the code base and noticing things that I'd like to change. I've also received a number of useful comments from people that are making me reconsider some things. One change that will probably be coming will be to move a lot of the logic that is currently in my service layer into my gateways. This will allow me to decouple Transfer from the service layer, something that I'm already trying to do to a certain extent (e.g., by the use of the Get() method in the service). For now I'm going to continue documenting what I'm doing at this moment, and when I do start to refactor I'll post about that as well.
I might be being stupid but, in your abstract Service you have a GetList method which has a required 'arg' argument, but your Abstract Gateway doesn't use the argument collection you pass to it.
Abstract Service:
<cffunction name="GetList" access="public" output="false" returntype="query" hint="Gets a default listing from the default gateway">
<cfargument name="args" type="struct" required="yes" hint="Pass in the attributes structure.">
<cfreturn getTheGateway().getList(argumentCollection=arguments.args) />
</cffunction>
Abstract Gateway:
<cffunction name="GetList" access="public" output="false" returntype="query" hint="Returns a query containing all records in the default table, ordered by the DescColumn">
<cfreturn getTransfer().list(getTransferClassName(),getDescColumn()) />
</cffunction>
The reason for this is that a concrete gateway could override the GetList() method and may require that arguments be passed to it, for criteria or sorting for example. The GetList() method in the Abstract Gateway does not need anything passed to it because it just does a simple transfer.list() using default values.
By using argumentCollection in the GetList() method of the Abstract Service I can override GetList() in a concrete gateway without also having to override it in the corresponding concrete service.
Does that make sense?
You make an interesting point though - perhaps I should have included an optional cfargument in the Abstract Gateway for "args", even though it's not used, as that would have made the API clearer. That's just an example of me being a lazy coder.
<cffunction name="GetList"
access="public"
output="false"
returntype="query"
hint="Gets a default listing from the default gateway">
<cfargument name="args"
type="struct"
required="false"
hint="Pass in the attributes structure.">
<cfset var qList = "" />
<cfif StructKeyExists( arguments, "args" )>
<cfset qList = getTheGateway().getList( argumentCollection=arguments.args ) />
<cfelse>
<cfset qList = getTheGateway().getList( ) />
</cfif>
<cfreturn qList />
</cffunction>
I do have another question for you if you don't mind :) In "Part II - Model Architecture" you said that you generally create one Service Object per subject area (like packages in Transfer), which makes sense to me. However, I don't see how you'd do that with the way your concrete service extends your AbstractService.
To use the blog example, if I have a transfer package with 2 objects: post and comment. If I understand correctly (which is rare!) then I'd need to have a PostService, CommentService (both extend AbstractService), PostGateway, CommentGateway (both extend AbstractGateway)and define them all in my Coldspring.xml config.
Sorry if I'm being dense or asking too many questions!
Thanks
1. I always pass a struct into the getList method, because I'm mostly using Fusebox so I just pass in "attributes" from my controller. I don't want to be bothered with knowing whether or not arguments are required, so I always just pass in everything submitted by a user. This allows me to use argumentCollection in my service, again not having to worry about which arguments are required by which gateway.
2. I don't want to have to introduce conditional code such as that in your example. To me, what I'm doing is just much simpler. I don't see a need to complicate it to the degree that you've done.
If I did want to make args optional, I'd just specify a default value of StructNew() on the cfargument tag and then I wouldn't need all of that extra code anyway.
Regarding the other question - I can see how what I've described can be confusing based on the limited examples that I've shown. You are not the first person to question this.
The way I was doing things when I wrote this series was to have a service for a package, and that service would have a "main" object, for which all of the default values would work. If I wanted to work with another object in the same package I'd have to provide arguments to indicate which object I wanted to work with, OR I'd have new methods defined in the concrete service for those "subordinate" objects, OR I'd work with those objects outside of the service entirely with something that I call a ValueListService, which is a generic Service/Gateway pair that is used to work with all of my "value list" objects. Things like UserGroup, Category, Colour, etc - what people often refer to as code tables.
I'm actually in the process of revisiting how my services and gateways are structured and created, and will probably do something similar to what Paul Marcotte has done with Metro (metro.riaforge.org), which involves one service per package, and one gateway per object, all of which can be automatically generated from abstract classes. I'm still not sure exactly how it's all going to work, but I'll write about it once I've got it figured out.
Like you I like having an abstract service which I then extend with my (mostly empty) concrete service. As you say the problem with that is that you have to use generic names for your abstract methods (such as get(), save() etc) rather then getUser(), saveUser(). Personally I like being able to do myUser.save(). However this does mean that my "subordinate" objects need to be called via myUser.savePermissions() etc. I hadn't thought about doing myUser.save('user.Permissions') so thanks for the brain fodder :)
Looking forward to your follow up post!