Create Iterating Domain Objects Using Composition

Composition , Iterator , ColdFusion , LightWire , ColdSpring Add comments
Over the past month, I've worked on some projects that required special data handling and calculated values. I considered using Peter Bell's Iterating Business Object, but instead decided to build an Iterator that can be composed into my domain objects. I'm a big fan of using "composition over inheritance", so I saw this as a prefect opportunity to add iteration to my domain objects with a minimum impact.Iterator provides methods to load ColdFusion queries and traverse records. For instance, if you use constructor injection, your init() method would accept Itertator as an argument. You would then store it in variables scope and add delegate methods to utilize Iterator in your domain object. The following examples assume you are using either ColdSpring or LightWire to inject your object dependencies and that your maintain a reference to either as Application.beanFactory". Here is a snippet of code from a ColdSpring XML bean definition. I currently place Iterator in a generic "util" package, but you could place the file anywhere you like. <bean id="Product" class="model.product.Product" singleton="false">
   <constructor-arg name="iterator">
      <ref bean="Iterator" />
   </constructor-arg>
</bean>
   
<bean id="Iterator" class="util.Iterator" singleton="false" />
And the Product init() method would be: <cffunction name="init" access="public" returntype="model.product.Product" output="false">
   <cfargument name="iterator" type="Iterator" required="true" />
   <cfset variables.iterator = arguments.iterator />
   <cfreturn this />
</cffunction>
The minimum delegate methods required to use Iterator are loadQuery(), hasNext() and next() (rewind() is optional but recommended). Here are the example delegate methods: <cffunction name="loadQuery" access="public" returntype="void" output="false">
   <cfargument name="rs" type="query" required="true">
   <cfargument name="maxRecordsPerPage" type="numeric" required="false">
   <cfset variables.iterator.loadQuery(argumentCollection=arguments) />
</cffunction>

<cffunction name="hasNext" access="public" output="false" returntype="boolean">
   <cfreturn variables.iterator.hasNext() />
</cffunction>
   
<cffunction name="next" access="public" output="false" returntype="void">
   <cfif variables.iterator.hasNext()>
      <cfset variables.instance = variables.iterator.next() />
   </cfif>
</cffunction>
   
<cffunction name="rewind" access="public" output="false" returntype="void">
   <cfset variables.iterator.rewind() />
</cffunction>
How it works The example delegate function next() provides insight into how to use Iterator. Assuming you maintain your domain object properties in variables.instance, calling next() will replace variables.instance with the next record from the loaded query. Let's say you have a list page for a family of "Products" for a given "Category". To display the Product list, you might make a method call to your Service Layer to retrieve a query result and subsequently loop over the query to display the Products. Example using a query: <cfset productList = Application.beanFactory.getBean("ProductService").getProductsByCategory(CategoryId) />

<cfloop query="productList">
   {display code here}
</cfloop>
Using an iterating domain object, you would first create an object instance, then load the query into the domain object. Example using Iterator: <cfset products = Application.beanFactory.getBean("Product") />
<cfset productList = Application.beanFactory.getBean("ProductService").getProductsByCategory(CategoryId) />
<cfset products.loadQuery(productList) />

<cfloop condition="#products.hasNext()#">
   <cfset products.next() />
   {display code here}
</cfloop>
At a glance one might say, "well, the second example has more lines of code." True, but if you has ever had to display totals or other calculated values in your view, you would need to create variables in your views and manage the setting of those variables. To reduce the amount of logic in your views, any calculated values can be encapsulated in the iterating domain object. This facilitates better cohesion and reusability in your code. If you are faced with some business problems that would benefit from using iteration, try Iterator out. [UPDATE] The comments for this entry discuss why the example above is flawed. After a couple of stabs, I got it right. If you don't care to read the entire dialogue, note that you can use an Iterator in place of an array of objects when your objects have smart getters. Or, if you want to use a "rich object map" in place of a mix of objects and queries. An example that better illustrates the use of Iterator follows: Imagine you have a Customer that may have multiple Addresses (Billing, Shipping, HQ). When your Customer logs in to your web site, you persist the Customer object in Session scope. Instead of having an array of Address objects (or a query of the Customer addresses) in your Customer object called "addressList", you could use an Iterating Address object. To display the address list, you would loop over the condition Customer.getAddressList().hasNext().

14 responses to “Create Iterating Domain Objects Using Composition”

  1. Rob Wilkerson Says:
    Hey man -

    Just a quick heads up that your feed links often don't work for me (using Google Reader). The link for this post references /5/9/ rather than /5/10/. Normally I wouldn't post this as a comment since it's utterly irrelevant to the post itself, but I didn't see any other way to reach you. It's happened several times in the past, but I've never paid enough attention to know whether it happens for every single feed item.
  2. Brian Kotek Says:
    Interesting stuff, but regarding the idea of encapsulating something like a grant total of product prices in the domain object, this seems like a bad idea. A Product is supposed to model a single product, that is its purpose. Are you concerned that diluting it with a total of a bunch of products that actually has nothing to do with an individual Product is not proper separation of concerns? I'd say such a total really belongs in, say, a shopping cart object, to which the grand total is much more applicable.

    Am I reading into the specific example too much or are you just willing to reduce the cohesion of your domain object in exhange for the benefits you get from the iterator?
  3. Joe Rinehart Says:
    I was going to leave a comment, but Brian said my thoughts very well.
  4. Paul Marcotte Says:
    Brian and Joe,

    You are absolutely right. My Product list example is off the mark. A more accurate use case would be for reporting things like product views, sales, or returns over a given date range. I was hesitant to draw out a concrete example, but can vouch that for a stats project I'm working on, Iterator is invaluable.

    Part of my intent was to illustrate that, regardless of requiring calculated fields, you can use an iterating domain object in place of a query even for a simple display page. The difference being that your view code would loop over the object while the condition hasNext() returns true and output #objectName.getAttribute()# instead of #queryName.attributeName#.
  5. Brian Kotek Says:
    Again though, maybe I'm missing something but I still don't think I'd agree. In my mind, a Product instance represents a single product. Things like how many times it's been sold, returned, etc. apply to many products, not one. An individual Product can only be returned once or sold once. An aggregation of how many times a particular KIND of product have been sold or returned is something I'd handle with a ProductGateway, or an InventoryService or something like that. It would be like getting into your Honda Accord (an individual instance of an Accord) and asking it how many Accords Honda has sold this year in the US. I'm not trying to be a nitpicker since I know people have different ways of approaching a problem, just voicing my thoughts to see if I'm overlooking something.
  6. Paul Marcotte Says:
    Okay, let's go with a Car example and use Make, Model, Price, Year, MilesDriven and VIN as attributes. I'll pretend to be a used car salesman with a website. When a user searches for a car on my site, they submit a form with the attributes above (with the exception of VIN). To be a bit more realistic, the Year and Price form vars would probably be selects for &quot;newer than&quot; (Year) and &quot;less than&quot; Price.

    To display results on my list page, I'd call CarService.getByAttributes(Form) (assuming my form struct is the same as the arguments of getByAttributes()). Then I call CarList.loadQuery(query).

    In my view, I want to list the Car attributes in a standard table output and a summary field called BestValue which is calculated as (Price*(MilesDriven/TotaMilesDriven))*Age.

    So, if I have a user search for Honda Accords, I might have 20 on my lot. If the total miles of the 20 cars is 2,000,000. The 2001 Accord with 80,000 miles for $6000 is a &quot;better value&quot; than the 1997 Accord with 100,000 miles $4000.

    If the user chooses a car from the list, s/he would click the hyperlink to view the full details (which includes the VIN as a url param). My Car display page calls CarService.getCarByVIN(url.VIN) which returns a single record query and I treat the object as a single car since my iterating domain object can load a query with 1..n records.

    The benefit here is that I can encapsulate my BestValue rating as a single method call (say, getValue()) within my object and keep my view clean. Internally, getValue() would call other calculated methods like getTotalMiles() and getAge().

    Again, I agree my original product example is misleading. I hope this example clarifies the intent for using an Iterator.

    And, yes, I am willing to sacrifice cohesion for iteration. I'd rather have a dumb view and a less cohesive object than vice versa. :)
  7. Brian Kotek Says:
    That does make more sense, and I can see now the reason you took that approach. Personally, I wouldn't do it like that. I'd wrap up the determination of which Car in a set of cars possesses the highest value into an object outside of Car. but to each their own, as always! I agree that dumb views are to be striven for, and I also wouldn't like having this determination made in the view. I just wouldn't implement it in the Car either.
  8. Joe Rinehart Says:
    Paul -

    CarList is definitely more the way to go rather than having a Car be able to list Cars.

    Not trying to nitpick, but I still think we're going to ArchitectureAstronautland where a SQL statement would suffice.
  9. Paul Marcotte Says:
    Gents,

    I'm glad the CarList example is more descriptive. I don't expect many folks will want to climb aboard this starship. Although I must admit, ArchitechtureAstronautland is an interesting place. I'll endeavour to return as an abasssador to alternative object models. Preferably unassimilated by Borg, but I make no promises... ;)
  10. Peter Bell Says:
    Just to jump in here, firstly I agree that I wouldn't put any reporting information within the business object being reported on. You *could* but that in the #ObjectName#Gateway, but I'd be even more tempted to model the report as a separate domain object as per Brians suggestion (perhaps an inventory object).

    The problem as Fowler pointed out with the use of the business object &quot;Car&quot; is that we don't really know what we mean by Car. In the Smalltalk world, Car represents both the platonic ideal of a car and a specific Mint-Green Mini with flared arches. In CF we often copy from the Java approach of having a CarService to represent the concept and a Car to represent our tricked out Mini.

    The reason I started doing Iterating Business objects (a Car object containing a collection of Cars and including iterator functionality) was that very pragmatic. ColdFusion is slow to instantiate objects so it is impractical to create 200 transient car objects just to list 200 cars on a page. You culd just use a query, but what happens if the Car needs smart getters? With a UserList I want to list FullName and Age, but the DB fields are FirstName, LastName and DateofBirth. I find a single iterating business object with smart getters much more usable and elegant than the three main alternatives (calculating the fields in every view screen, looping through the recordset in the service layer to generate the calculated fields or in the general case passing the processing back to the DB using something like SELECT (FirstName + ' ' + LastName) AS FullName which works find for full name but not for much more complex calculations such as discounted prices.

    If I never had business objects with smart getters, I'd stick with looping through recordsets, but even the sites I generate need more sophistication than that, so I use a generalized solution which is the IBO and it has allowed me to generate some fairly complex sites quickly, elegantly and maintainably.
  11. Paul Marcotte Says:
    Peter,

    Thanks for clarifying your intent for an Iterating Business Object. I vaguely recall understanding this when I started out, but obviously got side-tracked by other edge cases. Rookie mistake.

    So, there you have it folks, use an Iterator in place of an array of objects when your objects have smart getters. Or, if you want to use a &quot;rich object map&quot; in place of a mix of objects and queries.

    I'll risk a final example: Imagine you have a Customer that may have multiple Addresses (Billing, Shipping, HQ). When your Customer logs in to your web site, you persist the Customer object in Session scope. Instead of having an array of Address objects (or a query of the Customer addresses) in your Customer object called &quot;addressList&quot;, you could use an Iterating Address object. To display the address list, you would loop over the condition Customer.getAddressList().hasNext().
  12. Peter Bell Says:
    Hi Paul,

    Two comments. Firstly, your address example is spot on. That is how my service layer handles composed objects so if you ask CategoryService.getByID(&quot;12&quot;) it'll return Category and if you want to get the products I call Product = Category.get(&quot;Product&quot;). I've played with the ProductList nomenclature that Joe mentioned, and I am still going back and forward. I like the distinction of ProductList clearly being n-records, but I have some views that need to work for both 1 and n records, so the Jury is still out for me.

    Secondly, I think it is important to hack around and try stuff. Just because Brian, Joe and I might not put our reporting into an iterator, try it. Then find out why others don't do it and make your own mind up. Sometimes we're smarter, sometimes we're just fossilized - trust your experience more than your peers :-&gt;
  13. Austin condos Says:
    Great Work !
  14. Jack Says:
    http://www.twincling.org/node/742

Leave a Reply



Powered by Mango Blog. Design and Icons by N.Design Studio