Saturday, July 15, 2006

Decoupling with DynaBeans

A producer-consumer design is a fairly common pattern in business application programming. A producer module produces some data which the consumer module uses. Decoupling the two modules means that data must flow from the producer to the consumer, and the data produced by the producer must be parseable by the consumer.

A naive implementation may attempt to get around the problem of parseable data by interleaving the producer and consumer code in the same module. This is not too maintainable, since the two modules have to be developed in parallel and are usually very tightly coupled and are not very extensible.

For implementations where the problem space is well defined, the data could be passed as a JavaBean. However, if we are designing an engine where we are able to plug in various implementations of the Producer and Consumer to produce unique data flows, then a logical extension would be to provide various implementations of a bean interface to pass the data between them. In this case, the data bean interface would be simply a marker interface, since there is unlikely to be commonality between data bean implementations to justify specifying methods that they must implement.

However, we know that all implementations are going to be simple data carriers. The Producer will know how to build the bean and the Consumer would know how to parse it. A logical choice in such situations would be to specify that all beans connecting various Producer and Consumer implementations in our engine should implement DynaBeans.

DynaBeans are a unique data structure and are provided by the Jakarta Commons BeanUtils project. Like object implementations in some scripting languages, the DynaBean "object" is really a HashMap. A DynaBean is an instance of a DynaClass, which is created from an array of DynaProperties. The base DynaBean will not allow getting and setting properties which are not declared in the DynaClass. However, you can get this behavior in implementations of MutableDynaBean, such as LazyDynaClass, where DynaProperties can be added as needed. The ResultSetDynaClass and RowSetDynaClass are useful DynaClass implementations that can be used with SQL ResultSets and RowSets.

Assume that the Producer in our system was running an SQL query, and writing out an XML representation of the results. The snippets of code below illustrate how DynaBeans can be used to pass the data over from the Producer to the Consumer. Note that there are no messy unrolling of the ResultSet using ResultSetMetaData and getObject() calls.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class Producer {
    ...
    public void produce() {
        ...
        PreparedStatement ps = conn.executeQuery(sql);
        ResultSet rs = ps.executeQuery();
        ResultSetDynaClass rsdc = new ResultSetDynaClass(rs);
        for (Iterator it = rsdc.iterator(); it.hasNext();) {
            DynaBean row = (DynaBean) it.next();
            consumer.consume(row);
        }
        rs.close();
        ...
    }
}

public class Consumer {
    ...
    public void consume(DynaBean object) {
        DynaProperty[] props = dynaBean.getDynaClass().getDynaProperties();
        for (int i = 0; i < props.length; i++) {
            String key = props[i].getName();
            String value = (dynaBean.get(key) == null ? "" : dynaBean.get(key).toString());
            ostream.write(("<" + key + ">").getBytes());
            ostream.write(value.getBytes());
            ostream.write(("</" + key + ">\n").getBytes());
        }
    }
    ...
}

There are situations where we want to do more processing than run a single SQL query. For example, we could get a foreign key to another table out of the resultset and get additional data from it, and add these new columns to our result. So effectively we will have to add new "member variables" to our DynaBean. For that, we will need to instantiate an instance of a MutableDynaBean such as a LazyDynaBean. The following snippet illustrates this.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
    PreparedStatement ps = conn.prepareStatement(sqlQuery);
    ResultSet rs = ps.executeQuery();
    ResultSetDynaClass rsdc = new ResultSetDynaClass(rs);
    // create our output DynaClass contains an extra property than 
    // that returned from the ResultSet.
    DynaProperty[] props = rsdc.getDynaProperties();
    LazyDynaClass odc = new LazyDynaClass();
    for (int i = 0; i < props.length; i++) {
        odc.add(props[i].getName(), props[i].getType());
    }
    odc.add("extraProperty", String.class);
    // iterate through the ResultSet
    for (Iterator it = rsdc.iterator(); it.hasNext();) {
        DynaBean row = (DynaBean) it.next();
        // clone the data into the mutable DynaBean
        DynaBean orow = odc.newInstance();
        for (int i = 0; i < props.length; i++) {
            orow.set(props[i].getName(), row.get(props[i].getName()));
        }
        orow.set("extraProperty", "foo");
        // do something with orow
        ...
    }
    ps.close();

DynaBeans offer a unique advantage compared to plain JavaBeans. JavaBeans would work if we knew exactly what our Producer produced. However, populating the JavaBean in the Producer means writing a bunch of setXXX() methods, and parsing in the Consumer means writing a bunch of getXXX() methods, which is error prone and high maintenance. We could use introspection on both ends to build and parse the bean, but populating and doing key lookups off a HashMap is faster than introspection.

In some cases the data format may not be fully known, such as when the Producer uses an SQL query to generate its data, and the SQL query is provided to the Producer during runtime. In such cases, the data structure to use would probably be a simple HashMap. DynaBeans provides stronger typing by requiring that the property being set or read be specified in the corresponding DynaClass.

DynaBeans provide a middle ground between the strong type checking of concrete bean implementations and the flexibility of a HashMap. There are some convenience implementations of DynaBean which can help in reducing the amount of repetitive code you have to write. I never had much use for DynaBeans before this, but for this particular application, it seemed tailor made for the job.

On a completely unrelated note, this is the first blog entry I am writing using Opera 9. I recently download and started using it, after I needed a second browser application on my Linux workstation for some work I was doing. I have been using Firefox before this, and I am too new to Opera to be able to compare, but so far, most of the features I use in Firefox seem to be available. Opera does seem to have more keyboard shortcuts.

2 comments:

  1. I like the idea, too. Have you had a look at ibatis (http://ibatis.apache.org/)? This seems to be a complete solution for what you are writing about..

    -Stefan

    ReplyDelete
  2. Hi Stefan, thanks for the comment. And yes, I did use ibatis briefly for one project at a previous job, and was quite impressed with its power and simplicity (kind of a bridge between a full blown ORM and JDBC). Its quite possible that design described in this post may have been (sub-consciously) inspired by ibatis. However, the actual project that spawned this design had database persistence as one of the components of the overall system. In retrospect, I think it may have been preferable to make the database the pivot of this application, in which case, ibatis would probably have been a better choice.

    ReplyDelete

Comments are moderated to prevent spam.