Hi all,
There has been a lot of talk recently about building standard components that present collections in one way or another, such as tables, trees, tab panes, etc. I would like to share some of my thoughts about the issues that involve developing truly reusable components of this kind. It seems to me that anyone who wants to develop something like that must ask the questions below and select the answers he finds appropriate. I am looking forward to your comments as to whether you think my approaches are correct.
Issue 1: Data and state separation
The components of this type typically deal with two types information:
- data to be displayed (e.g. table rows)
- state of the visualization (e.g. page to be displayed and sorting order for a table, expanded nodes for a tree, etc.)
It is logical to separate these two, but one has to keep in mind that they are often interrelated in some ways. For example, when new elements are added to a table, their positions as perceived by the user must depend on the current sorting order. In order to achieve efficiency, therefore, either the data part has to know what the state is, or the state must know that new entries have been added to the data. One reasonable solution to this problem seems the following:
a) The data part and the state part are separate, but the state part ties in to the data part as a listener. When an entry is added or deleted, the state part is notified and takes some appropriate action (this is what Swing does)
A somewhat more general approach to do this is also possible as well:
b) The data and state present a unified interface for both managing data and managing state. What happens when you change state or data is a matter of the implementation. The implementation could provide separation in a way mentioned above, or it could be an entirely monolithic one � depends on the circumstances.
The first approach is cleaner, but the second approach allows much closer interaction between state and data, and in some cases that is quite necessary. For example, consider a table that gets its data at every request from the database, and performs the sorting and paging using the ORDER BY and LIMIT clauses in SQL � that would be hard to implement using the first approach, but would work fine using the second. Hence, my suggestion here is to define an interface using the second approach, and create an implementation of that interface using the first. In this way one can get the best of both worlds � freedom and cleanliness.
Issue 2: What to save in the session
The Web interface presents a very different set of challenges than the GUI one does. The biggest one is probably how to deal with information between requests (something that Tapestry actually excels at, but this is another topic). There is a trade off here not present in the GUI apps � storing more information in the session makes life easier, but may increase the memory consumption and decrease the performance of the application. Throw in clustering, where you have to keep the session synchronized among several machines, and you�ve got a problem.
To cut to the chase, I see four typical ways to save information in the session:
1) Save everything � both data and state. This is probably the easiest approach, but it may lead to significant resource consumption.
2) Save state and IDs of data � a good hybrid approach that often works well
3) Save state only � may lead to performance issues when there are interactions between data and state, such as sorting, but would work well when there are no such interactions.
4) Save nothing � this may make sense when the state is derived from the actions that are being undertaken. One such example is a tab pane where pages know under which tab they should be.
Needless to say, each of these approaches is suitable for a particular type of situation. For example:
- If you want to display small tables, and you want a quick �fire and forget� mechanism, approach (1) may certainly be easiest.
- If you know that the data in the DB will not change, and you want to minimize performance costs for adjusting to the state (e.g. due to sorting), approach (2) may be the best
- If you want to display the changes in the DB in real time, approach (3) may be ideal
- For (4) please see the example given with it.
- One important case that is not that frequent but does need to be noted is when you need to place stateful (form) components within some entries (and perhaps even all), such as text fields, etc. In cases like that Tapestry performs a rewind, and an imperative of rewind is that it must produce exactly the same page (at least structurally) as the render in the previous RequestCycle of this session, otherwise it is unlikely to work properly. If we are working with a database that may change in the period between the render and the rewind (up to 30 mins in the standard settings), then we must use approach (2), or even perhaps approach (1).
So how to make a common component that could encompass all of these possibilities? The simplest approach is to leave the matters of saving the information to the parent component (page). The parent would certainly know what the best policy is, and can act appropriately. This approach, however, increases the amount of work needed to use the component and in a sense ends up wasting a lot of time.
The alternative that I personally prefer is to delegate the decision of what to be saved via an interface that looks something like this (the example is with a table):
public interface ITableSessionState {
Object getSessionState(ITableModel objModel);
ITableModel recreateModel(Object objSessionState);
}
This interface can then be implemented to suit each of the approaches above, as well as others. Approaches (1), (3), and (4) are very easy to implement and standard objects for them can be provided as part of the package. One of these (say approach (1)) can be chosen as a default, which would eliminate the need for another required parameter. The result is that in the minimal case the component can be used very easily, but the flexibility to accommodate very different and custom scenarios is preserved.
Issue 3: Where to save the session data
There are two obvious answers to this question:
- As a persistent property. This would make things very simple for use, but it essentially requires that the component is used with the same data only on a single page (since the state cannot propagate to other pages). Since some of the components are typically used in this way, the approach may make sense for them.
- Within the Visit. This is obviously the only approach when a single component is used with the same data over several pages (e.g. tab pane). In order for this to work, however, there are a couple of prerequisites: first the component must be provided with an ID via its parameters to locate its data (components that use the same data/state would be given identical IDs); second the Visit must allow saving of such data i.e. via a map. (Alternatively, the data can be saved in the servlet�s session, but then I think one may run into the problem of supporting clustering � read Howard�s recent emails for more info). The easiest and most portable way to do that, I think, is to require the Visit to implement a particular interface that provides an access to a map. I think all Visit objects can easily implement such an interface.
One big question that remains is deciding which component would implement which approach. A possibility is to have components implement both. By default, the first approach would be used, but if an ID is provided via an optional argument, the component would use the second. I feel that this method would allow maximum flexibility.
Issue 4: Providing flexibility for display of data
There are really two competing requirements (again) when deciding how to display the data. On one hand simplicity is desirable, so by default the component needs to be able to just present a string representation of a particular entry. On the other hand, one needs to be able to place components of random complexity in each cell of the table or each node of the tree, etc. A simple way to combine the two is to get the presentation of an entry via a method that returns IRender, the default implementation of which could then use RenderString to present a simple string:
IRender getRender(�) {
return new RenderString(getValue(�).toString());
}
If a more complex presentation is necessary, the method can be overridden to return something other than RenderString, for example a Component. I think Howard made a similar change somewhere in the framework recently, but I cannot remember where at the moment.
This particular issue becomes much more complex when particular components are discussed, since a lot of other questions arise, such as internationalization, how to handle sorting, how to provide PAC-style delegation, etc. I think this document already got too large, so I am not going to get into this at the moment :).
As you have probably guessed already, we have implemented a number of components of this category in our company and I am essentially discussing the issues that we have faced and the solutions that we have found to work. Unfortunately, contributing those would not be practical, since they depend heavily on our libraries and infrastructure (e.g. we have our own methods for internationalization, a standard implementation of Visits, validators that we interface with Tapestry via adaptors, PAC infrastructure, etc.).
My goal instead is for us to discuss and possibly agree on a standard consistent approach for building such components. If we do that, I think we would have most of the work completed � writing code is easy, deciding what code to write is hard J.
I hope all this makes sense, and I hope I have not bored anyone.
Best regards,
-mb
Do You Yahoo!?
Yahoo! Finance - Get real-time stock quotes
