Using Delegation to Change Your Application's Architecture
This article may contain URLs that were valid when originally published, but now link to sites or pages that no longer exist. To maintain the flow of the article, we've left these URLs in the text, but disabled the links.
Using Delegation to Change Your Application's Architecture
In the January issue of FoxTalk, Mike Helland introduced you to two functions in Visual FoxPro 8.0: BindEvent() and RaiseEvent(). This month, he explains how delegation can change the architecture of an application at any level, and shares some practical advice on using the functionality.
By now you've seen how small systems can be created with objects that are capable of raising and handling events. If you recall, the biggest advantage to using events was that you could create more encapsulated code—it was possible to create classes that didn't know about any other classes, and still have them work naturally with other objects. But is it wise to use delegation everywhere in your applications? Sure it is, and to demonstrate that, I'm going to take this same idea and apply it at the ground floor level of a Windows application: how the user interface interacts with the business logic.
In many n-tier applications, a form will communicate directly with the business rules of the application. This means that the form must 1) know how to instantiate classes from the middle tier, 2) know how to call methods of that class, and 3) be able to respond to the class's behaviors. All of these requirements imply that the form must be very familiar with the class. But is this all the responsibility of the user interface? Here I've listed what I think a form should be capable of doing:
- Display data to the user.
- Allow the user to modify that data.
- Allow the user to indicate that the modifications should be accepted.
With these three requirements for the UI in mind, I created a class based on a form with text boxes, and the text boxes are bound to properties of an object called thisform.oRecord. What makes the form display and edit the data isn't a new idea, so I won't put that code here. (The code for the form is included in the accompanying Download file.) How the form satisfies the third requirement is what I want to examine, and this code is listed here:
Procedure command1.Click Raiseevent(Thisform, "Save", Thisform.oRecord) Endproc *-- Event is raised when user wants to Save Procedure Save LParameters oRecord Endproc
It looks like this code is only raising the Save() event and passing the record as a parameter. So how does the form actually save the changes that the user made? It doesn't. Understand that all the form is required to do is let the user tell the system to save—I never said that the form actually has to write the record itself!
For the record to actually be saved, an object must handle the Save() event of the form when it's raised. The following class represents an application (myApp) that consists of our form and a business object (myBizObj):
Define Class myApp as Custom OForm = .NULL. OBizObj = .NULL.
Function Init this.oForm = CreateObject("myForm") this.oBizObj = CreateObject("myBizObj") BindEvent(this.oForm, "Save", this, "SaveData") Return
Function EditData this.oForm.EditRecord(this.oBizObj.GetRecord()) this.oForm.Show() Return
Function SaveData Lparameters oRecord If this.oBizObj.SaveRecord(oRecord) Local laE If AEvents(laE, 0) > 0 laE.Hide() EndIf Else * Show a message if something went wrong EndIf Return
To see this code in action, create an instance of myApp from the Command Window and call its EditData() method. The form will appear, with a blank value, and when the OK button is clicked, the business object will attempt to save the new value, and the form will close.
Before we leave this code, I should explain the part with the AEvents() function. If you call AEvents() with an object, it will create an array for you detailing the event bindings that object is involved in. But if you pass it a zero, it will create an array with information about the currently executing event, including a reference to the object that raised the event. The application then uses this array element to close the form after a record had been saved.
I should address the same question that I asked in my first article: This is interesting, but why is this a good idea? The answers are:
- We've eliminated all the non-UI code from the user interface.
- It's now trivial to extend the functionality of the form, regardless of the form's parent class.
Let me clarify that last point. Take the application that uses myForm for data entry, and add the specification that "every time the user adds a record, we're supposed to bring up the blank form again to add another record until the user hits the Cancel button." In VFP 7, there are basically two ways this can be done: The first is to build this functionality right into the form, and the second is to call the form in a do-while loop over and over again.
The problem with the first solution is that every form will need this modification. Now, obviously, if every data entry form in the application is based on the same class, this isn't a major setback. But let's be honest—sometimes the inheritance tree doesn't make this a simple task, especially if we're dealing with a large application. In any case, adding this logic to the user interface means I've given the form more responsibilities than the initial three I outlined earlier. So what about the other option, having the form called repeatedly? That would work, except there's the pesky problem of having the form opening and closing, leaving an annoying flicker in your application. The user interface simply looks unsophisticated.
Fortunately, raising events will get the best of both worlds. In essence, our forms will be dumber but they'll look smarter. Again, don't underestimate the flexibility of being able to get different behavior out of the form with great ease. Take the example of adding multiple records in succession, but imagine a ceiling limit to how many records should be added. If a business rule for the application is "every sales person can only be working on five accounts at a time," our form will stop asking for accounts once this limit has been reached. Had our looping mechanism been implemented in the form itself (or worse, in the data entry form parent class), this wouldn't be a simple enhancement.
It should be noted that "dumbing down" the form may make previously straightforward tasks slightly more difficult. For example, with the user interface ignorant of the middle and data tiers, providing data-rich lookup forms for populating foreign keys becomes less direct. One possibility would be to raise an event when a user needs to populate a value. To handle this event, the system that contains the form would present the user with a lookup form based on the event's parameters, and then return the selected value. (This is done by setting a property on the parameter object—an example of this is also included in the article's Download file.)
Returning to myApp, I should explain why I made the decision to bind the form's Save() event decision to "this" in myApp.Init(). Because myApp.SaveRecord() simply forwards the oRecord parameter to the business object, it at first might have made sense to bind the form directly to the business object. But after some deliberation, using the method in between the two objects appears to be the better choice. What would happen if either class changed so that the parameters weren't exactly equal? In that scenario, where you've programmed your classes to another class's interface, you'd have to revise the outdated class, or put an adapter method in between after all. It's usually a better idea to just create the neutral method in the first place.
Another issue that should be addressed is the matter of the nFlags parameter to BindEvent(). If the last parameter isn't passed, simply executing a method call is enough for the delegate to be called. But if the value 2 is passed, the event binding won't kick in unless RaiseEvent() is explicitly called. My advice is to always pass the 2 when the event you're binding to is purposefully designed to be an event in the interface of the class.
In my experience, this will be fine most of the time. Being able to handle a method call that wasn't executed via BindEvent() does have its place, especially where the original design didn't take event binding into consideration. The ability to trigger a delegate when a method is called or a property is changed also has great potential in debugging. But honestly, it seems like this parameter was implemented backwards, from a class designer's point of view.
Finally, what does BindEvent() do to the performance of an application? Obviously, raising an event, handling the event, and calling methods from another class will result in more overhead than simply calling those methods directly. But the overhead isn't substantial. When I run one million iterations of the two preceding approaches, the direct method calling completes it in about 7.3 seconds, while calling the method via delegation (including the adapter method) runs around 11.1 seconds. When parameters are involved, the ratio jumps to 8.2 seconds vs. 14.15 seconds, respectively. So the classic design vs. performance trade-off surfaces here. By adding another layer of sophistication, is the flexibility of the design worth the overhead? You'll have to ask that question with your system's specific requirements in mind, but in most situations choosing the open design has been the better choice for me.
That's all, folks
After reading this article and its predecessor, you should be able to develop more robust classes by giving them the ability to raise events and delegating tasks between objects. You should also have an idea of how different these classes may be when compared to their equivalents from the pre-user-defined-events days of VFP. But the best way to grok how RaiseEvent() will affect your design methodologies is to get your hands on Visual FoxPro 8.0 as soon as possible and put all of its new features to work. Between delegation and exception handling, it runs like a whole new Fox.
To find out more about FoxTalk and Pinnacle Publishing, visit their website at http://www.pinpub.com/html/main.isx?sub=57
Note: This is not a Microsoft Corporation website. Microsoft is not responsible for its content.
This article is reproduced from the June 2003 issue of FoxTalk. Copyright 2003, by Pinnacle Publishing, Inc., unless otherwise noted. All rights are reserved. FoxTalk is an independently produced publication of Pinnacle Publishing, Inc. No part of this article may be used or reproduced in any fashion (except in brief quotations used in critical articles and reviews) without prior consent of Pinnacle Publishing, Inc. To contact Pinnacle Publishing, Inc., please call 1-800-493-4867 x4209.