About this chapter
This chapter describes how to implement selected object-oriented programming techniques in PowerBuilder.
Classes, properties, and methods
In object-oriented programming, you create reusable classes to perform application processing. These classes include properties and methods that define the class's behavior. To perform application processing, you create instances of these classes. PowerBuilder implements these concepts as follows:
-
Classes
PowerBuilder objects (such as windows, menus, window controls, and user objects)
-
Properties
Object variables and instance variables
-
Methods
Events and functions
The remaining discussions in this chapter use this PowerBuilder terminology.
Fundamental principles
Object-oriented programming tools support three fundamental principles: inheritance, encapsulation, and polymorphism.
-
Inheritance
Objects can be derived from existing objects, with access to their visual component, data, and code. Inheritance saves coding time, maximizes code reuse, and enhances consistency. A descendant object is also called a subclass.
-
Encapsulation
An object contains its own data and code, allowing outside access as appropriate. This principle is also called information hiding. PowerBuilder enables and supports encapsulation by giving you tools that can enforce it, such as access and scope. However, PowerBuilder itself does not require or automatically enforce encapsulation.
-
Polymorphism
Functions with the same name behave differently, depending on the referenced object. Polymorphism enables you to provide a consistent interface throughout the application and within all objects.
Visual objects
Many current applications make heavy use of object-oriented features for visual objects such as windows, menus, and visual user objects. This allows an application to present a consistent, unified look and feel.
Nonvisual objects
To fully benefit from PowerBuilder's object-oriented capabilities, consider implementing class user objects, also known as nonvisual user objects:
Standard class user objects
Inherit their definitions from built-in PowerBuilder system objects, such as Transaction, Message, or Error. The nvo_transaction Transaction object in the Code Examples sample application is an example of a subclassed standard class user object. Creating customized standard class user objects allows you to provide powerful extensions to built-in PowerBuilder system objects.
Custom class user objects
Inherit their definitions from the PowerBuilder NonVisualObject class. Custom class user objects encapsulate data and code. This type of class user object allows you to define an object class from scratch. The u_business_object user object in the Code Examples sample application is an example of a custom class user object. To make the most of PowerBuilder's object-oriented capabilities, you must use custom class user objects. Typical uses include:
-
Global variable container
The custom class user object contains variables and functions for use across your application. You encapsulate these variables as appropriate for your application, allowing access directly or through object functions.
-
Service object
The custom class user object contains functions and variables that are useful either in a specific context (such as a DataWindow) or globally (such as a collection of string-handling functions).
-
Business rules
The custom class user object contains functions and variables that implement business rules. You can either create one object for all business rules or create multiple objects for related groups of business rules.
-
Distributed computing
The custom class user object contains functions that run on a server or cluster of servers.
For more information, see Part 6, "Distributed Application Techniques".
PowerBuilder provides full support for inheritance, encapsulation, and polymorphism in both visual and nonvisual objects.
Creating reusable objects
In most cases, the person developing reusable objects is not the same person using the objects in applications. This discussion describes defining and creating reusable objects. It does not address usage.
PowerBuilder makes it easy to create descendant objects. You implement inheritance in PowerBuilder by using a painter to inherit from a specified ancestor object.
For examples of inheritance in visual objects, see the w_employee window and u_employee_object in the Code Examples sample application.
Example of ancestor service object
One example of using inheritance in custom class user objects is creating an ancestor service object that performs basic services and several descendant service objects. These descendant objects perform specialized services, as well as having access to the ancestor's services:
Figure: Ancestor service object
Example of virtual function in ancestor object
Another example of using inheritance in custom class user objects is creating an ancestor object containing functions for all platforms and then creating descendant objects that perform platform-specific functions. In this case, the ancestor object contains a virtual function (uf_change_dir in this example) so that developers can create descendant objects using the ancestor's datatype.
Figure: Virtual function in ancestor object
For more on virtual functions, see Other techniques.
Encapsulation allows you to insulate your object's data, restricting access by declaring instance variables as private or protected. You then write object functions to provide selective access to the instance variables.
One approach
One approach to encapsulating processing and data is as follows:
-
Define instance variables as public, private, or protected, depending on the desired degree of outside access. To ensure complete encapsulation, define instance variables as either private or protected.
-
Define object functions to perform processing and provide access to the object's data.
To do this
Provide this function
Example
Perform processing
uf_do_operation
uf_do_retrieve (which retrieves rows from the database)
Modify instance variables
uf_set_variablename
uf_set_style (which modifies the is_style string variable)
Read instance variables
uf_get_variablename
uf_get_style (which returns the is_style string variable)
(Optional) Read boolean instance variables
uf_is_variablename
uf_is_protected (which returns the ib_protected boolean variable)
Another approach
Another approach to encapsulating processing and data is to provide a single entry point, in which the developer specifies the action to be performed:
-
Define instance variables as private or protected, depending on the desired degree of outside access
-
Define private or protected object functions to perform processing
-
Define a single public function whose arguments indicate the type of processing to perform
Figure: Defining a public function for encapsulation
For an example, see the uo_sales_order user object in the Code Examples sample application.
Polymorphism refers to a programming language's ability to process objects differently depending on their datatype or class. Polymorphism means that functions with the same name behave differently depending on the referenced object. Although there is some discussion over an exact definition for polymorphism, many people find it helpful to think of it as follows:
Operational polymorphism
Separate, unrelated objects define functions with the same name. Each function performs the appropriate processing for its object type:
Figure: Operational polymorphism
For an example, see the u_external_functions user object and its descendants in the Code Examples sample application.
Inclusional polymorphism
Various objects in an inheritance chain define functions with the same name.
With inclusional polymorphism PowerBuilder determines which version of a function to execute, based on where the current object fits in the inheritance hierarchy. When the object is a descendant, PowerBuilder executes the descendant version of the function, overriding the ancestor version:
Figure: Inclusional polymorphism
For an example, see the u_employee_object user object in the Code Examples sample application.
PowerBuilder allows you to implement a wide variety of object-oriented techniques. This section discusses selected techniques and relates them to PowerBuilder.
In function overloading, the descendant function (or an identically named function in the same object) has different arguments or argument datatypes. PowerBuilder determines which version of a function to execute, based on the arguments and argument datatypes specified in the function call:
Figure: Function overloading
Global functions
Global functions cannot be overloaded.
Dynamic lookup
In certain situations, such as when insulating your application from cross-platform dependencies, you create separate descendant objects, each intended for a particular situation. Your application calls the platform-dependent functions dynamically:
Figure: Dynamic lookup
Instantiate the appropriate object at runtime, as shown in the following code example:
// This code works with both dynamic and // static lookup. // Assume these instance variables u_platform iuo_platform Environment ienv_env ... GetEnvironment(ienv_env) choose case ienv_env.ostype case windows! iuo_platform = CREATE u_platform_win case windowsnt! iuo_platform = CREATE u_platform_win case else iuo_platform = CREATE u_platform_unix end choose
Although dynamic lookup provides flexibility, it also slows performance.
Static lookup
To ensure fast performance, static lookup is a better option. However, PowerBuilder enables object access using the reference variable's datatype (not the datatype specified in a CREATE statement).
Figure: Static lookup
When using static lookup, you must define default implementations for functions in the ancestor. These ancestor functions return an error value (for example, -1) and are overridden in at least one of the descendant objects.
Figure: Ancestor functions overridden in descendant functions
By defining default implementations for functions in the ancestor object, you get platform independence as well as the performance benefit of static lookup.
Delegation occurs when objects offload processing to other objects.
Aggregate relationship
In an aggregate relationship (sometimes called a whole-part relationship), an object (called an owner object) associates itself with a service object designed specifically for that object type.
For example, you might create a service object that handles extended row selection in DataWindow objects. In this case, your DataWindow objects contain code in the Clicked event to call the row selection object.
To use objects in an aggregate relationship:
-
Create a service object (u_sort_dw in this example).
-
Create an instance variable (also called a reference variable) in the owner (a DataWindow control in this example):
u_sort_dw iuo_sort
-
Add code in the owner object to create the service object:
iuo_sort = CREATE u_sort_dw
-
Add code to the owner's system events or user events to call service object events or functions. This example contains the code you might place in a ue_sort user event in the DataWindow control:
IF IsValid(iuo_sort) THEN Return iuo_sort.uf_sort() ELSE Return -1 END IF
-
Add code to call the owner object's user events. For example, you might create a CommandButton or Edit>Sort menu item that calls the ue_sort user event on the DataWindow control.
-
Add code to the owner object's Destructor event to destroy the service object:
IF IsValid(iuo_sort) THEN DESTROY iuo_sort END IF
Associative relationship
In an associative relationship, an object associates itself with a service to perform a specific type of processing.
For example, you might create a string-handling service that can be enabled by any of your application's objects.
The steps you use to implement objects in an associative relationship are the same as for aggregate relationships.
Using user objects as structures
When you enable a user object's AutoInstantiate property, PowerBuilder instantiates the user object along with the object, event, or function in which it is declared. You can also declare instance variables for a user object. By combining these two capabilities, you create user objects that function as structures. The advantages of creating this type of user object are that you can:
-
Create descendant objects and extend them.
-
Create functions to access the structure all at once.
-
Use access modifiers to limit access to certain instance variables.
To create a user object to be used as a structure:
-
Create the user object, defining instance variables only.
-
Enable the user object's AutoInstantiate property by checking AutoInstantiate on the General property page.
-
Declare the user object as a variable in objects, functions, or events as appropriate.
PowerBuilder creates the user object when the object, event, or function is created and destroys it when the object is destroyed or the event or function ends.
Many applications use a DataWindow visual user object instead of the standard DataWindow window control. This allows you to standardize error checking and other, application-specific DataWindow behavior. The u_dwstandard DataWindow visual user object found in the tutorial library TUTOR_PB.PBL provides an example of such an object.
Since DataStores function as nonvisual DataWindow controls, many of the same application and consistency requirements apply to DataStores as to DataWindow controls. Consider creating a DataStore standard class user object to implement error checking and application-specific behavior for DataStores.