Photo from Chile

How I Use Transfer - Part V - A Concrete Service Object

I was going to describe my Abstract Gateway Object in this post, but during a conversation with a fellow developer it was suggested that I should take a moment to describe a Concrete Service Object, as there was still a bit of confusion in his mind about how I use the Abstract Service Object.

To recap a bit, I have an Abstract Service Object and it is used as an extension point for most of my Concrete Service Objects. Perhaps a bit more of a definition is in order.

  • The Abstract Service Object
  • Is never instantiated as an object.
  • Cannot be used as is.
  • Is only ever used as a base object for Concrete Service Objects.
  • There is only one Abstract Service Object, called AbstractService.cfc.
  • Does not have any Transfer classes associated with it.
  • Concrete Service Objects
  • Are instantiated as objects.
  • Methods on them are called by Controllers, other Concrete Service Objects and Business Objects.
  • Most extend AbstractService.cfc.
  • There are many Concrete Service Objects, e.g., UserService.cfc, ProductService.cfc, ReviewService.cfc, etc.
  • Have one "main" Transfer class associated with them, but can interact with others via code specific to the Concrete Service.

I'll digress for a moment to discuss the comment that "Most extend AbstractService.cfc." Really, the Abstract Service Object is a starting point for all Service Objects that persist their data in a database, like UserService, ProductService, etc. If a Service Object does not persist data in a database, it really gains nothing by extending AbstractService. For example, I have a CartService Object, which only persists data in the session scope. Therefore my CartService Object does not extend AbstractService.

Let's take a look at an example of a Concrete Service Object, ReviewService.cfc. To start, here's the Bean definition of this service from my Coldspring config file:

view plain print about
1<bean id="ReviewService" class="model.service.ReviewService">
2    <property name="TransferClassName"><value>product.review</value></property>
3    <property name="EntityDesc"><value>Review</value></property>
4    <property name="TheGateway">
5        <ref bean="ReviewGateway" />
6    </property>
7</bean>

In here I indicate that the main Transfer class with which this service interacts is product.service. That means that calls to Get(), Update() and Delete() will be directed at the table defined to Transfer as product.review. I can write additional methods in my service that will interact with other Transfer classes, but the default methods, inherited from the AbstractService, will be directed at product.review.

I also indicate that the description of the main class is Review. That will be used for UI messages (e.g., "The Review has been updated"). And I specify that the ReviewGateway, which is defined in a separate Coldspring bean, is the default Gateway Object for this service. That means that calls to GetList() will be directed at that Gateway Object.

And here's the code for ReviewService.cfc:

view plain print about
1<cfcomponent displayname="ReviewService" output="false" hint="I am the service layer component for the Review model." extends="AbstractService">
2
3<cffunction name="Get" access="Public" returntype="any" output="false" hint="I override the abstract get in order to determine the proper ReviewId for a member.">
4    <cfargument name="theId" type="any" required="yes" />
5    <cfargument name="needsClone" type="any" required="false" default="false" />
6    <cfargument name="args" type="any" required="no" default="" />
7
8    <cfset var TQL = "" />
9    <cfset var TQuery = "" />
10    <cfset var qryReview = "" />
11    <cfset var ReviewId = 0 />
12    
13    <cfif StructKeyExists(arguments.args,"CurrentUser")>
14        <cfif arguments.args.CurrentUser.IsAdmin()>
15            <cfset ReviewId = arguments.theId />
16        <cfelse>
17            <cfsavecontent variable="TQL">
18                <cfoutput>
19                    SELECT    Review.ReviewId
20                    FROM    product.review AS Review
21                    JOIN    product.product AS Product
22                    JOIN    user.user AS TUser
23                    WHERE    TUser.UserId = :UserId
24                    AND        Product.ProductId = :ProductId
25                </cfoutput>
26            </cfsavecontent>
27            <cfset TQuery = getTransfer().createQuery(TQL) />
28            <cfset TQuery.setParam("UserId",arguments.args.CurrentUser.getUserId()) />
29            <cfset TQuery.setParam("ProductId",arguments.args.ProductId) />
30            <cfset TQuery.setCacheEvaluation(true) />
31            <cfset qryReview = getTransfer().listByQuery(TQuery) />
32            <cfif qryReview.RecordCount>
33                <cfset ReviewId = qryReview.ReviewId />
34            </cfif>
35        </cfif>
36    </cfif>
37
38    <cfreturn super.Get(ReviewId,arguments.needsClone,arguments.args) />
39</cffunction>
40
41</cfcomponent>

No big surprise, there's almost nothing in there! My ReviewService is inheriting GetList(), Update() and Delete() from the AbstractService, as it doesn't have to do anything special in those methods. The only method that I need to override (in fact I'm extending it, not overriding it) is Get().

The issue with Get() is that I have two different algorithms for determining which Review I should return, depending on whether the current user is logged in as an administrator or not. If the user is an administrator then I just return whichever review they requested, as an administrator is able to read and edit all reviews. However, if the user is not an administrator then I must return only their own review.

Because Review is a child of Product, and User is a child of Review:

view plain print about
1<package name="product">
2    <InvalidTag name="product" table="tblProduct" decorator="model.product">
3        <id name="ProductId" type="numeric" />
4        <property name="ProductName" type="string" column="ProductName" />
5        ...
6        <onetomany name="Review" lazy="true">
7             <link to="product.review" column="ProductId"/>
8            <collection type="array">
9                <order property="LastUpdateTimestamp" order="desc" />
10            </collection>
11        </onetomany>
12    </object>
13    <InvalidTag name="review" table="tblReview" decorator="model.review">
14        <id name="ReviewId" type="numeric" />
15        <property name="Rating" type="numeric" column="Rating" />
16        <property name="LastUpdateTimestamp" type="date" column="LastUpdateTimestamp" />
17        ...
18        <manytoone name="User">
19            <link to="user.user" column="UserId"/>
20        </manytoone>
21    </object>
22</package>

I need to use TQL to join the objects together to find the Review that corresponds to the current user, and the ProductId (which is passed in via args). That's what the bulk of the code above is doing. Once I have the proper ReviewId, I then call super.Get() to actually return the Transfer Object. This allows me to use all of the logic that is built in to the Get() method in the AbstractService, so I don't need to duplicate any of that in the ReviewService.

So that's a simple example of a Concrete Service Object that extends my Abstract Service Object. I actually have one "special" Concrete Service Object, called ValueListService, which I use to manage all of my "code" or "lookup" objects. This is used for objects like UserGroup, OrderStatus, ProductCategory, Colour, etc. It too extends AbstractService, but is built itself in an abstract way so that I can use it to manage all of those "code" objects, without having to write a Concrete Service Object for each one. I plan on discussing that in a future post.

In the next installment I'll start looking at the Abstract Gateway Object.

TweetBacks
Comments
Bob, great series so far! I had a couple of comments and opinions about what you're doing here that I wanted to bring up. I may create a blog entry on it if you or anyone else would like me to expand, but in brief:

I'm curious about how much benefit you feel you get by having to specify the name of a specific Transfer object in your ColdSpring config. I mean, you already know that this is a "ReviewService", and you've said that you may need to interact with other Transfer objects by name, so this mixing of a dynamically supplied object name with hardcoded object names would seem to be making things more complex than they might need to be.

My take is that the service layer should be less about wrapping a single Transfer object or database table and more about creating a useful external API for your applications to use.

My opinion would also be that this service is too dependent on Transfer. I try to make my services as "dumb" as possible. This ReviewService is tightly coupled to Transfer and its implementation. Even the method signature is exposing the fact that Transfer is in use with the needsClone argument, meaning stuff "outside" of the service has to "know" about Transfer and under what conditions a clone is required.

I try to keep the database and Transfer-specific stuff down in a Gateway object. Gateways are meant to encapsulate interaction with an external resource, which is just what a database is. That way, if I ever need to switch out the ORM, or remove Transfer completely, I ideally only need to modify one layer of the application (the Gateways).

Lastly, as big a fan as I am of Transfer, I'm less a fan of TQL. It looks to me like the same thing could be done with normal SQL in about half the lines. Since it looks like you prefer the TQL approach, I'd be interested to hear your thoughts on its use compared to straight SQL.

On a parallel note, it appears that you're enforcing some sort of security here within the service by limiting the review by the userID. Security can always be a difficult problem to tackle, but I wondered if you had considered any alternate methods of enforcing your security rules, such as a ColdSpring AOP advice?

Thanks, keep up the good work! I hope Mark is aggregating this into the Transfer Wiki!
# Posted By Brian Kotek | 7/8/08 9:01 AM
Thanks Brian.

For anyone following this thread, this conversation is continued here: http://silverwareconsulting.com/index.cfm/2008...
# Posted By Bob Silverberg | 7/8/08 10:34 AM
Great stuff. Great stuff. I am really liking this series. You are a superb writer as well.
# Posted By John Allen | 7/9/08 10:07 AM
Thanks John, that's always nice to hear.
# Posted By Bob Silverberg | 7/10/08 4:58 AM