In applying the Gamma/Helm Observer pattern to a PowerBuilder application, Mark and Nick show how design patterns can help PowerBuilder developers make good design decisions.
June 01, 1996
URL:http://www.drdobbs.com/tools/applying-design-patterns-to-powerbuilder/184409903
Mark and Nick are members of the research and development staff at MetaSolv Software in Dallas, Texas. Mark can be reached at [email protected]; Nick, at [email protected].
As the applications being developed with PowerBuilder become increasingly complex, PowerBuilder developers often struggle to apply sound design techniques to their applications. With support for most object-oriented programming features, PowerBuilder provides a great environment for implementing many of the recently published object-oriented design patterns. Like many other programming languages however, PowerBuilder allows you to break all the rules of good object-oriented design. Design patterns help to provide a road map to achieving greater reusability and maintainability in applications. Many of us know there are usually more questions than answers when it comes to designing software systems, and anything describing solutions tends to get our attention. While design patterns have the potential to help PowerBuilder developers, most examples and related information on patterns are presented in languages other than PowerBuilder. This leaves the translation into PowerBuilder up to the developer and can limit the exposure of patterns to the PowerBuilder community.
In this article, we'll examine how common design patterns can help PowerBuilder developers make good design decisions. In particular, we'll apply one of the patterns to design a window communication mechanism. We'll then implement this design in a simple PowerBuilder application. Once you've seen one of the patterns implemented in PowerBuilder, you should be able to find other design patterns useful as well.
Most applications developed using PowerBuilder contain multiple windows that display different views of the database. In many instances, multiple windows are displayed at the same time, reflecting different views of the same data. A common dilemma facing many PowerBuilder developers is deciding how and when information will be passed between windows to keep the information in the windows synchronized.
For example, Figure 1 shows a simple inventory application containing three windows. The Item Picture and Item Chart windows display different graphical representations of the item selected on the Inventory List window.
When you select an item from the Inventory List, the Item Picture and Item Chart windows' contents must change to reflect the selected inventory item. Decisions must be made about how the windows talk to one another. To synchronize displayed windows, it is common for PowerBuilder developers to simply add the necessary code to the Inventory List window to directly reference the other windows and their controls.
While this approach may solve the synchronization problem, it introduces others. First, because users may choose to open the windows in any order (or not open them at all), the Inventory List window becomes burdened with not only having to keep track of which specific windows are depending on it, but also what those windows are doing with its information.
As this type of tightly coupled design is spread throughout the system, maintenance of the application becomes difficult. Developers working on the display windows may not be aware that other windows are making direct references to its controls. As new features are added to the system, old features can begin to break. Also, if a new display window is needed, the other windows need to be updated to handle its synchronization. This tight coupling also has a negative effect on reuse. The Inventory List window would not function well in another context without modifications.
The desired design would allow enhancements and additions to the windows without changing existing windows in the system. To achieve these goals, windows must be loosely coupled--that is, they must minimize direct references to one another. This is where design patterns are extremely useful. In their book Design Patterns: Elements of Reusable Object-Oriented Software (Addison-Wesley, 1995) Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides provide a catalog of general-purpose object-oriented design patterns. These design patterns can be used to solve many common object-oriented design problems. In reviewing the pattern catalog, we discovered a pattern referred to as the "Observer." This pattern, which was also presented in the article "Observations on Observer," by Erich Gamma and Richard Helm (Dr. Dobb's Sourcebook, September/October 1995), describes a register-and-notify mechanism that provides indirect communication among objects.
According to Design Patterns, the Observer pattern "Define[s] a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically." In other words, the Observer pattern is designed so that one or more independent objects (Observers) watch or observe another object (the Subject) and perform some action based on changes in, or actions taken by, the subject.
The key to the Observer pattern is the registration and notification concept. An object that needs to observe another object registers its interest with that object. When the observed object is subsequently modified, it sends a message to each of its registered objects notifying them that a change has occurred. Each notified object then queries the subject object for the modification and performs the appropriate action to synchronize itself with the new state of the subject.
In Figure 2, a class diagram depicts the structure of the Observer pattern using OMT notation. As you can see, the Observer pattern consists of four classes:
At run time, Observers register with a Subject, indicating their interest in the Subject. As the Subject changes, it invokes its Notify() operation (defined in the superclass), which invokes the Update() operation on all registered Observers. Polymorphism is used here, allowing the Subject to only need to know about an abstract Observer class. This is the key to achieving the loose coupling of windows. Upon receiving the Update() message, each Observer interrogates the Subject for its new state, and then, maintaining encapsulation, updates itself accordingly.
Since many PowerBuilder applications are built with some type of class library or framework, we'll assume all our windows are inherited from a common ancestor window. This being the case, we can implement the Subject and Observer classes within this common ancestor window, which we'll call w_master. By implementing the pattern this way, all windows inherited from w_master can act as a subject, an observer, or both. The result is reflected in Figure 3. Referring to Figure 2, the association between the Subject class and the Observer class can be implemented as an instance variable in w_master, declared as an array of itself. This variable (iw_observers[]) will be used to hold references to each registered object. The operations of both the Subject and Observer classes are implemented as window functions of w_ master. The f_Attach() and f_Detach() functions are responsible for maintaining the array of registered objects, while f_ Notify() is responsible for looping through the array and invoking the Update() operation on each element. The Update() operations on w_master and all observing windows were renamed to f_Refresh() to avoid confusion with the Update() function of the datawindow. In the concrete windows, the associations between the ConcreteObservers and the ConcreteSubject are implemented as instance variables in each of the ConcreteObservers: w_picture, w_piechart, and w_bargraph (see Listing One).
The menu is used to open the observing windows and uses the f_SetSubject() function to set their subject to w_inventory (see Listing Six). Within f_SetSubject, the iw_ subject variable is set to hold a reference to w_inventory, then the window registers its interest in w_inventory by calling the f_Attach() function, using itself as an argument (see Listings Three, Four, and Five).
The notification process needs to begin when the row focus changes in the w_ inventory window's datawindow. The script in this event simply calls w_inventory's f_Notify() function (inherited from w_ master).
Finally, each observing window implements its own f_Refresh() window function to process the change. This function's purpose is to retrieve the current state of w_inventory (for instance, what are the quantity values for the selected item?), then refresh the window's state to reflect that of w_inventory. Encapsulation is achieved by the creation and use of the access functions provided by w_inventory (see Listing Two).
As a test of the design's loose coupling, the w_bargraph window was actually added as an afterthought. No existing code was changed. The w_bargraph window was created by inheriting from w_master. The appropriate script was added to have it register with the subject and its f_Refresh() function was scripted to reflect the subject's state.
Several implementation variations are worth mentioning. The observing windows could also perform updates to the subject window. In this case, the subject would again simply call its f_Notify() function to alert all registered observers of any change. You also have options for establishing the reference to the subject from the observing windows. We chose to implement the f_SetSubject() function, but this could be performed in other ways, depending on whether it's appropriate for the observing windows to initiate relationships to the subject. The detaching of observers from the subject is also specific to your application. We chose to close all observers when the subject closes.
Design patterns can take much of the guesswork out of making design decisions. They can help validate your design ideas and can provide a starting point for weighing trade-offs and contradictions presented during object-oriented system development. Putting design patterns to work in PowerBuilder involves mapping the generic object-oriented constructs present in the patterns to PowerBuilder-specific constructs. In this example, we implemented a variation of the Observer pattern in PowerBuilder (available electronically; see "Availability," page 3). In making the translation, the pattern was modified slightly to fit within common PowerBuilder development practices. The important thing to remember while working with patterns is that they only present a generic solution to a design problem. In many cases, the actual implementation varies significantly from the basic pattern. If you don't take everything literally, and experiment a little, design patterns can be invaluable when developing PowerBuilder applications.
Gamma, E., R. Helm, R. Johnson, J. Vlissides. Design Patterns: Elements of Reusable Object-Oriented Software, Reading, MA: Addison-Wesley, 1995.
Coplien, J. and D. Schmidt, eds. Pattern Languages of Program Design, Reading, MA: Addison-Wesley, 1995.
Powersoft
561 Virginia Road
Concord, MA 01742-2732
508-287-1500
http://www.powersoft.com
MetaSolv Software
14900 Landmark, Suite 530
Dallas, TX 75240
214-239-0623
http://www.metasolv.com
w_master -- Ancestor window for subjects and observers instance variables // holds references to attached observers w_master iw_observers[] window functions: public function boolean f_attach (w_master aw_observer) public function boolean f_detach (w_master aw_observer) public function boolean f_notify () public function boolean f_refresh () public function boolean f_attach (w_master aw_observer); // purpose: attach the passed object as an observer // add the passed object to next slot in the array iw_observers[upperbound(iw_observers) + 1] = aw_observer // tell new observer to refresh itself based on current state of subject aw_observer.f_refresh() return true public function boolean f_detach (w_master aw_observer); // purpose: detach the passed object, removing it from list of known observers // declare tmp array to hold observers still attached w_master lw_tmp_observers[] integer li_total_observers, li_index, li_tmp_index = 1 // get total number of observers li_total_observers = upperbound(iw_observers) // loop through observers and tranfer them to the tmp // array except for the passed observer for li_index = 1 to li_total_observers if iw_observers[li_index] <> aw_observer then lw_tmp_observers[li_tmp_index] = iw_observers[li_index] li_tmp_index = li_tmp_index + 1 end if end for iw_observers = lw_tmp_observers return true public function boolean f_notify (); // purpose: call the f_refresh() function on all attached observers. // this function should be called whenever changes are made to this subject integer li_index, li_totalobservers // get the total number of attached observers li_totalobservers = upperbound(iw_observers) // tell all attached observers to refresh for li_index = 1 to li_totalobservers iw_observers[li_index].f_refresh() end for return true public function boolean f_refresh (); // purpose: resync to new state of the subject this function should be // implemented in descendants to refresh based on changes to subject object return true
w_inventory -- Inventory list window as the subject controls: dw_items from datawindow within w_inventory window functions: public function string f_getitemname () public function long f_getonhand () public function long f_getbackordered () public function long f_getonorder () public function long f_getreserved () public function string f_getitemname (); // purpose: return the name of the selected item // returns: string - name of selected item return dw_items.getitemstring(dw_items.getrow(),"itemname") public function long f_getonhand (); // purpose: return onhand value of the selected item // returns: long - onhand value return dw_items.getitemnumber(dw_items.getrow(),"onhand") public function long f_getbackordered (); // purpose: return backorder value of the selected item // returns: long - backorder value return dw_items.getitemnumber(dw_items.getrow(),"backordered") public function long f_getonorder (); // purpose: return onorder value of the selected item // returns: long - onorder value return dw_items.getitemnumber(dw_items.getrow(),"onorder") public function long f_getreserved (); // purpose: return reserved value of the selected item // returns: long - reserved value return dw_items.getitemnumber(dw_items.getrow(),"reserved") window events: on open // initialize the datawindow to the first row dw_items.setrow(1) dw_items.postevent(rowfocuschanged!) end on on close // close all observer windows do while upperbound(iw_observers) > 0 close(iw_observers[1]) loop end on control events: on dw_items.rowfocuschanged // purpose: highlight the selected row and notify observers // highlight the chosen row this.selectrow(0, false) this.selectrow(getrow(), true) // tell the window to notify all observers of the change parent.f_notify() end on
w_picture -- Item picture window as an observer controls: p_item from picture within w_picture instance variables: private: // holds a reference to the subject object w_inventory iw_subject window functions: public function boolean f_refresh () public function boolean f_setsubject (ref w_master aw_subject) public function boolean f_refresh (); // purpose: update ourself to reflect the current state of the subject // get the item name from the subject and set the bitmap on picture control. if isvalid(iw_subject) then p_item.picturename = iw_subject.f_getitemname() + ".bmp" end if return true public function boolean f_setsubject (ref w_master aw_subject); // purpose: set the subject object // set the instance variable used to hold the subject iw_subject = aw_subject // attach ourself with the subject if isvalid(iw_subject) then iw_subject.f_attach(this) end if return true window events: on close // purpose: detach with the subject // detach with the subject if isvalid(iw_subject) then iw_subject.f_detach(this) end if end on
w_piechart -- Item pie chart window as an observer controls: dw_pie from datawindow within w_piechart instance variables private: // holds a reference to the subject object w_inventory iw_subject window functions: public function boolean f_refresh () public function boolean f_setsubject (ref w_master aw_subject) public function boolean f_refresh (); // purpose: update ourself to reflect the current state of the subject // return if not attached to a subject if not isvalid(iw_subject) then return false end if // set the piechart data elements based on the subject. dw_pie.setitem(1, "quantity", iw_subject.f_getonhand()) dw_pie.setitem(2, "quantity", iw_subject.f_getbackordered()) dw_pie.setitem(3, "quantity", iw_subject.f_getonorder()) dw_pie.setitem(4, "quantity", iw_subject.f_getreserved()) return true public function boolean f_setsubject (ref w_master aw_subject); // purpose: set the reference to the subject and attach // set the instance variable used to hold the subject iw_subject = aw_subject // attach ourself with the subject if isvalid(iw_subject) then iw_subject.f_attach(this) end if return true window events: on close // detach with the subject if isvalid(iw_subject) then iw_subject.f_detach(this) end if end on
w_bargraph -- Item Bar chart window as an observer controls: dw_bar from datawindow within w_bargraph instance variables: private: // holds a reference to the subject object w_inventory iw_subject window functions: public function boolean f_setsubject (ref w_master aw_subject) public function boolean f_refresh () public function boolean f_setsubject (ref w_master aw_subject); // purpose: set the reference to the subject and attach // set the instance variable used to hold the subject iw_subject = aw_subject // attach ourself with the subject if isvalid(iw_subject) then iw_subject.f_attach(this) end if return true public function boolean f_refresh (); // purpose: update ourself to reflect the current state of the subject // return if not attached to a subject if not isvalid(iw_subject) then return false end if // set the barchart data elements based on the subject dw_bar.setitem(1, "quantity", iw_subject.f_getonhand()) dw_bar.setitem(2, "quantity", iw_subject.f_getbackordered()) dw_bar.setitem(3, "quantity", iw_subject.f_getonorder()) dw_bar.setitem(4, "quantity", iw_subject.f_getreserved()) return true window events: on close // detach with the subject if isvalid(iw_subject) then iw_subject.f_detach(this) end if end on
m_frame -- Main Frame menu type m_inventory from menu within m_view type m_picture from menu within m_view type m_piechart from menu within m_view type m_barchart from menu within m_view m_inventory from menu within m_view on clicked; // purpose: open w_inventory window which will become subject in this example // open inventory window as subject opensheet(w_inventory, w_Frame, 0, Original!) m_picture from menu within m_view on clicked // purpose: open w_picture window as an observer and set subject to w_inventory // open the w_picture OpenSheet(w_picture, w_frame, 0, Original!) // set its subject to w_inventory w_picture.f_setsubject(w_inventory) end on m_piechart from menu within m_view on clicked // purpose: open w_piechart window as observer and set subject to w_inventory // open the w_piechart window OpenSheet(w_piechart, w_frame, 0, Original!) // set its subject to w_inventory w_piechart.f_setsubject(w_inventory) end on m_barchart from menu within m_view on clicked // purpose: open w_bargraph window as observer and set subject to w_inventory // open the w_bargraph window OpenSheet(w_bargraph, w_frame, 0, Original!) // set its subject to w_inventory w_bargraph.f_setsubject(w_inventory) end on
Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.