Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

JVM Languages

Object-Oriented Design in Procedural Environments


Jun00: Object-Oriented Design in Procedural Environments

Thomas is a senior software architect and Sun Certified Java Developer. He can be contacted at [email protected].


It's no secret that object-oriented design promotes clean encapsulation of form and function. By packaging data along with the functionality related to manipulating that data, you achieve a simple modularity that promotes clarity in the big picture of a design, along with probable code reuse in future projects. Of course, these traits are also achievable with procedural languages, but they can be much less intuitive to implement. In this article, I'll illustrate how you can apply some of the inherent cleanliness of object-oriented design into your procedural language projects.

The PageWriter Project

I recently was involved in a project developing applications for Motorola's PageWriter two-way pager. As Figure 1 shows, the device is a little larger than an average pager because it folds open to expose a gray-scale screen and QWERTY keyboard.

PageWriter comes with a set of basic applications (such as e-mail and an address book) already installed. However, you can also write third-party applications for PageWriter. To enable this, Motorola provides the PageWriter SDK, a toolkit that's freely available at http://www.mot .com/MIMS/MSPG/spin/downloads.html (or on CD-ROM for a small fee).

PageWriter applications are written in a proprietary language called "FlexScript," which looks like a hybrid of C and Visual Basic. FlexScript is a procedural language with a primitive set of library routines. It passes arguments by value unless otherwise specified in the function declaration (not the function call) via the ref keyword. For some reason, you can't pass arrays into a function. There are a few other quirks you need to be aware of, so read the help files carefully if you brave the SDK.

Object-Oriented Design

Although I was raised on procedural languages such as C, Pascal, and Fortran, I've spent the last three years focusing on Java, a pseudopure object-oriented language. I say "pseudopure" because Java supports a few primitive data types while (ironically) offering synonymous object versions.

Working with Java has been a real eye opener. Because of it, I have embraced object-oriented design, reading books such as the classic Design Patterns: Elements of Reusable Object-Oriented Software, by Erich Gamma et. al (Addison-Wesley, 1995), and Martin Fowler's Refactoring: Improving the Design of Existing Code (Addison-Wesley, 1999). With this in mind, you can understand my disappointment in the procedural-language constraints levied upon me by the PageWriter SDK.

When assigned the PageWriter application project, I first turned to Rational Rose, my favorite Universal Modeling Language (UML) tool, and started designing solutions for the problem at hand. Once I had come to grips with the project requirements and how I wanted to assemble the application, I wondered how I was going to translate my object-oriented solution into a procedural language environment. I had to find ways to emulate encapsulation, modularity, inheritance, aggregation, exception propagation, and error handling.

In a procedural language, data and logic are separated. Data are usually defined as primitives such as integers, Booleans, and arrays of characters. You can often package together a small collection of diverse data types using a structure. Fortunately, FlexScript does support structures. I decided to use structures as the foundation of my "pseudoobjects" (the term I will use here to represent a procedural translation of an object-oriented object).

With an object-oriented language, functionality is inherently tied to the data. You can't invoke a method (synonymous with function for the purposes of this article) without having an instance of the object (basically its data) in memory. This is an oversimplification and there are, of course, special cases where this rule can be bent (as with static code, for instance). Within a procedural language, however, you can't directly tie functionality to data. In my case, I had to find a way -- indirect or not. I achieved the marriage of functionality and data by combining three simple techniques.

  • Employing a naming convention. Every function name started with the name of the structure that it modified. For example, I had a structure named PersonStruct; see Listing One. This structure contained an integer data member labeled age. Thus, the function for setting the age value was named personSetAge. This naming convention helped to clarify the logic of a function, a crude level of self-documenting code. It also prevented namespace collisions that might have resulted from linking two different libraries that both included a function called setName.
  • Making the first argument of every related function a reference to an instance of the structure that it modified. Expanding on the same example, the function personSetAge would take a pointer to an instance of PersonStruct as the first argument, and logically an integer representing the new age value as the second argument.

  • Placing the structure and all of its relative functions into their own separate file, aptly named Person.inc; see Listing Two. I repeated this for each pseudo-object. This made it simple to see which objects were used in each part of the application because the list of includes at the top of each source file told the story.

Although this approach is clean and simple, it isn't bulletproof. Another programmer reusing this library could modify a structure's internal data by referencing it directly. For example, if another programmer had an instance of PersonStruct called ps, he could change the age value using the syntax ps.age=26 instead of calling the function personSetAge(ps,26). This example seems harmless, and in some cases would be considered an optimization, but what if I was enforcing some constraints on the range of the age value from within the personSetAge function? The other programmer would be circumventing the constraint logic and possibly violating the integrity of the data. Since I was the only coder on this project, such abuse was not an issue. For group projects, however, the structures, functions, and overall library schema would have to be well documented to prevent such oversights. Periodic and frequent code reviews are always a good way to catch mistakes like this.

Most object-oriented languages support an error-handling technique known as "exception propagation." When a method encounters a problem that it cannot resolve, the method creates an exception and throws it up to the calling process. The caller, in turn, may handle the exception or propagate it up the chain. In procedural languages, however, error messages are typically handled via the return value of a function. A function would return an integer value of zero to signify success or nonzero (usually -1) to signify an error. This was the approach I chose to implement.

A safe range for a person's age is between 1 and 100. If a value less than 1 (including negative values) or greater than 100 was passed into the personSetAge function, the value would be discarded and -1 would be returned. This technique, however, also has some faults. A programmer calling the function does not have to check the return value. The programmer can completely ignore the error and continue on as if nothing ever happened. If I was using exception propagation in an object-oriented language the calling program wouldn't even compile unless it handled or propagated the error. Once again, solid documentation and frequent, thorough code reviews are the best alternative for enforcement.

Next came the tricky stuff. I had a few objects in my design that inherited from (some say extended) other objects. Translating this object-oriented concept into the procedural arena turned out to be a little easier than anticipated. I essentially used nesting on the part of the data and delegation on the part of the functionality.

To illustrate my solution for the inheritance dilemma, I introduce a new pseudoobject called UserAccountStruct. This new account object extends the previously defined person object, thus inheriting all of the person's data and functionality. As mentioned, I actually encapsulated the data that, in theory, was inherited. The new account structure simply contains a person structure. In other words, UserAccountStruct has a data member of type PersonStruct, which I declared as person. Thus, if I wanted to reference the age value of a instance of UserAccountStruct called account, the syntax would be account.person.age.

Recall that each function in the person library required an instance of a PersonStruct as the first argument. These functions could easily be reused with the account pseudoobject by passing in account.person, a reference to the nested PersonStruct instance, as the first argument; for example, personSetAge(account.person,26). However, this could get extremely complex to another programmer as the inheritance tree grew. The other programmer should only have to know about the account pseudoobject and its immediate functions while not having to worry about all of the nested data structures and their inherited functionality.

To preserve the clarity of the design and reduce the confusion of programmers utilizing pseudoobjects, I redeclared all of the functions that I wanted to inherit. The new account functions, following the predefined conventions, each started with userAccount and required a reference to UserAccountStruct as the first argument. The logic of these functions simply delegated the work to their predecessors. For example, the function called userAccountSetName, taking an instance of UserAccountStruct labeled account and an integer labeled age as arguments, did nothing more than call personSetAge(account.person,age). This solution might seem daunting for pseudoobjects with scores of functions because you'll have a lot of typing to do. However, any decent text editor with copy-and-paste and search-and-replace features can make short work of the chore. The time invested is well worth it in the end because the code is much more readable.

The file containing the source for account (UserAccount.inc) begins with an include statement for its parent Person.inc. Any application modules that utilize the account pseudoobject need only to include UserAccount.inc, thus cleanly hiding the gory details of the home-brew pseudoobject inheritance.

Conclusion

The next time you take a wrong turn and find yourself in the dark alley of procedural paradigms, remember these techniques and keep the faith of object-oriented design. I have no ill feelings toward procedural languages, so please put down the pen and save me the flames. I simply offer a basic survival kit for the proponents of one realm to live peacefully in the other.

DDJ

Listing One

structure PersonStruct
    integer age
endstruct

function personSetAge( PersonStruct person, integer age ) returns integer
    if( age > 0 )
    begin
        person.age = age
        return 0
    end
    else
    begin
        return -1
    end
end function


Back to Article

Listing Two

$$INCLUDE "Person.inc"

structure UserAccountStruct
    PersonStruct person
    string password
endstruct

function userAccountSetAge( UserAccountStruct account, integer age )
    integer status
    status = personSetAge( account.person, age )
    return status
end function

function userAccountCheckPassword(UserAccountStruct account,string password)
    if( account.password = password )
    begin
        return 0
    end
    else
    begin
        return -1
    end
end function




Back to Article


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.