Factory-Worker Objects

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.

Factory-Worker Model

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.

Shared recordset implementation

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.

  1. The client requests the EmployeeDetail recordset from the Employee Maintenance action object.

  2. The Employee Maintenance action object creates an Employee factory object.

  3. The Employee factory object obtains an Employee recordset from the DAL.

  4. The Employee factory object creates an Employee worker object for each of the rows in the recordset.

  5. The Employee factory object sets the corresponding property on the Employee worker object for each of the fields in that row of the recordset.

    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:

  6. The Employee Maintenance action object creates a recordset.

  7. The Employee Maintenance action object retrieves each Employee worker object from the Employee factory object and creates a row in the recordset.

  8. Each property on that Employee worker object is copied into a field in the recordset.

  9. This recordset is returned to the client and serialized on the client side.

  10. The client releases the reference to the action object.

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.

  1. The client requests an EmployeeDetail recordset from the Employee Maintenance action object.

  2. The Employee Maintenance action object creates an Employee factory object.

  3. The Employee factory object obtains an Employee recordset from the DAL.

  4. The Employee factory object keeps a reference to the recordset.

    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.

  5. The Employee Maintenance action object obtains a reference to the recordset from the Employee factory object via the Recordset property.

  6. This recordset is returned to the client and serialized on the client side.

  7. The client releases the reference to the action 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.

Factory Objects

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 a factory object

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 

Populating a factory object

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.

Obtaining a worker object

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 

Counting the worker objects

Couldn't be simpler:

MsgBox CStr(ofcEmployees.Count) Private Property Get Count() As Long     Count = colPicfEmployees.Count End Property 

Says it all, really.

Adding workers to factories

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.

Creating new workers

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.

Persistence (Perhaps?)

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.

Removing and Deleting

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 

Getting at the Recordset

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.

Determining the Parameters of a Factory

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.    

Worker Objects

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

Creating a worker

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.

Identification, please

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.

Show me the data!

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 

Methods in the madness

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.

Worker objects creating factories

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.

Where are my children?

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 

Business Object Browser

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.

Business object wizard

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.

Business object model design guidelines

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.

Wrap Up

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.



Ltd Mandelbrot Set International Advanced Microsoft Visual Basics 6. 0
Advanced Microsoft Visual Basic (Mps)
ISBN: 1572318937
EAN: 2147483647
Year: 1997
Pages: 168

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net