PowerBuilder Authors: Chris Pollach, Yeshim Deniz, Jayaram Krishnaswamy, Kevin Benedict, Avi Rosenthal

Related Topics: PowerBuilder

PowerBuilder: Article

Project Best Practices

Project Best Practices

You're all ready to go. The project has been approved and you're in charge of setting up the development environment.

This article discusses some of the recent changes in the PowerBuilder project development process, and how your team can leverage both old and new techniques to make your application a success. Although no specific coding standards will be discussed, you'll learn how to set up a PowerBuilder development framework. Basically, a full-cycle PowerBuilder project includes the following:

  • Setting up the workspace and target(s)
  • Determining how to manage development libraries
  • Coding framework - where to put the code
  • Evaluating third-party development tools
PowerBuilder Workspaces
In previous versions of PowerBuilder, the developer was allowed to work on one application at a time. While this "one-at-a-time" model made a great deal of sense at the time, it would be nice if objects from other applications could be opened, closed, and compared with objects currently within your library path. For example, larger PowerBuilder applications have at least two versions - development and current release. To compare objects within these two versions, the developer would usually have to toggle back and forth between them. Certainly a pain in the neck. What was needed was the ability to review and evaluate the attributes, methods, and properties of a multitude of PowerBuilder objects, whether or not they were part of your project.

Since the release of PowerBuilder 8, Sybase has solved this problem with the creation of a PowerBuilder workspace. From a workspace, a developer can simultaneously work on multiple PowerBuilder applications.

Workspace Properties
From the System Tree, workspace properties can be accessed via a simple right-click. There are three major workspace property categories.

All targets in the workspace are listed here along with a checkbox that determines if the target will be built or deployed when the programmer chooses one of those two options on the workspace level. In a production environment, workspaces usually contain more than one target - development and deployment. It's a common practice to perform a full build on the development libraries, then when all errors are fixed, copy over the current deployment libraries and deploy the workspace after the deployment target is checked. At any rate, configuration of this workspace property will determine which targets get built or deployed when performed from the workspace in the System Tree.

Deploy Preview
All targets and their "buildable" projects are listed under this tab. Note that all projects within a target will be deployed if the target is checked in the Target tab. In most client/server PowerBuilder applications, one application project is included for each target.

Source Control
All source control properties such as the Source Control System and related attributes are set here. PB Native check-in/out will be discussed later. Most client/server PowerBuilder applications use source control. If your project uses third-party source control, such as PVCS by Intersolv, this is where it's configured. Third-party source control tools will not be covered here.

PowerBuilder Targets
The PowerBuilder workspace consists of targets, which generally fall into one of three categories

  • Windows applications
  • Web applications
  • EAServer components
The focus of this article will be the development and deployment of Windows applications. If you are a client/server developer just coming up to speed with the recent PowerBuilder versions, the application object that you're familiar with is an object no more. It's become a target. You're allowed to create new or add existing applications as targets within your workspace. Remember, when deploying a workspace via the System Tree all objects contained within the workspace are built. When building Web or n-tier applications, workspaces containing one object are common, as distributed applications are deployed (or maintained) component by component. Figure 1 illustrates a workspace containing one project.

The System Tree
The System Tree window provides developers with a Tree View representation of all targets within the open workspace. This Tree View has the same look and feel as Windows Explorer and supports drag-and-drop operations. The System Tree provides functionality that was previously provided in the Library Painter, Object Browser, and Project Painter.

The System Tree provides the developer with a "one stop" place for developing and deploying applications. As far as development is concerned, you no longer have to open an object to understand it. All information about its functions, events, and properties is easily accessible within the System Tree. Also, the System Tree can be used to perform builds. This is a boon to developers who have projects containing more than one executable. If, for example, a workspace contains three Windows application workspaces, and each contains its own project object, all three applications can be built simultaneously if the programmer chooses to "build" the entire workspace.

Library Organization
After the workspace and target are created, the next step is to determine how the application's objects will be organized within the libraries. There is no correct or "fast" answer to this. However, there is a set of guidelines that can be used that are considered "best practices," based on many successful PowerBuilder projects developed throughout the years. The application's library path and the fashion in which it contains the objects quite simply is a judgment call influenced by the following factors:

  • How many developers will be programming this application?
  • Will the application be sharing objects from a different application?
  • How will the application be distributed?
  • What deployment model will be employed?

    Library Size
    Before we go further, I would like to dispel a myth about the size of the libraries and how many objects they can contain. Just like an old wives' tale or urban myth that won't go way, the same can be said of library size constrictions. These restrictions were considered good guidelines in PowerBuilder's old days - up until version 4 - but can now be safely ignored. Libraries can be organized without regard to how big they can get or how many objects they may contain. Library size may be a project limitation, but is no longer a limitation of the PowerBuilder IDE.

    Developer Access
    While developing applications, the main objective in creating your library list should be to provide easy and intuitive access to the developers. Libraries should not be created arbitrarily. Nothing is more confusing to the developer than looking for an object in an overly inflated library list. Within reason, make your library list as small as possible - but not any smaller. Obviously, if too few libraries are used, the libraries will wind up with too many unrelated objects making them disorganized. Think of PowerBuilder libraries as being an empty filing cabinet. It's up to you to populate them with folders with meaningful names, and to fill them with the appropriate related documents.

    Once a library structure has been adopted, it's important that all team members agree to stick with the standard. There are few things as destructive as rogue developers arbitrarily adding libraries or changing the search path without the endorsement of the rest of the team. The use of an object librarian is particularly helpful in maintaining order.

    Shared Objects
    By now you should already know that PowerBuilder applications make extensive use of object-oriented techniques such as inheritance. Indeed, this flexibility is the reason many shops choose to use PowerBuilder in the first place. With this in mind, most PowerBuilder applications use shared components. The degree of sharing depends on your organization. Some applications use a full-blown application framework, such as the PFC. Other shops may have a smaller "homegrown" framework containing proprietary business objects. In any case, these libraries should be at the top of the application's library search path. Because of the shared nature of these libraries, most of the time they are read-only or may even be compiled into PBD files.

    When Using Read-Only Libraries or PBD Files
    It's important to point out that when a library is read-only, changes may not be made to any object within it. If you attempt to make changes, you'll be greeted with a "Save of Object Failed" message. Also, read-only libraries must be made readable before any regeneration or build. When using PBD files in the library search path, objects within them cannot be viewed, because they're already compiled. Also, debug stops may not be placed in compiled objects. Because PBD files are compiled, their use makes the application run faster in the development environment.

    Remaining Library Structure
    With the shared objects at the top of the path, it's time to create and organize the rest of the libraries. But how? Before we continue I would like to point out a couple of definite no-nos.

    First of all, never group libraries by developer. For example, don't create a library called "Joe" and give it to Joe. Remember that the team is working in a shared, distributed development environment; there's no such thing as "my" objects and "your" objects.

    In addition, it's generally not a good idea to create libraries by object type, for example, a library for DataWindows, one for functions, etc. Within the library painter exists the functionality to sort objects by their PowerBuilder class. Also, by using a simple right-click, a filter criteria can be applied to show objects of a specific type.

    With the "don'ts" out of the way, let's focus on a correct way to design the library structure. First ask yourself this question: "How will this application be deployed?" Most large applications can be broken down into functional, if not technical, areas. I have worked on many large projects that were broken down into "modules," such as the Accounting Module and Reporting Module. When designing the application, these modules are often taken into consideration. For example, as the application goes into production, user requests for changes in the Reporting Module were expected to be a frequent everyday occurrence, with an expected quick turnaround. User requests for the modifications of the Accounting Module were expected to be few and far between, with weeks or months of programming required before the next released version.

    Actually, the design of the aforementioned application dictates how the libraries should be organized. Since the reporting portion would have more changes than the accounting portion, at least two libraries should be created, one for the reporting objects and one for the accounting. This way, when changes to the Reporting Module are made, the library can be deployed as a PBD (or DLL) file, while leaving the other dynamic libraries intact. By organizing libraries by function, only the libraries that contain objects that have changes need to be deployed - a pretty sound plan. Now multiply our example by dozens of modules. It starts to become obvious that our application is really a collection of smaller apps. By employing this modular approach to deployment, we're constantly upgrading portions of our application without unintentionally changing portions of existing stable code.

    Library Maintenance
    So you settled on a library structure and that's the end of it, right? Not really. PowerBuilder libraries should be maintained on a regular basis. At the bare minimum, you should have a schedule to optimize and regenerate them. These two steps are easy and don't require a lot of time. A little maintenance will keep you out of trouble down the road by keeping your libraries shiny and happy.

    As you already know, a PowerBuilder library is a collection of PowerBuilder objects. When you create a PowerBuilder object, it must be placed within a library. What you may not know is that when an object is either moved or deleted, the object code is still contained in the library; the object is still there, only its reference has disappeared. This is not unlike how the Windows operating system deletes files. After weeks and months of PowerBuilder development, the libraries continue to grow in size, and objects long since deleted take up much of that space. Think of library optimization as a sort of defragmenter. It cleans up deleted objects, making your library smaller and thus more efficient. Libraries can be optimized via the System Tree or the Library Painter.

    As project development moves forward, there's always the possibility that a working Object references a deleted object. For example, object A references a function on object B. Object B is deleted. Object A now has a problem - it references a null object. The time to find this out is not when performing a build. On a weekly basis, every library should be regenerated, and all resulting problems should be resolved before attempting to deploy the application.

    Coding Framework
    In this section I'll discuss "best practices" as it refers to a programming architecture. While no specific conventions will be introduced, the focus will be on creating a tight, object-centric application.

    All good object-oriented applications are highly encapsulated. Encapsulation is a programming term that refers to an object hiding its internal properties and functionality from other objects. A bit obscure? Let me try again. An object should never be able to manipulate a property on another object. Instead, objects should have methods (functions and events) that manipulate properties on themselves. In other words, object X should never be allowed to manipulate a property on object Y. To be encapsulated, object Y should have a function that manipulated properties on itself. Object X should use this function to modify the property on object Y. Object functions that act in this nature are called a public interface. A public interface is a function that other objects can call to perform the desired action on object Y. When building objects, they should be encapsulated. Other objects should not directly change internal properties. Still confused? Let's look at a nonprogramming example.

    Encapsulated Systems
    If you look around you, chances are that you're looking at different encapsulated systems. Take the electrical system in your house. This system comprises many components. Electricity enters the house through the fuse box. From this point it's distributed through wires into different parts of the house. Wires then carry the electricity into each room where they terminate at an outlet, switch, or lighting fixture. When using electricity in your home, you usually give little thought to how the electrical system works. You just plug an appliance into an outlet, screw a light bulb into a socket, or hit a switch to illuminate a lamp. The outlet, light fixture, and wall switch are the only things to be concerned about within this system. With this in mind, they can all be called public interfaces into the electrical system. When used, these interfaces perform some kind of action that's usually unknown to the user. How the interface reroutes electricity is of no interest to the person turning on the light switch. It can be said that the actual functionality behind the action of hitting the light switch is hidden from the person performing the action. The user is interested only in the end result, the public interface (the light going on, the electrical appliance running, etc.). Also, to use the electricity in your house, you must use one of these provided public interfaces. Because of these reasons, it can be said that this system is encapsulated. All encapsulated systems possess the following properties:

  • Have a common public interface
  • Hide the internal functionality and properties from the user (or other objects)
  • Only perform internal actions via a public interface

    When used in PowerBuilder, encapsulation works as described. When programming, strive to give your objects a public interface. Don't allow properties to be directly changed or accessed from outside objects. Always provide an interface to perform such changes. Remember, encapsulation refers to a programming design practice, not to a specific PowerBuilder programming rule. Most of you are familiar with the term "garbage in, garbage out." It's up to you, the programmer, to make sure that your object design is encapsulated.

    A program needs to change two instance variables on the object Trade. There are two ways to do this. One is encapsulated; one is not.

  • Nonencapsulated
    The following code example exists in the calling program:

    Trade.iiShares = 100
    Trade.iDecPrice = 76.78

    This method breaks a cardinal rule of encapsulation. Never allow an object to directly access or manipulate a property on another object. The example below provides an encapsulated way to set the values of the member variables on the object Trade.

  • Encapsulated
    A program calls an object function on Trade to set the value of the two instance variables. First, two object functions must be created in the Trade class:

    Function 1:

    Public Function setShares(ByVal shares As String)
    iiShares = shares
    End Function

    Function 2:

    Public Function setPrice(ByVal price As Decimal)
    dblPrice = price
    End Function

    Now when a program (or any other object) needs to modify two instance variables on Trade, it would use these two functions. They're called a public interface into Trade. In other words, no program or objects should be able to set the two instance variables without calling the two public functions. However, to make this example totally encapsulated, one more step needs to be taken.

    The default access to instance variables in PowerBuilder is public (more on access protection later in this article). Variables declared without a private or protected keyword have public access. They're accessible from within any script that contains their declaration and from anywhere else in the same application. As you'll find out later, to be correctly encapsulated, these member variables need to have protected access. Variables declared with the protected data members have what's called protected access. They're accessible only from within their own class or from a derived class. Then our declarations would look like:

    Protected Integer iiShares
    Protected Decimal{2} idecPrice

    With the variables protected, no other object can directly change them (a compile error would result). Since the access is protected, other objects are forced to use the functions on Trade to set the value of the two instance variables. Finally, Trade is properly encapsulated. In real life, functions that encapsulate the properties and behavior are more complicated; however, this is still a good example because the concept of encapsulation remains the same regardless of the complexity involved.

    Final Thoughts on Encapsulation
    Well, that about does it. Our Trade class is completely encapsulated. We had to do a bit of work, but our encapsulated object is designed better than its unencapsulated counterpart. Advantages and beneficial characteristics of encapsulation include:

  • Objects behave in strict predefined ways
  • Objects have a common public interface
  • Objects can only change properties on themselves
  • Objects tend to be easier to maintain, debug, and reuse
  • Encapsulation hides the complexity of objects' functionality from other objects

    Remember, encapsulation is a good thing. Highly encapsulating objects is considered a good programming practice and the key to object-oriented software design. When building objects, always strive to hide their internal workings and provide a public interface to change the internal properties. Remember, good encapsulated designs will lead to good encapsulated objects.

    Where Does the Code Go?
    Good coding requires good encapsulation techniques. So where do you place your properly encapsulated code? I'll discuss three common places for coding PowerBuilder functions as well as the advantages/disadvantages of each. Remember, coding placement is a guideline, not a rule. Enjoy in moderation.

    Global Functions
    When functions are created via the function painter, they're considered global. Usually, when programming in an OO language, the term "global" is a four-letter word. The application has no say-so over the "life" of a global function. It gets loaded into memory every time your code uses it. Also, if the function is large, it may require multiple I/O operations to load it.

    As a general rule global functions are to be avoided - but not at all costs. Other disadvantages of global functions:

    • Cannot be overridden or overloaded
    • Cannot be grouped by functionality
    Ancestor Functions
    Most well-planned applications have an inheritance hierarchy. Placing functionality (functions) in an inheritance hierarchy from generic (in abstract classes) to more specific (in derived classes) offers a more tightly bound object-orientated approach over the use of global functions. By placing functions in an inheritance hierarchy, you can take advantage of overriding and overloading. Also, functions in this fashion are slightly faster. For example, if an application contains an abstract DataWindow for the ancestor of all DataWindows, and if that DataWindow contains a function called of_print(), that function is loaded into memory the first time a DataWindow is instantiated. When subsequent DataWindows are instantiated, all ancestor functions (such as of_print) don't have to be reloaded. A good thing right?

    Ironically, the way functions are loaded into memory is also the drawback of this model. When placed into an inheritance hierarchy, all functions are loaded - whether you need them or not (when an ancestor object is loaded). When placing functions in an inheritance hierarchy, strive only to include those that are used the most. If a function won't be used (let's say) 80-90% of the time, you probably shouldn't place it in an ancestor. Where do you put it?

    Nonvisual Objects
    This is probably the best place to put functions. First and foremost, functions can be grouped by functionality instead of inheritance hierarchy. For example, if you had a DataWindow and a window that needed to access printer functions, they would be able to access them within the NVO. If the functions were placed in ancestors, they may have to be duplicated as the window and DataWindow would probably have different ancestors. Also, when functions are grouped on an NVO, they can be created and destroyed as needed.

    Comprehensive Example
    With these guidelines in mind, let's go over a (simple) real-world example. When designing an application, it's determined that there will be many places where a user will want to "print" something. It could be:

    • A full screen print
    • The current window
    • Contents of the active DataWindow
    We could make all of them global functions. This approach will work but would be cumbersome. What we need are three functions called of_print, each with a different signature:
    • of_print(Environment)
    • of_print(Window)
    • of_print(DataWindow)
    Global functions can't be overloaded. We can discount global functions from our design.

    Next, we can consider placing the functionality in our inheritance hierarchy. In this scenario, a common ancestor must be found for each object that needs to call a print function. By looking at the types of objects that need to use a print function, it's unlikely that these functions can be placed in a common ancestor. Clearly, these functions can't be placed in the inheritance hierarchy.

    That leaves the last method: place all the print functions in an NVO. This way, print functions can be grouped by functionality (the NVO can be called n_cst_print) and the functions can be overloaded accordingly. Since the functions are now correctly grouped in a class by themselves, they can be accessed by creating an instance of the NVO, calling the desired print function, then destroying the object when it's no longer needed. Technically, the object doesn't even need to be explicitly destroyed anymore, but for me, old habits die hard!

    What Code Goes in the Object?
    Of course, applications are also made up of object-specific code. In those cases, don't hesitate to add functions directly on the object level. Ask yourself this question: "Is the functionality generic or reusable?" If the answer is no, place it directly on the object. Try not to overabstract the application. Some go directly within the object. Things should be as simple as possible, never any simpler. Add the function directly to the object and be done with it.

    What About Events?
    With all this talk about functions, you may be wondering what code goes in events. While I'm not going to go into all the differences between functions and events, from a best practices perspective, significant differences do exist. These differences will determine where to put the code - in a function or an event.

  • Events are always PUBLIC. Functions can be defined as PUBLIC, PRIVATE, or PROTECTED. This is a very important consideration when it comes to encapsulation. If a method shouldn't be exposed to other objects, you should use a function and make it PROTECTED or PRIVATE.
  • PowerBuilder will generate a system error if a dynamically called function doesn't exist at runtime. But, it won't crash if a dynamically called event does not exist. It may sound silly, but there are times when you may want to dynamically fire off a method conditionally. If the method exists, fire it off; if it doesn't, who cares? In this scenario, use an event over a function.
  • Events cannot be overloaded like functions. If your design requires methods to be highly overloaded, use a function over an event.
  • When using Visual User Objects, after they're placed on a window, their functions can't be overloaded/overridden, and the functions can never be extended. If you need to extend functions in a user object, the function probably should be an event.
  • Events can be mapped to system/user actions while functions cannot. If, for example, a method needed to fire every time the user moused over a control, use an event over a function.

    PowerBuilder, like many recent languages such as Java and VB.NET, supports object-oriented features such as inheritance and polymorphism. Actually, PowerBuilder is the first major client/server language to incorporate these features (snickering allowed here). Project abstraction refers to the design-time task of determining an inheritance hierarchy for objects used within the application, specifically, the window hierarchy and the overloaded functions contained within them. Before I go on, I would like to point out that abstraction is a sound design practice. It allows like functionality to be implemented in ancestor classes, thus providing transparent (for the most part) functionality in extended classes. Indeed, a properly abstracted application pays off in spades, in decreased coding time as well as better and more stable maintainability down the road.

    Abstraction is a proven design technique, but it can be taken too far. Sometimes, as the project progresses, there's some functionality that comes up that doesn't fit into our cute cookie-cutter plan. When confronted with this situation there are two real options: take the purist object-oriented approach and redo your abstraction model, or say the heck with it and place this functionality in some other logical place in your code. Which one should you do? The answer may surprise you. I usually go by this rule: if most (90%) of common functionality can be buried in ancestors, consider your project a success. Always make room for exceptions. Think of your abstraction design as a blueprint - not the Ten Commandments. Sometimes when building a house, things need to be done that are off the blueprint. Does that mean you redesign the entire blueprint? Of course not. If adding an electrical outlet requires that a bedroom be moved, you really have to evaluate what you are trying to accomplish. In the spirit of getting the job done, it's better to make an exception to the blueprint and just implement the functionality. Overabstraction increases development time. Java, for example, is so perfectly abstracted, it can be very difficult to accomplish simple tasks. This approach may make the ivory tower OO designer cringe (I can already feel the e-mails coming), but trying to fit a square peg into a round hole has the following consequences:

  • A simple exception to your abstraction design can be implemented in short order; changing the design will most likely take longer.
  • Exception management should never rule project abstraction. Apply the 80/20 rule.
  • Anytime an abstraction design is changed after coding has begun, you risk the chance of dramatically influencing tested working code. For example, if ancestor classes are changed, we need to test each and every descendant.

    When deviating from the plan, comment your code, explaining why you had to make an exception to the abstraction model. Posterity will forgive you.

    Balancing Your Design
    In the spirit of overabstraction, strive to keep your development goals balanced. Remember that all applications have the following considerations:

  • Speed
  • Maintainability
  • Footprint (how large the app is)
  • RAD architecture (how long will it take to build this application)

    Not one of these considerations is more important than another. When designing your application, always look for acceptable trade-offs. If you gain 10 units of maintainability at the expense of one unit of speed - do it. Avoid monomaniacal practices. My favorite are "the speed demons" who strive to make the application as fast as possible, often sacrificing maintainability. If your application can be made .00001 of a second faster, but will take an additional month to code, well, that's not a good trade-off.

    Code for the Rules - Not the Exceptions
    If you're a perfectionist like most programmers, you strive to see the application as a whole, not the sum of its parts. Most applications follow the 80/20 rule. Most user functionality is in the 80% portion, i.e., 80% of the application can be designed and programmed in 20% of the total project time. Always use this rule to your advantage. Take a hard look at how the users will actually use the application, and apply your design efforts accordingly. At first, focus on the core functionality. As the application becomes stable, start implementing the fringe stuff. Many teams focus on trying to program the application "all at once." Try to avoid this practice. If your team is spending too much time on "once in a blue moon" scenarios, you may be sacrificing time in basic core (functionality) design. Remember that programmers were put on this earth to solve problems, not create them.

    Final Thoughts
    That's the long and short of it. A little planning now will ensure an organized, stable development environment. After your platform, abstraction model, and tools have been chosen, you're ready to start coding. Before you do, however, read up on coding standards such as variable scope and naming conventions. The fun is about to begin.

  • More Stories By Bob Hendry

    Bob Hendry is a PowerBuilder instructor for Envision Software Systems and a frequent speaker at national and international PowerBuilder conferences. He specializes in PFC development and has written two books on the subject, including Programming with the PFC 6.0.

    Comments (2)

    Share your thoughts on this story.

    Add your comment
    You must be signed in to add a comment. Sign-in | Register

    In accordance with our Comment Policy, we encourage comments that are on topic, relevant and to-the-point. We will remove comments that include profanity, personal attacks, racial slurs, threats of violence, or other inappropriate material that violates our Terms and Conditions, and will block users who make repeated violations. We ask all readers to expect diversity of opinion and to treat one another with dignity and respect.