This is primarily a technical paper, but some points may also interest non-technical managers.
This paper homes in on the principal criterion by which any software development methodology should be judged: programmer productivity. The business benefits of Hyper-Productivity are clear:
The bottom line is: more output, higher quality, lower cost.
If not gb_inline_object = TRUE Then this.enabled = TRUE Else this.enabled = FALSE End If
Several aspects of this code sample are unnecessarily complex. First, saying IF NOT ... THEN ... ELSE is always confusing. Its better to reverse the THEN and ELSE to produce simpler code that does the same thing, as follows:
If gb_inline_object = TRUE Then this.enabled = FALSE Else this.enabled = TRUE End If
This can be further simplified, since = TRUE is redundant:
If gb_inline_object Then this.enabled = FALSE Else this.enabled = TRUE End If
Further simplification is still possible, since the entire IF/ENDIF structure is unnecessary:
this.enabled = NOT gb_inline_object
Even this can be simplified, since this is the default object when an object property is referenced:
enabled = NOT gb_inline_object
The above suggests the enormous advantages that large-scale simplification can deliver in a software project:
Software is a complicated business. The evidence is all around us, since so many systems are buggy, if not outright failures. If the objective is to produce high-quality, correctly functioning software, then simplification, which eliminates layers of unnecessary complexity, is little more than common sense.
In physical systems, like parcel delivery or data transmission, redundancy is a good thing because one component of a system can take over when another fails. But software systems are in no way analogous. A piece of code will not break down, if it is correct. (PowerBuilder generally involves fairly conventional corporate software applications, so a real-time AI missile-guidance system, for example, is outside the scope of this paper.) Redundancy is a violation of simplicity: it makes systems larger, harder to understand and maintain, and more prone to bugs.
Some people argue that this.enabled or this.x are more intuitive than enabled or x. But this is only one of many defaults available to us in Powerscript. When we code embedded SQL, using SQLCA is the default; when we code dw.update(), we are implicitly specifying the default update() arguments (TRUE, TRUE).
In daily life, the advantages of defaults are obvious. When we dial a local number, we do not dial the area code. Yet some developers seem to want to dial the area code to remind themselves that theyre not going outside the area.
Specifying defaults is redundant by definition. Developers should exploit the simplification opportunities that the development tools defaults offer.
The following subsections list some of the ways to achieve clarity in PB.
Note that many developers (including me) consider it unnecessary to adhere to the standard for local variables.
customer_address = gnv_app.inv_string_service.of_replace( & model_dw_syntax, & ' name=' + as_object_name_array[1], & ' name=<name>')
A variable name like customer_address does nothing to enhance clarity. Cust_addr is perfectly clear, and if the context implies customer, then addr alone is sufficient.
Short names eliminate clutter, reduce the need to use line continuation (&), and make the logic (or lack of it) more apparent.
Unfortunately, some developers choose names without regard for the information that is readily available from context. The resulting names are unnecessarily complex. In the above example, an array variables name includes the word array even though variable references always make it clear when a variable is an array.
Words like object or businessobject, when included as part of a name, usually provide very little if any additional information.
Ninety nine percent of data models I have seen, in consulting to many organizations, have column names that indicate which table the column is in. Thus, an address column in a customer table will have a name like customer_address. Yet almost everyone realizes that this is completely redundant. When Broadway Ave. was named in New York City, it never occurred to anyone that it might be necessary to call it New York City_Broadway Ave. to distinguish it from Broadway Ave.s in other cities. Context eliminates ambiguity. Yet modern data modelers rarely apply the same principle.
In PB, a good rule to adopt in variable naming is to omit the suffix altogether in cases where the prefix alone naturally suggests some unique thing. For example:
Writing software involves a continuous series of decisions. Much of the time spent maintaining or updating software is spent searching, searching, for which decision was made by ones predecessor. Maintenance overhead is greatly reduced if the developers decisions are guided by consistent rules, instead of being made arbitrarily.
To achieve a uniform system of names, particularly on team efforts, it is often beneficial to set up a dictionary of abbreviations for reference in naming both database and PB objects (tables, columns, userobjects, variables, etc.).
Clarity demands that this kind of information be programmed in scripts, so it can organized and easily written, changed, and viewed.
Repetitive patterns are too often found in PB systems. For example, it is not unusual to see hundreds of destructor scripts doing the same thing:
If isvalid(x) then destroy x If isvalid(y) then destroy y If isvalid(z) then destroy z ... [Etc.]
The better alternative is to code an easy utility function, say of_destroy(), that can be invoked whenever needed:
of_destroy({x, y, z, ...})This is clearly more readable, more maintainable, less error-prone.
[Note: This particular function is no longer needed, thanks to the automatic garbage collection introduced in v. 6.]
EXAMPLE OF GENERALIZATION: One can build a kitchen sink with two spouts, one for hot water, one for cold. The result makes two temperatures available, hot and cold. The more general solution features a single spout, so the user can produce a flow of any temperature between hot and cold.
Iverson created what he called a tool for thought, a programming language designed to promote simplicity, brevity, and consistency. This actually made intrinsically difficult ideas easier to understand.
The manipulation of concepts is at the heart of the business of producing software. As noted above, there can be little question that the concepts are challenging, even in mundane corporate applications, considering that correct and satisfactory software is such a rarity.
Hyper-Productivity is all about bolstering native PowerBuilder with an array of well-designed, generic utilities. The advantages of a well-designed language, or notation, for improving our ability to manipulate difficult ideas has been understood since long before Iversons tool for thought. In 1911, the mathematician Alfred North Whitehead wrote:
By relieving the brain of all unnecessary work, a good notation sets it free to concentrate on more advanced problems, and in effect increases the mental power of the race. [...] By the aid of symbolism, we can make transitions in reasoning almost mechanically, by the eye, which otherwise would call into play the higher faculties of the brain. It is a profoundly erroneous truism [...] that we should cultivate the habit of thinking of what we are doing. The precise opposite is the case. Civilization advances by extending the number of important operations which we can perform without thinking about them. [An Introduction to Mathematics (New York and London, 1911), p. 59.]Similar insights were articulated centuries earlier, at a time when the general practice was to express mathematics in prose, without symbols:
[...] Which Treatise being not written in the usuall synthetical manner, nor with verbous expressions, but in the inventive way of Analitice, and with symboles or notes of things instead of words, seemed unto many very hard; though indeed it was but their owne diffidence, being scared by the newness of the delivery; and not any difficulty in the thing it selfe. For this specious and symbolicall manner, neither racketh the memory with multiplicity of words, not chargeth the phantasie with comparing and laying things together; but plainly presenteth to the eye the whole course and processe of every operation and argumentation. [William Oughtred, The Key of the Mathematicks (London, 1647), Preface.]The mathematician Howard Aiken once remarked: Dont worry about people stealing your ideas. If your ideas are any good, youll have to ram them down peoples throats.
This is illustrated by early opposition to the use of symbols, a way of working with ideas that is taken for granted today (at least in mathematics). One objection came from Thomas Hobbes (1588-1679), the famous English philosopher and political theorist:
Symbols, though they shorten the writing, yet they do not make the reader understand it sooner than if it were written in words. For the conception of the lines and figures [...] must proceed from words either spoken or thought upon. So that there is a double labour of the mind, one to reduce your symbols to words, which are also symbols, another to attend to the ideas which they signify. Besides, if you but consider how none of the ancients ever used any of them in their published demonstrations of geometry, nor in their books of arithmetic [...] you will not, I think, for the future be so much in love with them. [Quoted in A History of Mathematical Notations, by Florian Cajori (The Open Court Publishing Co., Chicago, 1928), pp. 426-431.]
IF() is an example of an omission. PB developers are familiar with its usefulness as a datawindow painter function. It is just as useful in PowerScript. Other languages provide it, but not PB.
OPENSHEETWITHPARM() is an example of inadequate function design. It is the unreliable method provided by PB for passing parameter information to a new window. OPENSHEETWITHPARM() uses the global MESSAGE variable, which may be overwritten by other processes (specifically, those resulting from the execution of the Constructor scripts of the new windows objects) before it is read by the new window. Moreover, OPENSHEETWITHPARM()s design makes parameter passing overly awkward.
User-defined functions for such tasks, having generic applicability, are referred to as utility functions. Since the utility functions we are adding to PB are needed pervasively in our code, we want to be able to regard them as part of the basic language facility. We want them to be as accessible as PBs own native functions. We do not want to suffer the awkwardness of writing something like:
is_msg = gnv_app.inv_beef.of_if( & ib_success, 'It worked.', 'It failed!')
... whenever we need to invoke them. In the Hyper-Productive framework, we can invoke utilities as easily as:
is_msg = g.if1( & ib_success, 'It worked.', 'It failed!')
Note the convenience. We purchase this convenience at the expense of specific, limited, and well-defined deviations from the PB naming standard:
Our names for these base objects would be the standard prefix without the suffix (e.g. u_dw for the base datawindow object) -- except that this is how PFCs base objects are named, and we want to be able to use these objects in projects that have previously chosen to use PFC. Therefore we append a 1 suffix, so the name for our base dw object is u_dw1.
In these objects, we have two categories of object functions:
Our system is to omit the of_ prefix and add a 1 suffix in naming functions in the first category -- thus, setrow1(); and to comply with the standard in naming functions in the second category -- thus, of_fixstatus().
The global variable <g> is declared as:
n_cst_global g
The app open event does:
g = create n_cst_global
In order for the utilities in u_dw1 to be available in a specific datawindow, it must be inherited (directly or indirectly) from u_dw1. Similarly for the other base objects in UTILS.PBL.
To apply Hyper-Productivity, the developer must appreciate:
The art of utility design often involves an incremental process of re-design and modification. To achieve great utilities, the developer must not be content with adequacy. He/she should be prepared to fix things that aint broke. That is the path to Hyper-Productivity.
This papers illustration of Hyper-Productivity therefore does not dwell on UTILS.PBL or the implementation details of utility functions. What we present here is a single, specific illustration of the methodology in action.
The Certificate Data tabpage (shown in the figures) groups the certificates, showing, from left to right, the CS certs alphabetically, then the non-CS certs alphabetically. Each CS cert has an Amount entry column and two computed fields; each non-CS cert has only the Amount entry column. In addition, there is the date column at the extreme left, as well as the aggregation columns for CS and non-CS certs.
Note the sheet title, indicating that the data pertains to a specific policy (selected by the user elsewhere in the system). Our database accesses need to refer to the appropriate policy.
We begin with script ue_setupds of u_dwpolpc, whose purpose is to set up datastores. It begins by defining ids_certs, a datastore with columns CERT_NAME and CREDIT_SUPPT_FLAG (a CHAR(1) column with value Y or N). These columns tell us the names of the certificates we have to display in the datawindow, and whether each is type CS or non-CS. We define ids_certs as follows:
/* sqlcerts select CERT_NAME, CREDIT_SUPPT_FLAG from POLICY_CERT where POLICY_ID = <pid> */ ids_certs = g.createds(g.rplc(& g.getstring(this, 'ue_setupds', 'sqlcerts', true),& '<pid>', & string(il_pid)))
What the above is doing is, first, generating the SQL Select string which is used to dynamically create the datawindow object for the datastore. This is done by getting the model Select statement shown in the comment, then modifying it by replacing <pid> (policy ID) by the actual policy number.
g.getstring() is the utility used to get the model Select statement stored in the comment. g.getstring() returns a single, arbitrary-length string stored between comment delimiters in an event or function script. Its arguments are: an object handle; the name of the event containing the desired string; the string identifier (in this case, sqlcerts); and a boolean indicating whether special characters (carriage returns, linefeeds, tabs) are to be removed. (getstring()s implementation uses the Classdefinition property.)
Once we have the form of the Select statement, we replace <pid> with the actual policy number. We now have the actual Select statement to be used in creating the datastore, and we can invoke g.createds(), a utility whose return value is a datastore of type n_ds1 (our base datastore object).
Our datastore is done, except for a couple of details. We do not want to assume that the database column will necessarily contain a Y or N. So we create a computed field which is Y if the flag column is Y or y, and N if it is anything else (including NULL):
ids_certs.of_createcf(&
'csflag', &
"if(isnull(CREDIT_SUPPT_FLAG), 'N', " + &
"if(upper(CREDIT_SUPPT_FLAG) = 'Y', 'Y', 'N'))")
Finally, we set the datastores sort order, in accordance with the required left-to-right cert sequence:
g.syserr(ids_certs.setsort('csflag D, cert_name A'))We take a diversion here to discuss the very useful g.syserr(). PB is full of built-in functions which can fail, and which indicate failure only via a return value. Error results are generally one of: a negative number (usually -1), the boolean FALSE, or a non-empty string.
Few PB developers check all return values for errors. The conventional IF / ENDIF code pattern for testing these values clutters code up considerably, harming clarity, and many tests are considered unnecessary. Still, an immediate warning of an unexpected error can save a lot of debugging time during development. Hence the usefulness of g.syserr().
g.syserr() does nothing if its argument (numeric, boolean or string) indicates no error. But if an error is indicated, it deliberately produces a PB runtime error condition by dividing a number by 0. When running an app in PB 6.5 under NT 4 via running man, this opens up the debugger and shows the line in syserr() where the zero-division is coded. Thanks to 6.5s enhanced debugger, the developer can then view the calling stack and see exactly where the unexpected error has occurred. In the case above, he/she will see the line containing the call to SETSORT(). Via the debuggers Set Context, one can at that point do things like examine the values of local variables in the script containing SETSORT().
Variations on this technique were also available in PB 5. There one could code:
g.l = g.syserr(ids_certs.setsort('csflag D, cert_name A'))g.l was a public instance var in n_cst_global of type long. g.syserr() returned a long value if no error, or a string if its argument indicated error. Thus PB would crash on the line containing the problem (i.e., the line containing setsort()).
g.syserr() can be used to confirm any condition, not just those involving return codes.
Of course, we want smoother handling of unexpected conditions in distributed executables. But we dont want to have to change our scripts. Therefore g.syserr() tests the condition,
0 = handle(getapplication())
and alters its deliberate crash behavior if the result is FALSE.
Continuing with our u_polpc tabpage, we now need to define our next datastore:
/* sqlppc
select
POLICY_ID, DISTRIB_DATE, CERT_NAME, BAL_AMT
from POLICY_POOLDISTRIB_CERT
where POLICY_ID = <pid>
order by distrib_date
*/
ids_ppc = g.createds(g.rplc(&
g.getstring(this, 'ue_setupds', 'sqlppc', true), &
'<pid>', &
string(il_pid)), &
{'POLICY_ID', 'DISTRIB_DATE', 'CERT_NAME'})Since we will be calling ids_ppc.update(), we use the polymorphic variant of g.createds() which permits us (via its second argument) to specify the datastores unique key columns (i.e., the tables Primary Key, which in this case is composite).
BAL_AMT (balance amount) is user-editable. We will need to rearrange this data to map it to the Runtime Datawindow, which has the certs arranged horizontally.
The system now proceeds to the ue_setupdw script, in which the Runtime Datawindow displayed in the figures is produced.
Our dw gets the look of perfection by having its objects positioned with mathematical precision. Consider the dws groups and items.
There are multiple groups of items: the date items (text and column object) are one group; Amsterdam is another group, consisting of four text objects, a column, and two computed fields; and so on. We are going to make the horizontal gap between each pair of consecutive groups identical. We call this gap il_gap.
The CS cert groups contain multiple horizontally spaced items: Amount, % Curr, % Begin. We are going to make the horizontal gap between each pair of contiguous items identical, within each CS cert group. We call this gap il_itemgap.
The static version of our tabpages datawindow object (see figures) has everything that the Runtime Datawindow shown in the figures has, except that it has exactly one CS cert and one non-CS cert.
We set il_gap as the distance between the date text object and the CS cert group header text object in our Static Datawindow. Similarly, we set il_itemgap as the distance between the Amount and % Curr text objects in the CS cert group of the Static Datawindow:
// gap between groups of items:
il_gap = of_hgap('distrib_date_t', 'cshd_t1')
// gap between items within group:
il_itemgap = of_hgap('cs_t1', 'cs_curpct_t1')Here we are using of_hgap(), a utility in our base dw which yields the horizontal gap between two dw objects.
With this technique, adjusting the identical horizontal gap between all groups in our Runtime Datawindow, or between items in all groups, is a simple matter of opening the dw painter and repositioning the objects being used as models.
We will need to set the X and TABSEQUENCE properties of dw objects. For this purpose we initialize il_cumpos and ii_cumtabseq:
il_cumpos = descl('cshd_t1.x')
ii_cumtabseq = 1u_dw1.descl() is like dw.describe() except it returns its result as type LONG.
The vars above are cumulative in the sense that they keep track of increasing values as we go left to right, setting dynamic datawindow object properties.
Now we are ready to call of_setupgrps(), a u_dwpolpc object function that is called once to set up all the CS groups, and a second time to set up all the non-CS groups.
It is time for another diversion, since one of of_setupgrps()s arguments is a structure type.
We rarely enter the structure painter, because UTILS.PBL already contains a structure, called s_generic, which fits just about every need:
global type s_generic from structure integer i[] long l[] string s[] date d[] any a[] any a_sheetkey powerobject po[]
Occasionally a need arises to add something that is missing in the above, e.g. datetime dtm[]. Such additions to s_generic have no effect on existing code.
In this case, the only structure property of interest is the string array, s[]. The others are unused. We are using s_generic only in order to get an array of arrays.
of_setupgrps()s first job is to set up groups of CS certs and define istr_cs[], an instance var in u_dwpolpc of type s_generic representing CS-type certs. of_setupgrps() defines istr_cs[j].s[i] as the name of the jth dw object in the ith group of CS certs.
Similarly, when it is called a second time, of_setupgrps() sets up groups of non-CS certs and defines istr_ncs[].
The items for CS group 1 and non-CS group 1 all exist already in our Static Datawindow (see figures). of_setupgrps() dynamically creates the objects for any other cert groups, depending on the dynamic cert data in ids_certs. Also, in case the data indicates 0 CS groups, of_setupgrps() makes the items in CS group 1 (which already exist) invisible; similarly if there are 0 non-CS groups.
of_setupgrps() takes the following arguments:
ref s_generic astr_grp[], // reference argument readonly string as_grpitems[], // the names of the items in the group, // minus the suffix indicating group # readonly string as_objtype, // the i'th character in this string is a code indicating // the type of as_grpitems[i]: Date, Amount, Count, or Percent; // or X if the format property does not need to be set. readonly integer ai_widthidx, // index # of item which defines the group's width; // - as_grpitems[ai_widthidx] spans the whole group width-wise readonly integer ai_textidx, // identifies the item (as_grpitems[ai_textidx] whose TEXT // property needs to be set dynamically according to the cert name. readonly boolean ab_cs // Indicates whether certs to be set up are CS-type.
The two calls to of_setupgrps() are as follows:
of_setupgrps ( &
istr_cs, &
{'cshd_t', 'cs_t', 'cs_curpct_t', 'cs_beginpct_t', 'cs', &
'cs_curpct', 'cs_beginpct'}, &
'XXXXAPP', &
1, &
1, &
true)
of_setupgrps ( &
istr_ncs, &
{'ncshd_t', 'ncs_t', 'ncs'}, &
'XXA', &
1, &
1, &
false)Here is how of_setupgrps() does the job:
First, it defines <cnt>, the number of cert groups to be created. This depends on the argument ab_cs, which tells of_setupgrps() whether it is being called to set up CS or non-CS groups of certs:
// # of groups of certs of this type:
flagexpr = 'csflag="' + g.if1(ab_cs, 'Y', 'N') + '"'
// e.g.: csflag = "Y"
cnt = ids_certs.descl(&
"Evaluate('Sum(if(" + flagexpr + ", 1, 0))', 0)")
Next, the main job is carried out:
// create groups of items (or make grp 1 invisible if cnt=0):
for j = 1 to upperbound(as_grpitems)
// before duplicating it, set format of object j:
fmt = g.case1(&
mid(as_objtype, j, 1), &
'DACP', &
{iw.is_fmtdate, iw.is_fmtamt, iw.is_fmtcount, &
iw.is_fmtpct, 'X'})
if fmt <> 'X' then &
modify1(as_grpitems[j] + '1', &
{'format = "' + fmt + '"'})
of_dupobj(as_grpitems[j], cnt, astr_grp[j].s)
nextThis involves a couple of utilities we have not yet seen. g.case1() is straightforward. By way of illustration,
case1('d', 'abcde', {1, 2, 3, 4, 5, 6}) returns 4; and
case1('f', 'abcde', {1, 2, 3, 4, 5, 6}) returns 6.
u_dw1.of_dupobj() is more involved. As an example, of_dupobj("woody", 10, arr) would expect an object woody1 (of any type -- column, text object, computed field, etc.) to exist in the dw. It would make 9 copies of it, so there would be 10 in all; name the new ones woody2, woody3, ..., woody10; and, for the the callers convenience, fill reference string parameter arr[1 to 10] with the values, woody1 to woody10.
If 0 were passed to of_dupobj() instead of 10, all it would do is make woody1 invisible, then exit.
Much of of_setupgrps()s job is done on completion of the loop above. If cnt = 0 there is nothing left to do, and it returns. Otherwise, it has to set the x, text, and tabsequence properties for various objects:
grpsize = descl(as_grpitems[ai_widthidx] + '1.width') + il_gap
// define item offsets within groups:
for j = 1 to upperbound(astr_grp)
offset[j] = descl(astr_grp[j].s[1] + '.x') - &
descl(astr_grp[1].s[1] + '.x')
next
// set x, tabsequence, text:
// txt1 is text of item ai_textidx up to and incl CRNL:
txt1 = describe(astr_grp[ai_textidx].s[1] + '.text')
// now cleanup, because describe() returns it with
// surrounding quotation marks!
txt1 = trim(txt1)
txt1 = g.midse(txt1, 2, len(txt1) - 1)
txt1 = left(txt1, pos(txt1, g.crnl(1)) + 1)
for i = 1 to cnt
lrow = ids_certs.find1(flagexpr, lrow + 1)
g.syserr(lrow > 0)
txt = txt1 + ids_certs.object.cert_name[lrow]
modify1a(astr_grp[ai_textidx].s[i], &
'text = "' + txt + '"')
for j = 1 to upperbound(astr_grp)
modify1a(astr_grp[j].s[i], &
'x=' + string(il_cumpos + offset[j]))
if upper(describe(astr_grp[j].s[1] + '.type')) = &
'COLUMN' then
if descl(astr_grp[j].s[1] + '.tabsequence') > 0 then
ii_cumtabseq += 1
modify1a(astr_grp[j].s[i], &
'tabsequence=' + string(ii_cumtabseq))
end if
end if
next
il_cumpos += grpsize
nextThe above involves a few simple utilities not yet seen:
g.crnl(ai_count):
returns fill('~r~n', 2 * ai_count)
g.midse(as, al_start, al_end):
like mid(), but args specify start and end
u_dw1.find1(as_expr):
returns find(as_expr, 1, rowcount())
u_dw1.modify1a():
similar to modify()The job is not quite complete. Some additional work remains, for example, positioning the aggregate CS and aggregate non-CS columns. But the above (which runs successfully in PB 6.5) should go some way towards describing the flavor of Hyper-Productivity and showing its potential.
Policies that make no sense either for Sybase or its customers cannot be meaningfully discussed, much less changed. PowerBuilders innumerable quality problems have always taken a heavy toll on productivity, and the situation persists in version 6. Awkward bug-reporting procedures convey more convincingly than words that Sybase is not keen to receive bug reports. Known bugs appear to be a corporate secret. Technical support is a disaster.
The people most intimately familiar with PBs quality problems are the PB software developers of the world. Although one would expect software professionals to want to devote their energies to true software challenges, rather than spending time rebooting computers and hunting for bugs that dont actually exist, developers have for the most part been uncritical. Some are vociferous in defending PBs failings.
PB developers are like nurses in a mismanaged hospital, routinely observing hair-raising accidents: fingers lopped off by a slipped scalpel, heavy equipment dropped by a surgeon onto an exposed heart, cleaning solution pumped into patients on dialysis machines. The nurses are resigned to the institutions shortcomings and can even joke about them, but outsiders would be horrified by what goes on.
These are some of the fundamental quality issues that have persisted in successive versions of PB over the years:
This extremely burdensome restriction is a clear violation of the well-understood benefits of the MDI interface.
Essentially, the current dialogue has built-in sort keys that are in backwards order. The major key should be object name, the minor key PBL name. Thus, for example, when pressing Shift-F2 to open a window, the developer should be presented with a 2-column list showing the apps window names in column 1 and the corresponding PBL names in column 2, with major/minor sort keys being columns 1 and 2 respectively.
Aside from the fundamental quality issues, the sheer sloppiness indicated by many minor issues suggests a disdain for quality. The following are examples in v. 6.5:
While it is regrettable that quality issues have not aroused more protest from PB developers and within Sybase itself, the scant attention paid to quality by corporate clients is the real puzzle. They are after all the ones for whom the defects translate directly into concrete monetary losses. Yet one does not get a sense that clients have been clamoring for Sybase to address the product deficiencies and poor tech support that have been an integral part of the PB experience for years. While these problems have persisted, Sybase has expended apparently substantial efforts on the addition of language features that offer little real benefit to the majority of clients. One example is the illusory distributed processing features of version 5, which in the typical case probably yielded only negative benefits by luring clients into expensive and fruitless technology experiments.
It seems improbable that Sybase has been defying demands and pressure from a large segment of the PB client base. More likely, decision-makers in client companies are unaware of the extent of quality problems, or have failed to properly assess their cost.
Managers and developers concerned about productivity should be alert to featuritis, the tendency to evaluate software products by adding up the number of check marks in a feature list. While such a superficial evaluation procedure seems laughable, it may be more prevalent than one would suppose. (It is sometimes abetted by developers themselves, more eager to gain experience with cutting-edge technology features than to develop systems that work.)
Beyond avoiding featuritis, managers should appreciate that productivity -- hence their bottom line -- is powerfully impacted by the quality of the software tools in use by developers. It would therefore make sense for corporations with significant software development expenses to pay serious attention to quality problems.
The development of specifications is often disparaged these days as the waterfall approach. As if there were something unclean about a waterfall. Iterative development is much preferred. But natural law cannot be altered: one reaches ones destination sooner when one sets off in the right direction.
Many clients show impressive courage in awarding contracts worth millions to consultants based on PowerPoint presentations and sales talk. Losses and write-offs would be much reduced if clients demanded more sensible contracts.
It should not be very hard to understand that such policies should exclude software developers, who after all are expected to have some technical knowledge. Developers should have complete control over their own workstations, and should have all software installed on local drives for speed of access.
Developers should have unhindered access to PB resources on the Web. Of particular importance are Sybase technical documents, and newsgroups.
Related: Reflections on Corporate America