Selected Object-Oriented Programming Topics

About this chapter

This chapter describes how to implement selected object-oriented programming techniques in PowerBuilder.

Terminology review

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 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.

Implementing inheritance

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.

Implementing encapsulation

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.

Implementing polymorphism

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.

Other techniques

PowerBuilder allows you to implement a wide variety of object-oriented techniques. This section discusses selected techniques and relates them to PowerBuilder.

Using function overloading

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 versus static lookup

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.

Using delegation

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:

  1. Create a service object (u_sort_dw in this example).

  2. Create an instance variable (also called a reference variable) in the owner (a DataWindow control in this example):

    u_sort_dw iuo_sort
  3. Add code in the owner object to create the service object:

    iuo_sort = CREATE u_sort_dw
  4. 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
  5. 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.

  6. 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:

  1. Create the user object, defining instance variables only.

  2. Enable the user object's AutoInstantiate property by checking AutoInstantiate on the General property page.

  3. 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.

Subclassing DataStores

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.