So what, really, is a business object and how is it implemented? Well, in the Visual Basic world, a business object is a public object that exposes business-specific attributes. The approach TMS takes toward business objects is to employ the action-factory-worker model. We'll come to the action objects later, but for now we'll concentrate on the factory-worker objects.
A quick note about terminology: in this design pattern there is no such thing as an atomic "business object" itself. The combination of the interaction between action, worker, and factory can be described as a logical business object.
The factory-worker model stipulates that for each particular type of business object an associated management class will exist. The purpose of the management class is to control the creation and population of data in the business objects. This management class is referred to as a factory class. The interface of every factory class is identical (except under exceptional circumstances).
Likewise, the worker class is the actual business object. Business objects cannot be instantiated without an associated factory class. In Visual Basic-speak we say that they are "Public Not Creatable"—the only way to gain access to a worker object is through the publicly instantiable/creatable factory class. So when we refer to a business object we are actually talking about the combination of the factory and worker objects, since each is dependent on the other.
Adding all this factory-worker code to your application isn't going to make it any faster. If your worker objects had 30 properties each and you wanted to create 1000 worker objects, the factory class would have to receive 1000 rows from the database and then populate each worker with the 30 fields. This would require 30,000 data operations! Needless to say, a substantial overhead.
What you need is a method of populating the worker objects in a high-performance fashion. The Shared Recordset Model solves this problem, which means that one cRecordset is retrieved from the DAL and each worker object is given a reference to a particular row in that recordset. This way, when a property is accessed on the worker object, the object retrieves the data from the recordset rather than from explicit internal variables, saving us the overhead of populating each worker object with its own data.
Populating each worker object involves instantiating only the object and then passing an object reference to the Shared Recordset and a row identifier, rather than setting all properties in the worker object individually. The worker object uses this object reference to the recordset to retrieve or write data from its particular row when a property of the business object is accessed. But to establish the real benefits of using the factory-worker approach, we need to discuss briefly how distributed clients interface with our business objects. This is covered in much greater detail in the sections, "Action Objects" and "Clients," later in this chapter.
To make a long story short, distributed clients send and receive recordsets only. Distributed clients have no direct interface to the business objects themselves. This is the role of action objects. Action objects act as the brokers between client applications and business objects. The recordset supports serialization, so the clients use this "serializable" recordset as a means of transferring data to and from the client tier to the business tier via the action object.
It's quite common for a client to request information that originates from a single business object. Say, for example, that the client requests all the information about an organization's employees. What the client application wants to receive is a recordset containing all the Employee Detail information from an action object. The EmployeeDetail recordset contains 1800 employees with 30 fields of data for each row.
Let's look at what's involved in transferring this information from the business objects to the client if we don't use the Shared Recordset implementation.
We now have a factory-worker object containing information for our 1800 employees. But the client needs all this information in a recordset, so the client follows these steps:
Basically, the business object gets a recordset, tears it apart, and then the action object re-creates exactly the same recordset we had in the first place. In this case, we had 1800 × 30 data items that were set and then retrieved, for a total of 108,000 data operations performed on the recordsets!
Let's look at the difference if we use the Shared Recordset Model.
NOTE
Notice that at this point the factory will not create any objects; rather, it will create the objects only the first time they are requested—in essence, it will create a Just In Time (JIT) object.
Total data operations on the recordset—zero. We can now return large sets of data from a business object in a high-performance fashion. But this leads to the question of why you should bother with the business objects at all. If all the client is doing is requesting a recordset from the DAL, why the heck doesn't it just use the DAL directly?
Well, to do so would completely ignore the compelling arguments for object-oriented programming. We want contained, self-documenting abstracted objects to represent our core business, and this scenario is only receiving data, not performing any methods on these objects.
Remember that this is a particular case when a client, on a separate physical tier, requests a set of data that directly correlates to a single business object. The data will often need to be aggregated from one or more business objects. So the action object, which exists on the same physical tier as the business object, will have full access to the direct worker object properties to perform this data packaging role.
A client will often request that a complex set of business logic be performed. The action object will perform this logic by dealing directly with the worker objects and the explicit interfaces they provide. Thus, the action objects can fully exploit the power of using the objects directly.
Using the worker objects directly on the business tier means we are creating much more readable, self-documenting code. But because of the advantages of the Shared Recordset implementation, we are not creating a problem in terms of performance. If we need to shift massive amounts of data to the client, we can still do it and maintain a true business object approach at the same time—we have the best of both worlds.
Now that we have covered the general architecture of the action-factory-worker-recordset interaction, we can take a closer look at the code inside the factory and worker objects that makes all this interaction possible.
The interface for the factory object is as follows:
cFactory Interface
Member | Description |
Create | Initializes the business object with a DAL object |
Populate | Creates worker objects according to any passed-in parameter object |
Item | Returns a worker object |
Count | Returns the number of worker objects contained in the factory |
Add | Adds an existing worker object to the factory |
AddNew | Returns a new empty worker object |
Persist | Updates all changes in the worker objects to the database |
Remove | Removes a worker object from the factory |
Recordset | Returns the internal factory recordset |
Delete | Deletes a worker object from the data source and the factory |
Parameters | Returns the parameters that can be used to create worker objects |
Creating the factory is simple. The code below demonstrates the use of the Create method to instantiate a factory object. This code exists in the action objects (which we'll cover later). This sample action object uses the Employees business object to perform a business process.
Dim ocDAL As New cDAL Dim ocfEmployees As New cfEmployees ocfEmployees.Create ocDAL
Hold it—what's the DAL doing here? Isn't the purpose of business objects to remove the dependency on the DAL? Yes, it is. But something important is happening here, and it has everything to do with transactions. The scope and lifetime of the action object determines the scope and lifetime of our transaction for the business objects as well.
Say our action object needs to access four different factory objects and change data in each of them. Somehow each business object needs to be contained in the same transaction. This is achieved by having the action object instantiate the DAL, activating the transaction and passing that DAL to all four factory objects it creates. This way, all our worker object activities inside the action object can be safely contained in a transaction, if required. More on action objects and transactions later.
So what does the Create code look like? Too easy:
Public Sub Create(i_ocDAL As cDAL) Set oPicDAL = i_ocDAL ' Set a module-level reference to the DAL. End Sub
Creating a factory object isn't really that exciting—the fun part is populating this factory with worker objects, or at least looking like we're doing so!
Dim ocDAL As New cDAL Dim ofcEmployees As New cfEmployees Dim ocParams As New cParams ofcEmployees.Create oDAL ocParams.Add "Department", "Engineering" ofcEmployees.Populate ocParams
Here a recordset has been defined in the DAL as Employees, which can take the parameter Department to retrieve all employees for a particular department. Good old cParams is used to send parameters to the factory object, just like it does with the DAL. What a chipper little class it is!
So there you have it—the factory object now contains all the worker objects ready for us to use. But how does this Populate method work?
Private oPicRecordset As cRecordset Public Sub Populate (Optional i_ocParams as cParams) Set oPicRecordset = oPicDAL.OpenRecordset("Employees", i_ocParams) End Sub
The important point here is that the Populate method is only retrieving the recordset—it is not creating the worker objects. Creating the worker objects is left for when the user accesses the worker objects via either the Item or Count method.
Some readers might argue that using cParams instead of explicit parameters detracts from the design. The downside of using cParams is that the parameters cannot be determined for this class at design time and do not contribute to the self-documenting properties of components. In a way I agree, but using explicit parameters also has its limitations.
The reason I tend to use cParams rather than explicit parameters in the factory object Populate method is that the interface to the factory class is inherently stable. With cParams all factory objects have the same interface, so if parameters for the underlying data source change (as we all know they do in the real world) the public interface of our components will not be affected, thereby limiting the dreaded Visual Basic nightmare of incompatible components.
Also of interest in the Populate method is that the cParams object is optional. A Populate method that happens without a set of cParams is determined to be the default Populate method and in most cases will retrieve all appropriate objects for that factory. This functionality is implemented in the DAL.
After we have populated the factory object, we can retrieve worker objects via the Item method as shown here:
Dim ocDAL As New cDAL Dim ofcEmployees As New cfEmployees Dim owcEmployee As New cwEmployee Dim ocParams As New cParams ofcEmployees.Create ocDAL ocParams.Add "Department", "Engineering" ofcEmployees.Populate ocParams Set owcEmployee = ofcEmployees.Item("EdmundsM") MsgBox owcEmployee.Department
At this point, the Item method will initiate the instantiation of worker objects. (Nothing like a bit of instantiation initiation.)
Public Property Get Item(i_vKey As Variant) As cwEmployee If colPicwEmployee Is Nothing Then PiCreateWorkerObjects Set Item = colPicwEmployee(i_vKey).Item End Property
So what does PiCreateWorkerObjects do?
Private Sub PiCreateWorkerObjects Dim owcEmployee As cwEmployee Do Until oPicRecordset.EOF Set owcEmployee = New cwEmployee owcEmployee.Create oPicRecordset, oPicDAL oPicRecordset.MoveNext Loop End Sub
Here we can see the payback in performance for using the Shared Recordset Model. Initializing the worker object simply involves calling the Create method of the worker and passing in a reference to oPicRecordset and oPicDAL. The receiving worker object will store the current row reference and use this to retrieve its data.
But why is the DAL reference there? The DAL reference is needed so that a worker object has the ability to create a factory of its own. This is the way object model hierarchies are built up. (More on this later.)
The Item method is also the default method of the class, enabling us to use the coding-friendly syntax of
ocfEmployees("637").Name
Couldn't be simpler:
MsgBox CStr(ofcEmployees.Count) Private Property Get Count() As Long Count = colPicfEmployees.Count End Property
Says it all, really.
Often you will have a factory object to which you would like to add pre-existing worker objects. You can achieve this by using the Add method. Sounds simple, but there are some subtle implications when using the Shared Recordset implementation. Here it is in action:
Dim ocDAL As New cDAL Dim ocfEmployees As New cfEmployees Dim ocwEmployee As New cwEmployee ocfEmployees.Create ocDAL Set ocwEmployee = MagicEmployeeCreationFunction() ocfEmployees.Add ocwEmployee
You'll run into a few interesting quirks when adding another object. First, since the worker object we're adding to our factory has its data stored in another factory somewhere, we need to create a new row in our factory's recordset and copy the data from the worker object into the new row. Then we need to set this new object's recordset reference from its old parent factory to the new parent factory, otherwise it would be living in one factory but referencing data in another—that would be very bad. To set the new reference, we must call the worker Create method to "bed" it into its new home.
Public Sub Add(i_ocwEmployee As cwEmployee) oPicRecordset.AddNew With oPicRecordset.Fields .("ID") = i_ocwEmployee.ID .("Department") = i_ocwEmployee.Department .("FirstName") = i_ocwEmployee.FirstName .("LastName") = i_ocwEmployee.LastName End With oPicRecordset.Update i ocwEmployee.Create oPicRecordset End Sub
And there you have it—one worker object in a new factory.
You use the AddNew method when you want to request a new worker object from the factory. In the factory, this involves adding a row to the recordset and creating a new worker object that references this added row. One minor complication here: what if I don't have already have a recordset?
Suppose that I've created a factory object, but I haven't populated it. In this case, I don't have a recordset at all, so before I can create the new worker object I have to create the recordset. Now, when I get a recordset from the DAL, it comes back already containing the required fields. But if I don't have a recordset, I'm going to have to build one manually. This is a slightly laborious task because it means performing AddField operations for each property on the worker object. Another way to do this would be to retrieve an empty recordset from the DAL, either by requesting the required recordset with parameters that will definitely return an empty recordset, or by having an optional parameter on the OpenRecordset call. In our current implementation, however, we build empty recordsets inside the factory object itself.
But before we look at the process of creating a recordset manually, let's see the AddNew procedure in action:
Dim ocDAL As New cDAL Dim ocfEmployees As New cfEmployees Dim ocwEmployee As New cwEmployee ocfEmployees.Create ocDAL Set ocfEmployee = ocfEmployees.AddNew ocfEmployee.Name = "Adam Magee"
This is how it is implemented:
Public Function AddNew() As cwEmployee Dim ocwEmployee As cwEmployee If oPicRecordset Is Nothing Then Set oPicRecordset = New cRecordset With oPicRecordset .AddField "ID" .AddField "Department" .AddField "FirstName" .AddField "LastName" End With End If oPicRecordset.AddNew ' Add an empty row for the ' worker object to reference. oPicRecordset.Update Set ocwEmployee = New cwEmployee ocwEmployee.Create oPicRecordset, oPicDAL colPicwWorkers.Add ocwEmployee Set New = ocwEmployee End Property
This introduces an unavoidable maintenance problem, though. Changes to the worker object must now involve updating this code as well—not the most elegant solution, but it's worth keeping this in mind whenever changes to the worker objects are implemented.
So now we can retrieve worker objects from the database, we can add them to other factories, and we can create new ones—all well and good—but what about saving them back to the data source? This is the role of persistence. Basically, persistence involves sending the recordset back to the database to be updated. The DAL has a method that does exactly that—UpdateRecordset—and we can also supply and retrieve any parameters that might be appropriate for the update operation (although most of the time UpdateRecordset tends to happen without any reliance on parameters at all).
Dim ocDAL As New cDAL Dim ocfEmployees As New cfEmployees Dim ocwEmployee As New cwEmployee Dim ocParams As New cParams ocfEmployees.Create ocDAL ocParams.Add "Department", "Engineering" ocfEmployees.Populate ocParams For Each ocwEmployee In ocfEmployees .Salary = "Peanuts" Next 'ocwEmployee ocfEmployees.Persist ' Reduce costs
What is this Persist method doing, then?
Public Sub Persist(Optional i_ocParams As cParams) oPicDAL.UpdateRecordset oPicRecordset, i_ocParams End Sub
Consider it persisted.
What about removing worker objects from factories? Well, you have two options—sack 'em or whack 'em!
A worker object can be removed from the factory, which has no effect on the underlying data source. Maybe the factory is acting as a temporary collection for worker objects while they wait for an operation to be performed on them. For example, a collection of Employee objects needs to have the IncreaseSalary method called (yeah, I know what you're thinking—that would be a pretty small collection). For one reason or another, you need to remove an Employee worker object from this factory (maybe the worker object had the SpendAllDayAtWorkSurfingTheWeb property set to True), so you would call the Remove method. This method just removes the worker object from this factory with no effect on the underlying data. This is the sack 'em approach.
You use the other method when you want to permanently delete an object from the underlying data source as well as from the factory. This involves calling the Delete method on the factory and is known as the whack 'em approach. Calling Delete on the factory will completely remove the worker object and mark its row in the database for deletion the next time a Persist is executed.
This is an important point worth repeating—if you delete an object from a factory, it is not automatically deleted from the data source. So if you want to be sure that your worker objects data is deleted promptly and permanently, make sure that you call the Persist method! Some of you might ask, "Well, why not call the Persist directly from within the Delete method?" You wouldn't do this because of performance. If you wanted to delete 1000 objects, say, you wouldn't want a database update operation to be called for each one—you would want it to be called only at the end when all objects have been logically deleted.
Dim ocDAL As New cDAL Dim ocfEmployees As New cfEmployees Dim ocwEmployee As New cwEmployee ocfEmployees.Create ocDAL ocfEmployees.Populate For Each ocwEmployee In ocfEmployees With ocwEmployee If .Salary = "Peanuts" Then ocfEmployees.Remove .Index ' Save the peasants. Else ocfEmployees.Delete .Index ' Punish the guilty. End If End With Next 'ocwEmployee ocfEmployees.Persist ' Exact revenge.
One important point to note here is that worker objects cannot commit suicide! Only the factory object has the power to delete or remove a worker object.
Public Sub Remove(i_vKey As Variant) colPicwWorkers.Remove i_vKey End Sub Public Sub Delete(i_vKey as Variant) If VarType(i_vKey) = vbString Then oPicRecordset.AbsolutePosition = _ colPicwWorkers.Item(i_vKey).AbsolutePosition oPicRecordset.Delete colPicwWorkers.Remove oPicRecordset.AbsolutePosition Else oPicRecordset.AbsolutePosition = i_vKey oPicRecordset.Delete colPicwWorkers.Remove i_vKey End If End Sub
As discussed earlier, sometimes it is more efficient to deal with the internal factory recordset directly rather than with the factory object. This is primarily true when dealing with distributed clients that do not have direct access to the worker objects themselves. In this case, the factory object exports this recordset through the Recordset property.
The Recordset property can also be used to regenerate a business object. Imagine a distributed client that has accessed an EmployeeDetails method on an action object and has received the corresponding recordset. The distributed client then shuts the action object down (because, as we will soon see, action objects are designed to be stateless). This recordset is then modified and sent back to the action object. The action object needs to perform some operations on the business objects that are currently represented by the recordset.
The action object can create an empty factory object and assign the recordset sent back from the client to this factory. Calling the Populate method will now result in a set of worker objects being regenerated from this recordset! Or, if the data has just been sent back to the database, the action object could call Persist without performing the Populate method at all, again maximizing performance when the client is modifying simple sets of data.
Take particular care when using the Recordset property with distributed clients, though. It's important to ensure that other clients don't modify the underlying business object after the business object has been serialized as a recordset. In such a case, you'll end up with two different recordsets—the recordset on the client and the recordset inside the business object. This situation can easily be avoided by ensuring that the action objects remain as stateless as possible. In practice, this means closing down the business object immediately after the recordset has been retrieved, thereby minimizing the chance of the business object changing while a copy of the recordset exists on the client.
Dim ocfEmployees As New cfEmployees Dim ocParams As New cParams Dim ocRecordset As cRecordset Set ocfEmployees = New cfEmployees ocParams.Add "PostCode", "GL543HG" ocfEmployees.Create oPicDAL ocfEmployees.Populate ocParams Set ocRecordset = ocfEmployees.Recordset
Be aware that the above code is not recommended, except when you need to return sets of data to distributed clients. Data manipulation that is performed on the same tier as the factory objects should always be done by direct manipulation of the worker objects.
Because we don't use explicit procedure parameters in the Populate method of the factory class, it can be useful to be able to determine what these parameters are. The read-only Parameters property returns a cParams object populated with the valid parameter names for this factory.
The Parameters property is useful when designing tools that interact with business objects—such as the business object browser that we'll look at later on—since the parameters for a factory can be determined at run time. This determination allows us to automatically instantiate factory objects.
Dim ocfEmployees As New cfEmployees Dim ocParams As New cParams Set ocfEmployees = New cfEmployees Set ocParams = ocfEmployee.Parameters ' Do stuff with ocParams.
So far, we've concentrated mainly on the factory objects; now it's time to examine in greater detail the construction of the worker objects.
Factory objects all have the same interface. Worker objects all have unique interfaces. The interface of our sample Employee worker object is shown below.
cWorker Employee Interface
Member | Description |
ID | Unique string identifier for the worker object |
Name | Employee name |
Salary | Employee gross salary |
Department | Department the employee works in |
Create | Creates a new worker object |
As we saw in the discussion of the factory object, creating a worker object involves passing the worker object a reference to the shared recordset and a reference to the factory's DAL object. This is what the worker does with these parameters:
Private oPicRecordset As cRecordSet Private lPiRowIndex As Long Private oPicDAL As cDAL Friend Sub Create(i_ocRecordset As cRecordSet, i_ocDAL As cDAL) Set oPicRecordset = i_ocRecordset Set oPicDAL = i_ocDAL lPiRowIndex = I_ocRecordset.AbsolutePosition End Sub
Why is this procedure so friendly? (That is, why is it declared as Friend and not as Public?) Well, remember that these worker objects are "Public Not Creatable" because we want them instantiated only by the factory object. Because the factory and workers always live in the same component, the Friend designation gives the factory object exclusive access to the Create method. Notice also that the worker objects store the row reference in the module-level variable lPiRowIndex.
In this design pattern, an ID is required for all worker objects. This ID, or string, is used to index the worker object into the factory collection. This ID could be manually determined by each individual factory, but I like having the ID as a property on the object—it makes automating identification of individual worker objects inside each factory a lot easier.
In most cases, the ID is the corresponding database ID, but what about when a worker object is created based on a table with a multiple field primary key? In this case, the ID would return a concatenated string of these fields, even though they would exist as explicit properties in their own right.
Here is the internal worker code for the ID property—the property responsible for setting and returning the worker object ID. Note that this code is identical for every other Property Let/Get pair in the worker object.
Public Property Get ID() As Long PiSetAbsoluteRowPosition ID = oPicRecordset("ID") End Property Public Property Let ID(i_ID As Long) PiSetAbsoluteRowPosition PiSetPropertyValue "ID", i_ID End Property
The most important point here is the PiSetAbsoluteRowPosition call. This call is required to point the worker object to the correct row in the shared recordset. The recordset current record at this point is undefined—it could be anywhere. The call to PiSetAbsoluteRowPosition is required to make sure that the worker object is retrieving the correct row from the recordset.
Private Sub PiSetAbsoluteRowPosition() oPiRecordset.AbsolutePosition = lPiRowIndex End Sub
Likewise, this call to PiSetAbsoluteRowPosition needs to happen in the Property Let. The PiSetPropertyValue procedure merely edits the appropriate row in the recordset.
Private Sub PiSetPropertyValue(i_sFieldName As String, _ i_vFieldValue As Variant) oPiRecordset.Edit oPiRecordset(i_sFieldName) = i_vFieldValue oPiRecordset.Update End Sub
At the moment, all we've concentrated on are the properties of worker objects. What about methods?
An Employee worker object might have methods such as AssignNewProject. How do you implement these methods? Well, there are no special requirements here—implement the customer business methods as you see fit. Just remember that the data is in the shared recordset and that you should call PiSetAbsoluteRowPosition before you reference any internal data.
Factory objects returning workers is all well and good, but what happens when we want to create relationships between our business objects? For example, an Employee object might be related to the Roles object, which is the current assignment this employee has. In this case, the Employee worker object will return a reference to the Roles factory object. The Employee object will be responsible for creating the factory object and will supply any parameters required for its instantiation. This is great because it means we need only to supply parameters to the first factory object we create. Subsequent instantiations are managed by the worker objects themselves.
Dim ocDAL As New cDAL Dim ocfEmployees As New cfEmployees Dim ocwEmployee As New cwEmployee Dim ocParams As New cParams ocfEmployees.Create ocDAL ocParams.Add "ID", "637" ocfEmployees.Populate ocParams MsgBox ocfEmployees(1).Roles.Count
Here the Roles property on the Employee worker object returns the ocfRoles factory object.
Public Property Get Roles() As cfRoles Dim ocfRoles As New cfRoles Dim ocParams As New cParams ocParams.Add "EmpID", Me.ID ocfRoles.Create oPicDAL ocfRoles.Populate ocParams Set Roles = ocfRoles End Property
Accessing child factory objects this way is termed navigated instantiation, and you should bear in mind this important performance consideration. If I wanted to loop through each Employee and display the individual Roles for each Employee, one data access would retrieve all the employees via the DAL and another data access would retrieve each set of Roles per employee. If I had 1800 employees, there would be 1801 data access operations—one operation for the Employees and 1800 operations to obtain the Roles for each employee. This performance would be suboptimal.
In this case, it would be better to perform direct instantiation, which means you'd create the Roles for all employees in one call and then manually match the Roles to the appropriate employee. The Roles object would return the EmployeeID, which we would then use to key into the Employee factory object to obtain information about the Employee for this particular Roles object. The golden rule here is that navigated instantiation works well when the number of data access operations will be minimal; if you need performance, direct instantiation is the preferred method.
Dim ocfRoles As New cfRoles Dim ocfEmployees As New cfEmployees Dim ocwRole As cwRole ocfEmployees.Create oPicDAL ocfRoles.Create oPicDAL ocfEmployees.Populate ' Using default populate to retrieve all objects ocfRoles.Populate For Each ocwRole In ocfRoles MsgBox ocfEmployees(ocwRole.EmpID).Name Next ' ocwRole
An interesting scenario occurs when a worker object has two different properties that return the same type of factory object. For example, a worker could have a CurrentRoles property and a PreviousRoles property. The difference is that these properties supply different parameters to the underlying factory object Populate procedure.
It's useful to be able to query a worker object to determine what factory objects it supports as children. Therefore, a worker object contains the read-only property Factories, which enables the code that dynamically determines the child factory objects of a worker and can automatically instantiate them. This is useful for utilities that manipulate business objects.
The Factories property returns a cParams object containing the names of the properties that return factories and the names of those factory objects that they return. Visual Basic can then use the CallByName function to directly instantiate the factories of children objects, if required.
The Factories property is hidden on the interface of worker objects because it does not form part of the business interface; rather, it's normally used by utility programs to aid in dynamically navigating the object model.
Dim ocfEmployees As New cfEmployees Dim ocfEmployee As New cfEmployee Dim ocParams As New cParams ocfEmployees.Create oPicDAL ocfEmployees.Populate Set ocfEmployee = ocfEmployees(1) Set ocParams = ocfEmployee.Factories
After you've created the hierarchy of business objects by having worker objects return factory objects, you can dynamically interrogate this object model and represent it visually. A business object browser is a tremendously powerful tool for programmers to view both the structure and content of the business objects, because it allows the user to drag and drop business objects in the same fashion as the Microsoft Access Relationships editor.
Creating business objects can be a tedious task. If the data source you're modeling has 200 major entities (easily done for even a medium-size departmental database), that's a lot of business objects you'll have to build. Considering that the factory interface is the same for each business object and that the majority of the properties are derived directly from data fields, much of this process can be automated.
A business object wizard works by analyzing a data entity and then constructing an appropriate factory and worker class. This is not a completely automated process, however! Some code, such as worker objects returning factories, must still be coded manually. Also, any business methods on the worker object obviously have to be coded by hand, but using a business object wizard will save you a lot of time.
TMS uses a business object wizard to develop factory-worker classes based on a SQL Server database. This wizard is written as an add-in for Visual Basic and increases productivity tremendously by creating business objects based on the Shared Recordset implementation. If you need simple business objects, though, you can use the Data Object wizard in Visual Basic 6.0. The mapping of relational database fields to object properties is often referred to as the business object impedance mismatch.
Keep it simple—avoid circular relationships like the plague. Sometimes this is unavoidable, so make sure you keep a close eye on destroying references, and anticipate that you might have to use explicit destructors on the factory objects to achieve proper teardown. Teardown is the process of manually ensuring that business objects are set to Nothing rather than relying on automatic class dereferencing in Visual Basic. In practice, this means you call an explicit Destroy method to force the release of any internal object references.
Don't just blindly re-create the database structure as an object model; "denormalization" is OK in objects. For example, if you had an Employee table with many associated lookup tables for Department Name, Project Title, and so forth, you should denormalize these into the Employee object; otherwise, you'll be forever looking up Department factory objects to retrieve the Department Name for the employee or the current Project Title. The DAL should be responsible for resolving these foreign keys into the actual values and then transposing them again when update is required. Typically, this is done by a stored procedure in a SQL relational database.
This doesn't mean you won't need a Department factory class, which is still required for retrieving and maintaining the Department data. I'm saying that instead of your Employee class returning the DepartmentID, you should denormalize it so that it returns the Department Name.
MsgBox ocfEmployee.DepartmentName
is better than
MsgBox ocfDepartments(ocfEmployee.DepartmentID).Name
But at the end of the day, the level of denormalization required is up to you to decide. There are no hard and fast rules about denormalization—just try to achieve a manageable, usable number of business objects.
So there's the basic architecture for factory-worker model business objects with a Shared Recordset implementation. I feel that this approach provides a good balance between object purity and performance. The objects also return enough information about themselves to enable some great utilities to be written that assist in the development environment when creating and using business objects.