alextrs / oestandards

Style guides for OpenEdge projects

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

OpenEdge Coding Standards

Table of Contents

  1. Objects
  2. Error Handling
  3. Data Access
  4. Comments
  5. Performance
  6. Variables
  7. Naming Conventions
  8. Dynamic Objects
  9. Code Styling
  10. Other

Objects

Error Handling

  • 2.1 NO-ERROR: Use NO-ERROR only when you expect an error to occur, and if you use it, handle error appropriately

    Why? NO-ERROR suppresses errors, which can cause database inconsistency issues, memory leaks, infinite loops and more...

    /* bad (error is suppressed, cMemberName is never assigned */
    ASSIGN iMemberNumber = INTEGER("ABC")
           cMemberName   = 'ABC' NO-ERROR.
        
    /* good (ver 1) - using structured error handling */
    ASSIGN iMemberNumber = INTEGER("ABC")
           cMemberName   = 'ABC'.
    /* ... some code... */
    CATCH eExc AS Progress.Lang.ProError:
      MESSAGE "Error:" + eExc:GetMessage(1).
    END.
        
    /* good (ver 2) - classic error handling (split safe assignment from unsafe) */
    ASSIGN cMemberName   = 'ABC'.
    ASSIGN iMemberNumber = INTEGER("ABC") NO-ERROR.
    IF ERROR-STATUS:ERROR THEN DO:
        /* handle error here */
    END.

  • 2.2 BLOCK-LEVEL THROW Always use BLOCK-LEVEL ON ERROR UNDO, THROW statement

    Why? It changes the default ON ERROR directive to UNDO, THROW for all blocks (from default UNDO, LEAVE or UNDO, RETRY)

    Note: Use this parameter in legacy systems only. For new development use -undothrow 2 to set BLOCK-LEVEL ON ERROR UNDO, THROW everywhere

    Note: Use in new modules or when you're confident that the change in existing code is not going to break error handling

    /* bad (default ON ERROR directive used) */
    RUN internalProcedure.
    
    CATCH eExc AS Progress.Lang.AppError:
        /* this will never be executed */
    END.
        
    PROCEDURE internalProcedure:
        UNDO, THROW NEW Progress.Lang.AppError('Error String', 1000).
    END.
    
    /* good */
    BLOCK-LEVEL ON ERROR UNDO, THROW.
    
    RUN internalProcedure.
    
    CATCH eExc AS Progress.Lang.AppError:
        /* error will be caught here */
    END.
    
    PROCEDURE internalProcedure:
        UNDO, THROW NEW Progress.Lang.AppError('Error String', 1000).
    END.
    
    /* bad (routine-level doesn't cover FOR loops) */
    ROUTINE-LEVEL ON ERROR UNDO, THROW.
    RUN internalProcedure.
        
    CATCH eExc AS Progress.Lang.AppError:
        /* this will never be executed */
    END.
        
    PROCEDURE internalProcedure:
        FOR EACH bMemberRecord NO-LOCK:
            IF bMemberRecord.memberDOB < 01/01/1910 THEN
                UNDO, THROW NEW Progress.Lang.AppError('Found member with invalid DOB', 1000).
        END.
    END.
    
    /* good */
    BLOCK-LEVEL ON ERROR UNDO, THROW.
    
    RUN internalProcedure.
    
    CATCH eExc AS Progress.Lang.AppError:
        /* error will be caught here */
    END.
    
    PROCEDURE internalProcedure:
        FOR EACH bMemberRecord NO-LOCK:
            IF bMemberRecord.memberDOB < 01/01/1910 THEN
                UNDO, THROW NEW Progress.Lang.AppError('Found member with invalid DOB', 1000).
        END.
    END.

  • 2.3 CATCH/THROW Use CATCH/THROW instead of classic error handling (NO-ERROR / ERROR-STATUS).

    Why? One place to catch all errors.

    Why? No need to handle errors every time they occur. No need to return error as output parameter and then handle them on every call.

    Why not? When working with FIND use NO-ERROR and check buffer availability

    /* bad */
    RUN myCheck (OUTPUT cErrorMessage) NO-ERROR.
    IF ERROR-STATUS:ERROR THEN
        MESSAGE "Error: " + ERROR-STATUS:GET-MESSAGE(1).
    ELSE IF cErrorMessage > '' THEN
        MESSAGE "Error: " + cErrorMessage.
        
    PROCEDURE myCheck:
        DEFINE OUTPUT PARAMETER opcStatusMessage AS CHARACTER NO-UNDO.
        
        IF NOT CAN-FIND (FIRST bMember) THEN DO:
            ASSIGN opoStatusMessage = 'Can not find member, try again'.
            RETURN.
        END.
    END.
        
    /* good (any application or system error will be caught by CATCH block) */
    RUN myCheck.
        
    CATCH eExc AS Progress.Lang.ProError:
        MESSAGE "Error: " + eExc:GetMessage(1).
    END.
        
    PROCEDURE myCheck:
        IF NOT CAN-FIND (FIRST bMember) THEN
            UNDO, THROW NEW Progress.Lang.AppError('Can not find member, try again', 1000).
    END.
    

  • 2.4 CATCH MANY Use multiple catch blocks if you need to handle errors differently based on error type (only if you need to handle errors differently)

    Note: If you have multiple catch blocks put most specific first and then the most generic. If the first catch matches the exception, it executes, if it doesn't, the next one is tried

    /* bad (one catch - multiple error types) */
    ASSIGN iMemberId = INTEGER(ipcParsedMemberId).
    FIND FIRST bMember NO-LOCK
         WHERE bMember.memberId = iMemberId NO-ERROR.
    IF NOT AVAILABLE bMember THEN
        UNDO, THROW NEW Progress.Lang.AppError('Invalid Member Id', 1000).
        
    CATCH eExc AS Progress.Lang.Error:
        IF TYPE-OF(eExc, Progress.Lang.AppError) THEN
            RETURN 'Invalid Application Error: ' + eExc:GetMessage(1).
        ELSE
            RETURN 'Invalid System Error: ' + eExc:GetMessage(1).
    END.
        
    /* good */
    ASSIGN iMemberId = INTEGER(ipcParsedMemberId).
    FIND FIRST bMember NO-LOCK
         WHERE bMember.memberId = iMemberId NO-ERROR.
    IF NOT AVAILABLE bMember THEN
        UNDO, THROW NEW Progress.Lang.AppError('Invalid Member Id', 1000).
        
    CATCH eExcApp AS Progress.Lang.AppError:
        RETURN 'Invalid Application Error: ' + eExcApp:GetMessage(1).
    END.
        
    CATCH eExcSys AS Progress.Lang.Error:
        RETURN 'Invalid System Error: ' + eExcSys:GetMessage(1).
    END.
    /* bad (multiple catch blocks - the same error handling) */
    FIND FIRST bMember NO-LOCK
         WHERE bMember.memberId = 123 NO-ERROR.
    IF NOT AVAILABLE bMember THEN
        UNDO, THROW NEW Progress.Lang.AppError('Invalid Member Id', 1000).
        
    CATCH eExcApp AS Progress.Lang.AppError:
        RETURN 'Error: ' + eExcApp:GetMessage(1).
    END CATCH.
        
    CATCH eExcSys AS Progress.Lang.Error:
        RETURN 'Error: ' + eExcSys:GetMessage(1).
    END CATCH.
    
    /* good */
    FIND FIRST bMember NO-LOCK
         WHERE bMember.memberId = 123 NO-ERROR.
    IF NOT AVAILABLE bMember THEN
        UNDO, THROW NEW Progress.Lang.AppError('Invalid Member Id', 1000).
    
    CATCH eExc AS Progress.Lang.Error:
        RETURN 'Error: ' + eExc:GetMessage(1).
    END CATCH.

  • 2.5 RE-THROW Re-throw errors manually only if you need to do extra processing (like logging, or converting general error type to more specific) before error is thrown to upper level

    Why? Every procedure/class is supposed to change the default ON ERROR directive, forcing AVM to throw errors to upper level automatically

    /* bad */
    ASSIGN iMemberId = INTEGER('ABC_123').
    
    CATCH eExc AS Progress.Lang.ProError:
        UNDO, THROW eExc.
    END CATCH.
        
    /* good (convert General error into ParseError) */
    ASSIGN iMemberId = INTEGER('ABC_123').
    
    CATCH eExc AS Progress.Lang.ProError:
        UNDO, THROW NEW Mhp.Errors.ParseError(eExc:GetMessage(1), eExc:GetNumber(1)).
    END CATCH.
        
    /* good (write log message and throw error - use only at top most level) */
    ASSIGN iMemberId = INTEGER('ABC_123').
    
    CATCH eExc AS Progress.Lang.ProError:
        logger:error(eExc).
        UNDO, THROW NEW Mhp.Errors.ParseError(eExc:GetMessage(1), eExc:GetNumber(1)).
    END CATCH.

Data Access

  • 3.1 Never use SHARE-LOCK: Always use either NO-LOCK or EXCLUSIVE-LOCK

    Why? If you don't specify locking mechanism, SHARE-LOCK is used. NO-LOCK has better performance over SHARE-LOCK. Other users can't obtain EXCLUSIVE-LOCK on record that is SHARE locked

    /* bad */
    FIND FIRST bMember
         WHERE bMember.id EQ 0.346544767...
    FOR EACH bMember:...
    CAN-FIND (FIRST bMember WHERE bMember.id EQ 0.346544767)...
    
    /* good */
    FIND FIRST bMember NO-LOCK
         WHERE bMember.id EQ 0.346544767...
    FOR EACH bMember NO-LOCK:...
    CAN-FIND (FIRST bMember NO-LOCK
              WHERE bMember.id EQ 0.346544767)...

  • 3.2 Transaction Scope: Always explicitly define the transaction scope and strong scope applicable buffer

    /* bad */
    FIND FIRST bProvider EXCLUSIVE-LOCK NO-ERROR.
    IF AVAILABLE bProvider THEN
        ASSIGN bProvider.name = 'New Provider':U.
    /* ... some code... */
    FOR EACH bMember EXCLUSIVE-LOCK:
        ASSIGN bMember.memberName = 'New member name':U.
    END.
    
    /* good (provider should be updated separately from members) */
    DO FOR bProvider TRANSACTION:
        FIND FIRST bProvider EXCLUSIVE-LOCK
             WHERE bProvider.id = 0.657532547 NO-ERROR.
        IF AVAILABLE bProvider THEN
            ASSIGN bProvider.name = 'New Provider':U.
    END.
    /* ... some code... */
    FOR EACH bMember NO-LOCK
       WHERE bMember.category = 0.17567323 TRANSACTION:
        FIND FIRST bMember2 EXCLUSIVE-LOCK
             WHERE ROWID(bMember2) = ROWID(bMember).
        ASSIGN bMember2.memberName = 'New member name':U.
    END.

  • 3.3 No-wait: When use NO-WAIT with NO-ERROR, always check whether record is LOCKED or not

    Why? When you use NO-WAIT with NO-ERROR and record is locked, it also is not available. Checking only for AVAILABLE, will most likely cause undesirable outcome.

    /* bad */
    FIND FIRST bMember EXCLUSIVE-LOCK
         WHERE bMember.id = 0.346544767 NO-WAIT NO-ERROR.
    IF NOT AVAILABLE bMember THEN
        CREATE bMember.
    
    /* good */
    FIND FIRST bMember EXCLUSIVE-LOCK
         WHERE bMember.id = 0.346544767 NO-WAIT NO-ERROR.
    IF LOCKED bMember THEN
        UNDO, THROW NEW Progress.Lang.AppError('Member record is currently locked, please, try again later', 1000).
    ELSE IF NOT AVAILABLE bMember THEN
        CREATE bMember.

  • 3.4 No RECID: Don't use RECID, use ROWID. Don't store ROWID or RECID in database, use surrogate keys

    Why ROWID? RECID supported only for backward compatibility

    Why don't store? ROWID and RECID change after dump and load process. The same ROWID/RECID can be found in the same table (when multi-tenancy or data-partitioning is used)

    /* good */
    FIND FIRST bMember NO-LOCK
         WHERE ROWID(bMember) = rMemberRowId NO-ERROR.

  • 3.5 CAN-FIND: Use CAN-FIND instead of FIND FIRST/LAST or FOR FIRST/LAST if all you need is only to check that record exists

    Why not? If multiple indexes needed to effectively find a record (use FOR FIRST/LAST in this case).

    /* bad */
    FIND FIRST bMember NO-LOCK
         WHERE ROWID(bMember) = rMemberRowId NO-ERROR.
    IF AVAIABLE bMember THEN
        RETURN TRUE.
    ELSE
        RETURN FALSE.
    
    /* good */
    RETURN CAN-FIND (FIRST bMember NO-LOCK
                     WHERE ROWID(bMember) = rMemberRowId).

  • 3.6 TABLE-NAME: Always qualify table name for field name

    Why? To make code more readable

    Why? To make sure code is compliant with "Strict Compile" mode

    /* bad */
    FIND FIRST bMember NO-LOCK NO-ERROR.
    IF AVAIABLE bMember THEN
        RETURN memberId.
    
    /* good */
    FIND FIRST bMember NO-LOCK NO-ERROR.
    IF AVAIABLE bMember THEN
        RETURN bMember.memberId.

  • 3.7 USE-INDEX: Avoid using USE-INDEX statement. Use TABLE-SCAN if you need to read entire table.

    Why? AVM automatically selects the most appropriate index

    Why not? USE-INDEX can be used to force display order (applicable to temp-tables)

    Why not? In case you need to process records in batches and index selection can't be consistently enforced by WHERE clause (REPOSITION TO ROW-ID -> NEXT/PREV)

    /* bad */
    FOR EACH bMember NO-LOCK
       WHERE bMember.DOB > 01/01/1982 USE-INDEX memberSSNIdx:
    END.
    
    /* good (let AVM to choose index automatically). Use temp-table with appropriate index, if you need to have different order in UI */
    FOR EACH bMember NO-LOCK
       WHERE bMember.DOB > 01/01/1982:
    END.
    
    /* good (if the whole table scan is needed) */
    FOR EACH bMember NO-LOCK TABLE-SCAN:
    END.
    
    /* good (AVM will use index, if available, to prepare sorted result) */
    /* IMPORTANT: Be careful when combining WHERE and BY statement (use XREF to see used index) */
    FOR EACH bMember NO-LOCK BY bMember.firstName:
    END.

Comments

  • 4.1 Header comments: Every class or external procedure has to have header aligned to ABLDocs format

    Note: Put note in header if procedure/class is used by external system/service (exposed as REST service)

    /*------------------------------------------------------------------------
        File        : getMinUserInfo.p
        Purpose     : Use to find and return user information
        Syntax      : RUN getMinUserInfo.p (userId, OUTPUT dsUserInfo).
        Description : This procedure finds user information and returns it in ProDataSet
        Author(s)   : Aliaksandr Tarasevich
        Created     : <pre>Tue Nov 19 21:16:10 CET</pre>
        Notes       : Use to only retrieve minimal user information, otherwise, use getAllUserInfo.p
                      Used by external systems (REST/SOAP)
      ----------------------------------------------------------------------*/

  • 4.2 Internal comments: Use comments in internal procedures, functions, methods aligned to ABLDocs format

    /*------------------------------------------------------------------------------
     Purpose: Find a member and return TRUE if this member is active
     Notes: Doesn't take temporary member status into account
     @param ipdMemberId - member Id
     @return Is member active?
    ------------------------------------------------------------------------------*/
    METHOD PUBLIC LOGICAL isMemberActive (INPUT ipdMemberId AS DECIMAL):
    END METHOD.
    
    PROCEDURE isMemberActive:
      /*------------------------------------------------------------------------------
       Purpose: Find a member and return TRUE if this member is active
       Notes: Doesn't take temporary member status into account
       @param ipdMemberId - member Id
       @return Is member active?
      ------------------------------------------------------------------------------*/
    
      DEFINE INPUT  PARAMETER ipdMemberId AS DECIMAL NO-UNDO.
      DEFINE OUTPUT PARAMETER oplIsActive AS LOGICAL NO-UNDO.
    END PROCEDURE.
    
    FUNCTION isMemberActive RETURNS LOGICAL (INPUT ipdMemberId AS DECIMAL):
      /*------------------------------------------------------------------------------
       Purpose: Find a member and return TRUE if this member is active
       Notes: Doesn't take temporary member status into account
       @param ipdMemberId - member Id
       @return Is member active?
      ------------------------------------------------------------------------------*/
    END FUNCTION.

  • 4.3 Class Properties: Use comments for properties aligned to ABLDocs format

    /*
       Indicates whether document was previously loaded or not
     */
    DEFINE PUBLIC PROPERTY docLoaded AS LOGICAL NO-UNDO
      GET.
      PROTECTED SET.

  • 4.4 No commented code: Don't leave unused code commented - delete.

    Why? It makes code less readable and causes unused code to be picked up when developer tries to search code snip in codebase

    Why? Developers must use version control system to keep track of changes

    Note: If you commented out procedure / function / method calls, find whether commented procedure / function / method is used in other places, if not - delete it

    /* bad */
    
    /* removed as part of fix for PL-43516674 */
    /* IF bMember.memberId = dInvMemberId THEN DO:
           ...
       END. */
    
    /* temporary remove as part of N project */
    /* RUN makeMemberAsInvalid. */

Performance

  • 5.1 FOR FIRST/LAST: Prefer to use FOR FIRST or FOR LAST instead of FIND FIRST/LAST

    Why? FIND FIRST/LAST doesn't use multiple indexes (also there are issues with Oracle dataservers)

    Why not? Use if you need to update record and want to check whether record is locked or not.

    /* we have two single field indexes (benefitCodeIdx and memberIdIdx) */
    /* bad (only one index will be used) */
    FIND FIRST bMemberBenefit NO-LOCK
         WHERE bMemberBenefit.memberId    = 0.34521543
           AND bMemberBenefit.benefidCode = 'BLCA' NO-ERROR.
    
    /* good (both indexes will be used) */
    FOR FIRST bMemberBenefit NO-LOCK
        WHERE bMemberBenefit.memberId    = 0.34521543
          AND bMemberBenefit.benefidCode = 'BLCA':
    END.

  • 5.2 DEFINE BUFFER: Define buffer for each DB buffer

    Why? To avoid side-effects from buffer that may be used in internal procedures / functions

    Why? To prevent locking issues which can happen when buffer is used in STATIC/SINGLETON methods or PERSISTENT procedures

    Note: Define buffer as close as possible to a place it's going to be use (internal procedure or method). Try to avoid using globally defined buffers

    /* bad (if this find was called from static/singleton class - record will stay locked) */
    METHOD PUBLIC CHARACTER getMember():
        FIND FIRST member EXSLUSIVE-LOCK.
        IF AVAILABLE member THEN DO:
            ASSIGN member.memberViewCount = member.memberViewCount - 1.
            RETURN member.id.
        END.
    END.
    
    /* good (if this find was called from static/singleton class - record will lock will be released) */
    METHOD PUBLIC CHARACTER getMember():
        DEFINE BUFFER bMember FOR member.
        FIND FIRST bMember EXSLUSIVE-LOCK.
        IF AVAILABLE bMember THEN DO:
            ASSIGN bMember.memberViewCount = bMember.memberViewCount - 1.
            RETURN bMember.id.
        END.
    END.

  • 5.3 BY-REFERENCE: Always use BY-REFERENCE or BIND when passing temp-table or dataset to procedures/methods

    Why? By default AVM clones temp-table / dataset when it's passed as parameter (BY-VALUE)

    Why not? If you want to merge result manually or target procedure changes cursor positions in temp-table

    /* bad */
    RUN getMemberInfo.p ( OUTPUT DATASET pdsMemberInfo ).
    
    /* good */
    RUN getMemberInfo.p ( OUTPUT DATASET pdsMemberInfo BY-REFERENCE ).

Variables

  • 6.1 No-undo: Always use NO-UNDO on all temp-tables and variables

    Why? When you define variables, the AVM allocates what amounts to a record buffer for them, where each variable becomes a field in the buffer. There are in fact two such buffers, one for variables whose values can be undone when a transaction is rolled back and one for those that can't. There is extra overhead associated with keeping track of the before-image for each variable that can be undone, and this behavior is rarely needed.

    Why not? If you need to be able to revert value of variable on UNDO

    /* bad */
    DEFINE TEMP-TABLE ttMember
      FIELD member_name AS CHARACTER.
    DEFINE VARIABLE cMemberName AS CHARACTER.
    DEFINE INPUT PARAMETER ipcMemberName AS CHARACTER.
    DEFINE PROPERTY MemberName AS CHARACTER
      GET.
      SET.
    
    /* good */
    DEFINE TEMP-TABLE ttMember NO-UNDO
      FIELD member_name AS CHARACTER.
    DEFINE VARIABLE cMemberName AS CHARACTER NO-UNDO.
    DEFINE INPUT PARAMETER ipcMemberName AS CHARACTER NO-UNDO.
    DEFINE PROPERTY MemberName AS CHARACTER NO-UNDO
      GET.
      SET.

Naming Conventions

  • 7.1 Variable Case: Use appropriate case when naming variable

    • When define variable use camelCase
    /* bad */
    DEFINE VARIABLE Member_Name AS CHARACTER NO-UNDO.
    DEFINE VARIABLE membername  AS CHARACTER NO-UNDO.
    DEFINE VARIABLE MeMbErNaMe  AS CHARACTER NO-UNDO.
    /* good */
    DEFINE VARIABLE cMemberName AS CHARACTER NO-UNDO.
    • When define constants use UPPER_CASE
    /* bad */
    DEFINE PRIVATE PROPERTY lineseparator AS CHARACTER NO-UNDO INIT '|':U
        GET.
    DEFINE PRIVATE PROPERTY LineSeparator AS CHARACTER NO-UNDO INIT '|':U
        GET.
    DEFINE PRIVATE PROPERTY cLineSeparator AS CHARACTER NO-UNDO INIT '|':U
        GET.
    DEFINE PRIVATE PROPERTY LINESEPARATOR AS CHARACTER NO-UNDO INIT '|':U
        GET.
    
    /* good */
    DEFINE PRIVATE PROPERTY LINE_SEPARATOR AS CHARACTER NO-UNDO INIT '|':U
        GET.
    • When define property use camelCase, unless you do it for GUI for .NET, then use PascalCase
    /* bad */
    DEFINE PROPERTY Member_Name AS CHARACTER NO-UNDO
        GET.
        SET.
    DEFINE PROPERTY MeMbErNaMe AS CHARACTER NO-UNDO
        GET.
        SET.
    
    /* good for GUI for .NET */
    DEFINE PROPERTY MemberName AS CHARACTER NO-UNDO
        GET.
        SET.
    
    /* good */
    DEFINE PROPERTY memberName AS CHARACTER NO-UNDO
        GET.
        SET.

  • 7.2 Buffer Name: When define buffer, prefix with b

    /* bad */
    DEFINE BUFFER MI1 FOR memberInfo.
    DEFINE BUFFER bu-memberInfo FOR memberInfo.
    DEFINE BUFFER memberInfoBuffer FOR memberInfo.
    DEFINE BUFFER cMemberInfoBuffer FOR memberInfo.
    DEFINE BUFFER bu-memberInfo-2 FOR memberInfo.
    
    /* good */
    DEFINE BUFFER bMemberInfo FOR memberInfo.
    DEFINE BUFFER bMemberInfo2 FOR memberInfo.
    DEFINE BUFFER bProviderData FOR providerData.
    DEFINE BUFFER b_Field FOR _Field.
    DEFINE BUFFER bttMyTable FOR TEMP-TABLE ttMyTable.

  • 7.3 Variable Type: Prefix variable name with it's type

    DEFINE VARIABLE cMemberName   AS CHARACTER    NO-UNDO.
    DEFINE VARIABLE iMemberNumber AS INTEGER      NO-UNDO.
    DEFINE VARIABLE dMemberId     AS DECIMAL      NO-UNDO.
    DEFINE VARIABLE hMemberReq    AS HANDLE       NO-UNDO.
    DEFINE VARIABLE dtStartDate   AS DATE         NO-UNDO.
    DEFINE VARIABLE tEndDate      AS DATETIME     NO-UNDO.
    DEFINE VARIABLE tzEndDate     AS DATETIME-TZ  NO-UNDO.
    DEFINE VARIABLE mMemberDoc    AS MEMPTR       NO-UNDO.
    DEFINE VARIABLE rMemberKey    AS RAW          NO-UNDO.
    DEFINE VARIABLE oMemberInfo   AS member.info  NO-UNDO.
    DEFINE VARIABLE lcMemberNode  AS LONGCHAR     NO-UNDO.

  • 7.4 Global Variables: Prefix prefix global variables (variables defined in main block or in class body) with 'g'

    DEFINE VARIABLE gcMemberName   AS CHARACTER    NO-UNDO.
    DEFINE VARIABLE giMemberNumber AS INTEGER      NO-UNDO.
    DEFINE VARIABLE ghMemberReq    AS HANDLE       NO-UNDO.

  • 7.5 Prefix input/output variable: Prefix parameter variable with input/output type (ip - INPUT, op - OUTPUT, iop - INPUT-OUTPUT)

    DEFINE INPUT  PARAMETER ipcMemberName      AS CHARACTER    NO-UNDO.
    DEFINE OUTPUT PARAMETER opiMemberNumber    AS INTEGER      NO-UNDO.
    DEFINE INPUT-OUTPUT PARAMETER iopdMemberId AS DECIMAL      NO-UNDO.
    
    METHOD PUBLIC LOGICAL checkMemberReq (INPUT         iphMemberReq  AS HANDLE).
    METHOD PUBLIC LOGICAL checkMemberReq (OUTPUT        opdtStartDate AS DATE).
    METHOD PUBLIC LOGICAL checkMemberReq (INPUT-OUTPUT  iotEndDate    AS DATETIME).

  • 7.6 Prefix temp-table/prodataset: Put prefix on temp-tables (tt, bi), datasets (pds) and datasources (ds)

    /* bad */
    DEFINE TEMP-TABLE tbMember...
    DEFINE TEMP-TABLE ttblMember...
    DEFINE TEMP-TABLE ttMember NO-UNDO BEFORE-TABLE bttMember...
    DEFINE DATASET pMember...
    
    /* good */
    DEFINE TEMP-TABLE ttMember NO-UNDO BEFORE-TABLE bittMember...
    DEFINE DATASET pdsMember...
    DEFINE DATA-SOURCE dsMember...

  • 7.7 Meaningful Names: Define variables with meaningful names (applicable to context), but avoid extra long names (use abbreviations when possible)

    /* bad */
    DEFINE VARIABLE cMI AS CHARACTER NO-UNDO.
    /* good */
    DEFINE VARIABLE cMemberInfo AS CHARACTER NO-UNDO.
    
    /* bad */
    DEFINE VARIABLE cNationalDrugCode AS CHARACTER NO-UNDO.
    /* good */
    DEFINE VARIABLE cNDC AS CHARACTER NO-UNDO.
    
    /* bad */
    DEFINE VARIABLE cNationalDrugCodeRequiredIdentification NO-UNDO.
    /* good */
    DEFINE VARIABLE cNDCReqId AS CHARACTER NO-UNDO.

Dynamic Objects

  • 8.1 Delete Dynamic Objects: Always delete dynamic objects. Use FINALLY block to make sure object will be deleted.

    Why? Progress garbage collector takes care of objects, but doesn't handle dynamic objects (BUFFERS, TABLES, QUERIES, PERSISTENT PROCEDURES and etc)

    /* bad */
    PROCEDURE checkMember:
        DEFINE OUTPUT PARAMETER oplValidMember AS LOGICAL NO-UNDO.
        DEFINE VARIABLE hMemberBuffer AS HANDLE NO-UNDO.
    
        CREATE BUFFER hMemberBuffer FOR db + '.memberInfo'.
        /* ... */
        ASSIGN oplValidMember = TRUE.
        RETURN.
    END.
    
    /* good */
    PROCEDURE checkMember:
        DEFINE OUTPUT PARAMETER oplValidMember AS LOGICAL NO-UNDO.
        DEFINE VARIABLE hMemberBuffer AS HANDLE NO-UNDO.
    
        CREATE BUFFER hMemberBuffer FOR db + '.memberInfo'.
        /* ... */
        ASSIGN oplValidMember = TRUE.
        RETURN.
        FINALLY:
            IF VALID-HANDLE(hMemberBuffer) THEN
                DELETE OBJECT hMemberBuffer.
        END.
    END.

  • 8.2 MEMPTR: Always deallocate memory allocated for MEMPTR.

    Why? Progress garbage collector doesn't take care of memory pointers.

    Why not? If you need to pass memory pointer to caller procedure/method as output parameter. Then make sure you clean up there.

    /* bad (cause memory leak) */
    PROCEDURE parseFile:
        DEFINE VARIABLE mBlob AS MEMPTR NO-UNDO.
        ...
        COPY-LOB FILE 'path-to-file' TO mBlob.
        ...
    END.
    
    /* good */
    PROCEDURE parseFile:
        DEFINE VARIABLE mBlob AS MEMPTR NO-UNDO.
        ...
        COPY-LOB FILE 'path-to-file' TO mBlob.
        ...
        FINALLY:
            ASSIGN SET-SIZE(mBlob) = 0.
        END.
    END.
    
    /* good */
    RUN parseFile(OUTPUT mLoadedFile).
    
    FINALLY:
        ASSIGN SET-SIZE(mLoadedFile) = 0.
    END.
    
    PROCEDURE loadFile:
        DEFINE OUTPUT PARAMETER opmBlob AS MEMPTR NO-UNDO.
        ...
        COPY-LOB FILE 'path-to-file' TO opmBlob.
        ...
    END.
    

Code Styling

  • 9.1 Unnecessary Blocks: Don't create unnecessary DO blocks

    /* bad */
    IF NOT isValidMember(bMember.id) THEN DO:
        UNDO, THROW NEW Progress.Lang.AppError('Invalid Member', 1000).
    END.
    
    /* good */
    IF NOT isValidMember(bMember.id) THEN
      UNDO, THROW NEW Progress.Lang.AppError('Invalid Member', 1000).

  • 9.2 No Comparison operators: Avoid using comparison operators: EQ(=), LT(<), LE(<=), GT(>), GE(>=), NE(<>)

    /* bad */
    IF memberDOB GT 01/01/1980 THEN
    
    /* good */
    IF memberDOB > 01/01/1980 THEN

  • 9.3 Dot/Colon Same Line Put dot and colon on the same line:

    /* bad */
    IF memberDOB > 01/01/1980 THEN
      RETURN memberDOB
      .
    
    /* bad */
    IF memberDOB > 01/01/1980 THEN
      RETURN memberDOB
    .
    
    /* bad */
    FOR EACH memberInfo NO-LOCK
      :
    
    /* bad */
    FOR EACH memberInfo NO-LOCK
       WHERE memberInfo.memberId = 0.54764767
       :
    
    /* good */
    IF memberDOB > 01/01/1980 THEN
      RETURN memberDOB.
    
    /* good */
    FOR EACH memberInfo NO-LOCK:
    
    /* good */
    FOR EACH memberInfo NO-LOCK
       WHERE memberInfo.memberId = 0.54764767:

  • 9.4 Block Indentation: Use correct block indentation, put DO statement on the same line. Make sure you configured Tab policy in Eclipse to use Spaces only (4 spaces per tab)

    /* bad */
    IF memberDOB > 01/01/1980 THEN ASSIGN RETURN memberName.
    
    /* bad */
    IF memberDOB > 01/01/1980 THEN 
      DO:
        RETURN memberName.
      END.
    
    /* bad */
    IF memberDOB > 01/01/1980 THEN DO:
    IF memberCode = 'OBC' THEN DO:
    END.
    END.
    
    /* bad */
    IF memberDOB > 01/01/1980 THEN
      RETURN memberName.
      ELSE
        RETURN memberCode.
    
    

/* good (new line + tab) */ IF memberDOB > 01/01/1980 THEN RETURN memberName.

/* good */
IF memberDOB > 01/01/1980 THEN DO:
    ...
    RETURN memberName.
END.

```

  • 9.5 WHERE NEW LINE: Always put WHERE and AND/OR on next line.

    /* bad */
    FOR EACH memberInfo FIELDS(birthDate gender) NO-LOCK WHERE memberInfo.birthDate > 01/01/1920 AND memberInfo.gender = 'M':
    
    /* good */
    FOR EACH memberInfo FIELDS(birthDate gender) NO-LOCK
       WHERE memberInfo.birthDate > 01/01/1920
         AND memberInfo.gender    = 'M'

  • 9.6 Parameters: Put first method/function/procedure parameter on the same line. If method has more than 3 parameters, put every parameter on new line (aligned to first parameter)

    /* bad */
    RUN loadFile (
        cFileName,
        cFileType, OUTPUT mFileBody).
    
    /* bad */
    RUN loadFile (
        cFileName, cFileType, OUTPUT mFileBody
        ).
    
    /* bad */
    ASSIGN mFileBody =
        loadFile (cFileName, cFileType, OUTPUT mFileBody).
    
    /* good */
    RUN loadFile (cFileName, cFileType, OUTPUT mFileBody).
    
    /* good */
    RUN loadFile (INPUT cFileName,
                  INPUT cFileType,
                  INPUT cLoadCodePage,
                  OUTPUT mFileBody).
    
    /* good */
    ASSIGN mFileBody = loadFile (cFileName, cFileType).
    

  • 9.7 If Parentheses: Always use parentheses when have AND and OR conditions or use IF in ASSIGN statement

    Why? Even though precedence order is known, some people forget it or it gets mixed.

    /* good */
    IF (isValidMember OR showAll) AND (memberDOB < 01/01/1982 OR memberStatus = 'A') THEN
      ...
    
    /* bad (cause unexpected behaviour - last name will be only used if member doesn't have first name) */
    ASSIGN cMemberFullName = IF cMemberFirstName > '' THEN cMemberFirstName ELSE '' + ' ' + cMemberLastName.
    
    /* good */
    ASSIGN cMemberFullName = (IF cMemberFirstName > '' THEN cMemberFirstName ELSE '') + ' ' + cMemberLastName.

  • 9.8 End with type: Always specify what is end used for (PROCEDURE, CONSTRUCTOR, DESTRUCTOR, METHOD or FUNCTION)

    /* bad */
    PROCEDURE checkMember:
    END.
    
    FUNCTION checkMember RETURNS LOGICAL():
    END.
    
    CONSTRUCTOR Member:
    END.
    
    DESTRUCTOR Member:
    END.
    
    METHOD PUBLIC LOGICAL checkMember():
    END.
    
    /* good */
    PROCEDURE checkMember:
    END PROCEDURE.
    
    FUNCTION checkMember RETURNS LOGICAL():
    END FUNCTION.
    
    CONSTRUCTOR Member:
    END CONSTRUCTOR.
    
    DESTRUCTOR Member:
    END DESTRUCTOR.
    
    METHOD PUBLIC LOGICAL checkMember():
    END METHOD.

  • 9.9 Consistent method/function return: Either return value or use output parameters (don't mix) when working with methods / functions

    /* bad */
    ASSIGN lValidMember = oMemberInfo:getMemberInfo(iMemberId, OUTPUT cMemberName, OUTPUT dMemberDOB).
    
    /* good */
    oMemberInfo:getMemberInfo(iMemberId, OUTPUT lValidMember, OUTPUT cMemberName, OUTPUT dMemberDOB).
    
    /* good (split into couple calls) */
    IF oMemberInfo:isValidMember(iMemberId) THEN
      oMemberInfo:getMemberInfo(iMemberId, OUTPUT cMemberName, OUTPUT dMemberDOB).
    

  • 9.10 Don't abbreviate: Don't abbreviate keywords, tables, fields

    Why? To make code more readable

    Why? To make sure code is compliant with "Strict Compile" mode

    /* bad */
    DEF VAR iCnt AS INT NO-UNDO.
      
    /* good */
    DEFINE VARIABLE iCnt AS INTEGER NO-UNDO.
      
    /* bad (full field name - memberAgeInDays) */
    FIND FIRST bMember NO-LOCK
         WHERE bMember.memberAge > 4000.
      
    /* good */
    FIND FIRST bMember NO-LOCK
         WHERE bMember.memberAgeInDays > 4000.
    

  • 9.11 No keywords: Don't use keywords for properties, fields, variables, class names

    /* bad property name */
    DEFINE PRIVATE PROPERTY GUID AS CHARACTER NO-UNDO
      GET.
      SET.
    
    /* good property name */
    DEFINE PRIVATE PROPERTY memberGUID AS CHARACTER NO-UNDO
      GET.
      SET.
    
    /* bad class name */
    CLASS Message:
    
    /* good class name */
    CLASS JmsMessage:
    
    /* bad temp-table field name */
    DEFINE TEMP-TABLE ttMemberChanges NO-UNDO
        FIELD name AS DECIMAL.
    
    /* good temp-table field name */
    DEFINE TEMP-TABLE ttMemberChanges NO-UNDO
        FIELD termName AS DECIMAL.

  • 9.12 Backslash: Prefer to use forward slash to backslash. If forward slash can't be used, escape backslash with a tilde.

    Why? Can cause unexpected behavior as backslash is an escape-character on Linux

    /* bad */
    OUTPUT TO VALUE(SESSION:TEMP-DIRECTORY + '\export\dexp.txt').
    /* good (on Windows and Linux) */
    OUTPUT TO VALUE(SESSION:TEMP-DIRECTORY + '/export/exports.txt').
        
    /* bad - result in Linux - HELLO, in Windows - HE\LLO */
    MESSAGE 'HE\LLO'.
    /* good - result HE\LLO in Linux and Windows */
    MESSAGE 'HE~\LLO'.
    

  • 9.13 No LIKE Never use LIKE on database tables

    Why? Can cause unexpected results when schema changes (especially important when table is used by REST/SOAP adapter)

    /* bad */
    DEFINE TEMP-TABLE ttMember NO-UNDO LIKE memberInfo.
        
    /* good */
    DEFINE TEMP-TABLE ttMember NO-UNDO
        FIELD memberName LIKE memberInfo.memberName
        FIELD memberDOB  LIKE memberInfo.memberDOB
        ...

Other

  • 10.1 Block Labels: Always use block labels

    Why? If you do not name a block, the AVM leaves the innermost iterating block that contains the LEAVE statement. The same is applicable to UNDO and NEXT statements. THis can cause unexpected behavior

    /* bad */
    DO TRANSACTION:
        FOR EACH bMemberBenefit NO-LOCK:
            FIND FIRST bMemberBenefit2 EXCLUSIVE-LOCK
                 WHERE ROWID(bMemberBenefit2) EQ ROWID(bMemberBenefit).
            ...
            /* this will affect current iteration only */
            IF bMemberBenefit.benefitDate > TODAY THEN
                UNDO, LEAVE.
        END.
    END.
    
    /* good */
    UpdateMembersBlk:
    DO TRANSACTION:
        FOR EACH bMemberBenefit NO-LOCK:
            FIND FIRST bMemberBenefit2 EXCLUSIVE-LOCK
                 WHERE ROWID(bMemberBenefit2) EQ ROWID(bMemberBenefit).
            ...
            /* this will undo the entire transaction and leave DO block */
            IF bMemberBenefit.benefitDate > TODAY THEN
                UNDO UpdateMembersBlk, LEAVE UpdateMembersBlk.
        END.
    END.
    

  • 10.2 Assign Statement: Always use ASSIGN statement (even on single assignments)

    Why? This method allows you to change several values with minimum I/O processing. Otherwise, the AVM re-indexes records at the end of each statement that changes the value of an index component.

    ASSIGN bMember.dob  = 01/01/1980
           bMember.name = 'John'
           bMember.ssn  = '000-00-0000' WHEN lKeepSSN
           bMember.mid  = IF NOT lKeepSSN THEN '111' ELSE '000-00-0000'.

  • 10.3 Use Substitute: Use SUBSTITUTE to concatenate multiple values

    Why? If you try to concatenate values and one of the values is ? the entire result becomes ?, which is in most cases an undesirable result.

    ASSIGN cMemberName  = 'John'
           dtMemberDOB  = 01/01/1980
           cMemberAge   = ?.
    /* bad - show ? */
    MESSAGE cMemberName + STRING(dMemberDOB) + cMemberAge VIEW-AS ALERT-BOX.
    
    /* good - show 'John 01/01/1980 ?' */
    MESSAGE SUBSTITUTE('&1 &2 &3', cMemberName, dMemberDOB, cMemberAge).

  • 10.4 IN THIS-PROCEDURE: When call internal procedures use RUN ... IN THIS-PROCEDURE.

    Why? To prevent STOP condition when the specified procedure is not found.

    /* good */
    RUN myProcedureName IN THIS-PROCEDURE.
    

  • 10.5 Class Structure: Use the following class structure

    /* Class Description */
        
    /* USING Statements */
        
    /* Routine Level */
        
    /* Preprocessor Definitions */
        
    CLASS className...:
            
        /* Includes with temp-tables/dataset definitions */
            
        /* Define Variables */
            
        /* Define Events */
            
        /* Define Properties */
        
        /* Constructors */
        
        /* Methods */
        
        /* Destructor */
            
    END CLASS.
    

  • 10.6 Procedure Structure: Use the following procedure structure

    /* Procedure Description */
    
    /* USING Statements */
        
    /* Routine Level */
        
    /* Preprocessor Definitions */
        
    /* Define Input-Output Parameters */
        
    /* Includes with temp-tables/dataset definitions */
        
    /* Define Local Variables */
        
    /* Define Functions */
        
    /* MAIN-BLOCK */
        
    /* Define Procedures */
        

About

Style guides for OpenEdge projects