Simple Object Relationships

Business objects like to expose themselves and their relations. In this article I show you how.

Simple Object Relationships



In the last article we looked at how the classic Stock, Order and Customer requirement could be modelled as three separate business objects, each exposing their state through well-defined properties. Although these objects could now be used as-is (although in a very limited way - we haven't yet defined how to make their information persistent!), they lack any concept of relationships. In other words, how do we know which Customer placed a given Order, and for which StockItem?



It is illuminating at this stage to pause and think about how this would traditionally be handled in a non-OO, data-centric fashion. Typically, a database record representing the Order would expose two additional fields, one storing a foreign key to the Customer table, and another to the StockItem table. One of the deficiencies of this approach has already been demonstrated: all database fields are exposed to the same degree; there is no concept of one field being more "private" than another. Of course, it is possible to express this to a certain extent with a database view, but at the raw data level fields cannot be distinguished in terms of visibility or role except by name, and that way madness lies.



Let us now consider how we would access information about the customer who placed a given order in this traditional model. Assuming we have got the information about the order in some kind of dataset (maybe a TQuery), we would inspect the value of the customer foreign key field, construct a new query selecting from the Customer table based on this value, and then inspect the data returned. Note that this is actually quite a lot of code to write, requiring at least one local object (the customer dataset) to be constructed, and such code quickly proliferates if we need to access the stock item or other related data items. An alternative to this approach is to select all of the data in a single query using a multi-table join. This overcomes the amount of code to be written but introduces another issue - the hard coding of the data relationships within the query string. In anything other than a trivial application such expressions are repeated in many different places throughout the source - and it is worth noting that the compiler can do nothing to assist in the correctness of such queries. If you change database fundamentals (perhaps changing the name or type of the foreign key field in a table) then it is necessary to resort to application-wide search and replace techniques to find occurrences which might need updating. In this situation it is very possible that some infrequently accessed form will be fall through the net and will retain an out of date reference to a field that no longer exists. This is a runtime error waiting to happen. It is possible to place such these queries in a centralised data module but that's just geography, and requires care so that two separate parts of the application don't attempt to use the same query concurrently.



Back in the orderly world of well-constructed business objects exposing the equivalent of a foreign key (in the form of our TObjectID) as a public property is anathema. Listing 1 shows how this would look and a quick glance shows that we have adulterated our public interface with details that we'd rather not expose. After all, if such properties are made public then everyone can assume that they have the right to use them.



Doing it with objects

Object orientation offers us a far more natural way of expressing these relationships rather than through implicit foreign key fields - we can actually expose the related object as a property. This allows us to use such constructs as Order.Customer and Order.StockItem to access the relevant objects for our order. Note that this is a natural and concise way of expressing the relationships and completely hides the implementation details. If our secondary objects themselves expose relationships (such as the company for a customer) then finding the dispatch address for an order can be as simple as using Order.Customer.Company.Address, an extremely powerful construct. It is worth pointing out that if we later decide to remove the Company relationship (or, indeed, change the way in which it is implemented), then the compiler will detect every instance throughout the application where it is being used incorrectly, and the compile will fail until all issues are addressed. Already object orientation has assisted us to help deliver a more reliable application: we can make assertions about the fact that all relevant changes have been made.



Assuming we are going to expose our related objects as properties, how is this going to be implemented? Obviously, we are going to need to a private "placeholder" field to store a reference to the related object. A naïve approach would be to create this related object in the constructor and simply have the property pointing to the private field. This approach has two main issues - one undesirable and the other catastrophic. The undesirable issue is that we are going to incur the burden of constructing this related object every time we construct the parent. Assuming we populate the properties of the related object from persistent storage, we have impacted the runtime performance of our application with the overhead of constructing a new instance and making a database access. In general, it is worthwhile taking some care to keep the constructor as lightweight as possible. Extending this situation, it can be seen that if constructing object A causes the instantiation of object B, whose own constructor causes the instantiation of the same object A then we are going to enter the world of infinite recursion and the first thing a user will see is an out of memory error message. This situation is no more than the very common 1-1 entity relationship.



The solution to this issue is a technique known as construct on demand, or lazy construction. This uses the idea of delaying construction of the object until just before the first time it is required. For future accesses the already instantiated object is simply returned. This delays any expensive operations until the last possible moment, while remaining transparent to all users of our class. Listing 2 shows how this can be implemented. For the time being, we will assume that all of our TPDObjects offer a Load method that populates the object properties from some kind of persistent storage, given a relevant unique object ID.



Leveraging the power of objects


This appears to solve our problem of cascading construction, and is a technique that can be used to great effect with any property (not just business objects) that is potentially expensive to construct. However, let us think about what will happen when we apply this technique to other business objects. What will the code for the accessor function of Order.StockItem look like, or indeed the general case of X.Y? The result will look very similar to Listing 2, substituting new references for the private fields and, crucially, the type of the class being constructed. Rather than repeat such similar code (and run the risk of not implementing all such accessor functions identically), is there any way in which we can use the power of our object hierarchy to reduce this coding burden? Unsurprisingly, the answer is an emphatic yes.



What we would like to do is to provide some kind of utility function in our base TPDObject class (part of our application-independent framework) which performs the same function but can be parameterised in some way. The salient details that we will need to pass as parameters are the placeholder for the object being constructed, the ID that should be loaded, and some way of constructing the correct class. To achieve this last parameter we will use a construct called a class reference, which is a variable that holds a specific type as part of a class hierarchy. In Delphi, this is defined with the class of keyword. Listing 3 shows the changes to our Framework unit to provide such functionality. Note that the object instance will actually be updated in the routine and this is therefore one of the few occasions where an object should be passed as a var parameter.



One other element of note is that the constructor for TPDObject has now been defined as virtual. Without this our generic routine would not be able to call a specific class constructor, and would always construct classes of type TPDObject. This is the same approach as is used for components within Delphi, and the consequence is the same: all of our constructors for our TPDObject descendants must now use the override keyword.



How will our accessor functions on our business objects now use this available method? Their interfaces must remain the same, parameterless. However, we can implement the call in a single line of code as shown in Listing 4. Two factors should be highlighted: the result of the call must be typecast to TCustomer as it returns a type TPDObject, although the actual instance will be of the correct type. Secondly, the private FCustomer field must be changed to type TPDObject. This is because Delphi insists that var parameters are exactly the expected type. In both cases these are safe operations as the details are private and we are guaranteed to be returned a class of the expected type, even if the ancestor class has no knowledge of them. We are now able to implement all such relationships in one line of code, using a centralised application-independent routine. The benefits of a true OO development are beginning to make themselves known. Having constructed these related business objects, we must ensure that they are destroyed. A simple approach is to place standard calls to Free in the destructor: the protection around Free will ensure that nothing untoward will happen if the object has not actually been constructed (Free on a nil object is an allowable operation).



Last article's problem

Previously I alluded to the set of classes that should not be used in a "true" OO application. Figure 1 from last month's column shows that interface elements cannot interact with the persistence layer, only the problem domain. Data-aware components break this rule, sending changes directly to the database connection, and therefore (controversially) should not be used. There are many, better, alternatives and these will investigated in future columns.



((( Listing 1 - Adulterating a class with properties to support relationships)))

type

  TOrder = class (TMyAppPDObject)

  private

    FCustomerID: TObjectID;

  public

    property CustomerID: TObjectID read FCustomerID;

    ... // Other properties follow...

  end;

((( End Listing 1 )))



((( Listing 2 - Using lazy construction for related objects)))

type

  TOrder = class (TMyAppPDObject)

  private

    FCustomerID: TObjectID;

    FCustomer: TCustomer;

    function GetCustomer: TCustomer;

  public

    property Customer: TCustomer read GetCustomer;

    ... // Other properties follow...

  end;



function TOrder.GetCustomer: TCustomer;

begin

  if FCustomer = nil then begin

    FCustomer := TCustomer.Create;

    FCustomer.Load (FCustomerID);

  end;

  Result := FCustomer;

end;

((( End Listing 2 )))



((( Listing 3 - Generic lazy construction of related objects)))

type

  TPDClass = class of TPDObject;

  TPDObject = class

  protected

    function GetObject (const ClassType: TPDClass; var PDObject: TPDObject; const ID: TObjectID): TPDObject;

  public

    constructor Create; virtual;

    procedure Load (ID: TObjectID);

  end;



function TPDObject.GetObject (const ClassType: TPDClass; var PDObject: TPDObject; const ID: TObjectID): TPDObject;

begin

  if PDObject = nil then begin

    PDObject := ClassType.Create;

    if ID <> NotAssigned then PDObject.Load (ID);

  end;

  Result := PDObject;

end;

 ((( End Listing 3 )))



((( Listing 4 - Our new TOrder.Customer accessor function)))

function TOrder.GetCustomer: TCustomer;

begin

  Result := TCustomer (GetObject (TCustomer, FCustomer, FCustomerID));

end;

((( End Listing 4 )))



Next in series




 

Share this article!

Follow us!

Find more helpful articles: