In this article I continue my look at implementing sets of objects.
Concise Custom Constructors
In my last article I set out the broad principles behind implementing sets of objects with a problem domain oriented class to act as a container for the business objects themselves. This relied upon an extended data management class that could support navigation through a database-dependent cursor of some kind. This month concludes this investigation by providing some more implementation details and we see that once the application independent framework has been provided, the actual code required for a real system is very small.
We have already seen the proposed interface for our class that handles the set of business objects, TPDList. This has classic First, Next and IsLast methods that delegate work off to a private data management object, which is constructed according to the needs of the list in question. This is the code for our base TPDList constructor:
constructor TPDList.Create (ListDMObject: TDMObject);
inherited Create; // Standard constructor
DMObject := ListDMobject; // Private reference
This code shows that the constructor expects to be passed a data management object as the parameter. This is expected to be of the correct type to support the list operations for the class in question, and will be provided by the application-specific list constructor. Once this data management object has been passed to our list it assumes full responsibility for it and therefore must free it in the destructor. This breaks a usual good rule of thumb where a class should be destroyed in the scope and context in which it was created, but it does allow for some particularly concise coding that eases the burden when implementing these classes in an application.
Listing 1 shows the actual implementation of a TCustomerList class that has a number of constructors. As can be seen each custom constructor simply calls the standard inherited constructor, dynamically constructing the data management object required. A particular point is that the parameters are passed unchanged to the second constructor, keeping the interface between business logic and persistence classes database-neutral. Apart from the property that returns the "current" problem domain object in the list typecast to one of an appropriate type, this is all the implementation that the business logic needs for each new type of list required. As can be seen, this is a very small amount of code, as most of the work is happening in our application-independent base classes.
Encapsulating Database Access
The reason that our business logic for handling these lists is so lightweight is because they delegate all of the work to their respective data management objects. Depending upon the particular database that is used to persist object data, the amount of work required here can vary enormously. Fortunately, the vast majority of systems these days store their data in an RDBMS, or at least access the data through a query language, in which case implementation is simple.
Our data management class will need a private cursor of some kind to the database. I am assuming that this cursor (in this instance) has the ability to execute a SQL query, and fetch the data. This cursor will of course be entirely database-dependent; generally I choose the fastest, most convenient or easiest to deploy option for a given database. This might be a generic API such as ADO or ODBC, or more usually I employ a specific approach such as an interface to a client API, maybe using a thin wrapper (such as IBExpress for Interbase access). Some may question why not build the database entirely layer around an existing database-independent API such as ODBC. There is nothing to say that this is not a viable solution, but keeping the interface to the database layer entirely neutral (and object-based) has the benefit of allowing all such API's to be used, where appropriate. The actual implementation of the database layer is of course free to use any API it so chooses, including generic ones such as ADO, ODBC or the BDE. It should be stressed that providing our own database-neutral interface for object persistence is not re-inventing the wheel; our interface is a very lightweight wrapper (often implemented as the Façade pattern) around a more functional API to which the bulk of the work is delegated. The key is that our wrapper encapsulates the required functionality, and facilitates the selection of an alternative database API should requirements change or needs dictate.
The FirstRecord method on our class (called by the First method in the TPDList) simply cancels any executing query within the class and issues the appropriate SELECT query. The NextRecord will be mapped onto a suitable method on the database cursor, as will the IsLast property. As each record in the cursor is accessed, the data management class should be prepared to instantiate and populate an appropriate business object expected by the calling TPDList. The population of this class from the database cursor fields should be shared with the population of a single object through the Load method, and is best achieved by calling an abstract method, passing the database cursor and the object to be populated. This method must be overridden by our application specific data management objects to actually update the known properties of a specific business object from named fields in the supplied cursor.
We have already stated that our data management classes will have constructors that match those in their problem domain list equivalents. Assuming our database supports queries, all these constructors need to do is to define a query string that selects the required records from the table, dynamically building up the query from the parameters passed in. For queries that are executed very frequently, or where performance is paramount, it is of course possible for this constructor to call a custom stored procedure on the database, passing parameters through to it in some convenient form (such as substituting object ID's for referenced objects passed as a parameter). The FirstRecord method in our class will actually be responsible for initiating the execution of the query (or calling a stored procedure); this is good practice as it keeps the constructor of a class lightweight. Listing 2 shows the corresponding constructors for the CustomerDM (data management) object.
Hierarchies of responsibility
I should finally pass comment on the class hierarchy involved on the data management side. Our base TDMObject should be kept database-independent, but it must provide an interface that the problem domain classes can use. This is achieved by providing a number of abstract methods on the base class. Actual database dependencies are introduced in custom descendants of TDMObject (such as TSQLServerDMObject, or TOracleDMObject) that implement the abstract methods, provide the services necessary to support database access against their target platform, and may provide further abstract methods that should be implemented by descendants.
These database-specific descendants are perfectly entitled to introduce dependencies on any other classes they need to communicate with their chosen database using any suitable API. Typically, they will provide a reasonable amount of functionality supporting such issues as establishing a connection to the engine, selecting a specific database, logging in as a named user and so on. How they achieve such needs is entirely under their own control: remember that our base TDMObject interface is a contract purely for the services required by the TPDObject and TPDList, and does not dictate any details such as a database context. This also means that our database-specific objects can take advantage of any features of the chosen database or API to maximise ease of use or performance, while resting easy in the knowledge that a move to another database is still possible without wholesale changes to the main body of application code. In fact, as these classes are application-independent, it is possible for a database expert to incorporate possibly high levels of complexity (to extract maximum performance from the API), while exposing a simple interface for the application-specific users of these classes.
One side effect of this hierarchy is that our application data management objects will be strongly tied to the functionality required of a specific database layer, and therefore a change of database will require a re-implementation of the data management objects. This impact can be reduced or possibly even completely removed if all database-specific classes are designed with identical services; experience suggests that while this approach is practical for similar database API's, providing an entirely database-independent class hierarchy limits the opportunities to take advantage of a specific platform. It is of course perfectly possible to introduce sub-hierarchies with identical interfaces, the most common being a set of database objects that all use SQL as their primary means of communication. This lends itself to hierarchies such as the following, where the first three classes lie within the framework (and include the bulk of the code), and the last two classes are application-specific:
TDMObject → TSQL_DMObject → TOracleDMObject → TMyAppDMObject → TCustomerDM
Last article's question
I asked how we can use our new list classes to represent object relationships such as 1-many. A classic example of such a relationship would be Customer.Orders, representing the set of orders ever placed by a customer. As with our other 1-1 and many-1 relationships, exposing them as a property on the related class is a pleasing and natural way of doing so. The first question is, how can they be implemented? The answer to this is very simple; we will use the standard lazy construction techniques on our property accessor function to only create the list the first time it is required. The actual list to be constructed will be the appropriate list based on the relationship we are expressing, in this case we would want to build the list of Orders for a known Customer. Listing 3 shows a complete implementation for such a relationship. The most telling feature is the use of "Self" to construct a list of Orders for a specific Customer instance.
The second part of the question was: can we handle such operations generically as we do with the simpler relationships (remember we implemented a generic GetObject method in our base TPDObject class)? On the face of it we might be able to implement a generic GetList method, but on closer inspection this is not the case. The reason for this inability to handle lists generically is that we want to call a specific constructor with particular parameters on the list required. This would require some kind of a reference to a constructor type and there is no concise way of expressing this while keeping parameter handling simple. So, while we can handle single object relationships generically we cannot do similar for set object relationships and they must each be handled explicitly as seen in Listing 1. As can be seen, the amount of code involved is minimal and this is therefore not too onerous a task.
((( Listing 1 - Custom constructors for TCustomerList)))
inherited Create (TCustomerDM.CreateAll);
constructor TCustomerList.CreateByName (const Name: String);
inherited Create (TCustomerDM.CreateByName (Name));
constructor TCustomerList.CreateByStockOrder (Item: TStockItem);
inherited Create (TCustomerDM.CreateByStockOrder (Item));
((( End Listing 1 )))
((( Listing 2 - Matching constructors for the Customer Data Management object)))
Query := 'SELECT * FROM Customer ORDER BY Name';
constructor TCustomerDM.CreateByName (const Name: String);
Query := 'SELECT * FROM Customer WHERE Name LIKE "' + Name + '%";
constructor TCustomerDM.CreateByStockOrder (Item: TStockItem);
Query := 'SELECT * FROM Customer WHERE ItemID=' + IDToStr (Item.ID);
((( End Listing 2 )))
((( Listing 3 - Implementing 1-many relationships )))
TCustomer = class (TPDObject)
function GetOrders: TOrderList;
destructor Destroy; override;
property Orders: TOrderList read GetOrders;
function TCustomer.GetOrders: TOrderList;
if FOrders = nil then FOrders := TOrderList.CreateByCustomer (Self);
Result := FOrders;
((( End Listing 3 )))