- Objects
- Error Handling
- Data Access
- Comments
- Performance
- Variables
- Naming Conventions
- Dynamic Objects
- Code Styling
- Other
-
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.
-
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.
-
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. */
-
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 ).
-
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.
-
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, oip - 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.
-
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.
-
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 ...
-
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 */