Introduction
What's in a name? That which we call a rose
By any other name would smell as sweet
William Shakespeare, "Romeo & Juliet"
In this article, we will describe a set of naming conventions for ObjectScript code.
Code naming conventions exist for several important reasons:
- Readability: Consistent names improve code clarity and comprehension. Following naming conventions makes it easier to identify and remember components.
- Maintainability: Properly named code elements ease the maintenance and updating of code and configuration items, reducing confusion and errors.
- Collaboration: in a team setting, having a common naming convention ensures that everyone is on the same page. It promotes smoother collaboration and reduces the likelihood of miscommunication.
- Debugging: clear and consistent names can help in quickly identifying and fixing issues. Descriptive names can indicate the purpose and scope of a code element, aiding in the debugging process.
- Documentation: following naming conventions can simplify the process of creating and maintaining documentation. Names that reflect their function or purpose make it easier to explain and understand the code.
As IRIS developers and administrators, we are naming various types of objects, some of which have numerous instances. For example, interoperability production with dozens of configurations items and data transformation classes is not at all uncommon in real-world scenarios such as healthcare patient and appointment data routing.
The aim of this article is to compile bits from various sources of information in a consistent proposal for robust naming conventions. As the many names defined are used in ObjectScript code, it also describes some coding guidelines.
Compiler items
In this part, we’ll describe guidelines for naming:
- packages
- classes, includes and routines
- class members: parameters, properties, methods and XData blocks
- local variables and method parameters
Packages
Package names use only lower-case letters and decimal digits. The first character must be a lower-case letter.
The root package has at least one component, and it identifies the source entity, organization or group.
Examples:
- dc
- myorg
- acme
Packages under root denote the purpose of the classes. They may have subpackages denoting a finer purpose or domain, and use lower case letters, except for application domains that are spelled in upper case letters.
The following root subpackage names are reserved
Compiler item purpose |
Package |
CSP page |
csp |
Data models and transfer objects (classes _not_ extending %Persistent) |
model |
Data type |
type |
Include resources: macros, constants, … |
inc |
Interoperability component - business adapter |
interop.ba |
Interoperability component - business service |
interop.bs |
Interoperability component - business process |
interop.bp |
Interoperability component - business operation |
interop.bo |
Interoperability component - message |
interop.msg |
Interoperability component - data transform |
interop.dt |
Persistent entity (classes extending %Persistent) |
entity |
REST API |
api |
Routines |
rou |
Scheduled tasks |
tas |
Service |
service |
SOAP Web Service |
ws |
Utility class |
lib |
Classes and other elements
Class names use upper camel case, must start with an uppercase letter and use letters and decimal digits. Avoid starting with "%".
Routine names should be all uppercase. Avoid starting with "%".
Item purpose |
Class name pattern |
Examples of fully qualified class names |
CSP page |
<name> |
myorg.csp.app.LogonForm |
Data models and transfer objects (classes _not_ extending %Persistent) |
<name> |
myhospital.model.Patient |
Data types |
<name> |
acme.type.FusionReactorType myhospital.type.patient.MRN |
Include resources: macros, constants, … |
<name> |
myorg.inc.Errors |
Interoperability component - inbound business adapter |
<name>InboundAdapter |
myorg.interop.ba.hl7.UNCPathFileInboundAdapter |
Interoperability component - outbound business adapter |
<name>OutboundAdapter |
acme.interop.ba.CustomTCPOutboundAdapter |
Interoperability component - duplex business adapter |
<name>DuplexAdapter |
myorg.interop.ba.CustomTCPDuplexAdapter |
Interoperability component - business service |
<name>Service |
myhosp.interop.bs.FileService |
Interoperability component - business process |
<name>Process |
myhosp.interop.bp.AppointmentCancelProcess |
Interoperability component - business operation |
<name>Operation |
myhosp.interop.bo.WISH.PatientOperation |
Interoperability component - request message (extends Ens.Request) |
<verb>[<resource>]Request |
myorg.interop.msg.patient.GetRequest myorg.interop.msg.patient.GetEncountersRequest |
Interoperability component - response message (extends Ens.Response) |
[<name>]Response |
myorg.interop.msg.patient.Response myhospital.interop.msg.invoicing.BillInsuranceResponse |
Interoperability component - other message (extends Ens.MessageBody) |
<name> |
myhospital.interop.msg.patient.PatientUpdatedEvent myorg.interop.msg.document.Container |
Interoperability component - data transform |
[<source-applicaton>]<source-format>To[<target-application>]<target-format> |
myorg.interop.dt.ULTRAGENDASIUToAppointmentUpdate |
Persistent entities (classes extending %Persistent) |
<name> |
myorg.entity.Document |
REST APIs - generated classes (lowercase letters) |
impl disp |
myhospital.api.terminology.impl myhospital.api.terminology.disp |
Routines |
<name> |
myhosp.rou.PHUTL001 |
Scheduled tasks |
<name>Task |
myhospital.task.CancelAppointmentsTask |
Service |
<name>Service |
service |
SOAP Web Services |
<name>WS |
acme.ws.accounting.SupplierWS |
Utility classes |
<name> |
myorg.lib.xml.Utils |
Class members
Class members names use upper camel case, must start with an uppercase letter (avoid using '%'), and use letters and decimal digits.
Member |
Convention |
Example |
Parameter |
Upper camel case or all uppercase |
MRNCODESYSTEM |
Properties |
Upper camel case |
BirthDate |
Method |
Upper camel case. Favor a <verb><object> pattern |
FetchPatient |
Xdata |
Upper camel case |
HL7Mappings |
Local variables & method parameters
Local variable names and method parameters use lower camel case and start with a lower-case letter.
Some examples: request, response, patientId, mrn
Instance (i%..., r%...) and process (%...) variables follow the same convention.
Coding guidelines
Block syntax
A block statement, or compound statement, lets you group any number of statements (including 0) into one statement.
ObjectScript currently supports two syntaxes for blocks:
- Curly brace block syntax
- Dot block syntax
Curly brace block syntax
It is similar to that in C, Java, C#, … making the following short example look very familiar to most programmers:
if a=0 {
write "foo",!
write "bar",!
}
Dot block syntax
This is the original MUMPS block syntax. It is supported for backward compatibility with (very) old code. Its use is strongly discouraged, as it can get quite confusing, especially when combined with the short version of commands and the lack of reserved words, as in the following (intentionally a little mischievous) example:
f j=1:1:d d
. r i
. i '$test b
. i i'="" d
.. s d=$p(l," ",1) 41)
.. s w=$p(l," ",2)
.. w d,?10,$e(^title(d),1,80),!
Post-conditionals
This is an implementation in ObjectScript of the concept of guarded command, as defined by Dijkstra (1975).
It is a conditionally executed statement, where a boolean expression "guards" the execution of the statement.
<command>:<condition> <command arguments>
It is functionally equilavent to
if <condition> <command> <command arguments>
Although the concept is well defined, the syntax is not common, so when should it be used instead of an if statement?
- execution flow control: quit, continue, throw
- default value assignment: set
some examples:
quit on error, continue on condition
quit:$$$ISERR(sc)
#Dim a as %Integer
while a > 0 {
…
continue:a=5
…
}
throw on condition
#Dim obj as %RegisteredObject
throw:'$isobject(obj) ##class(%Exception.General).%New("object not found")
Assign default value
#Dim obj as Foo
…
set:'$isobject(obj) obj = ##class(Foo).%New()
…
Use return instead of quit for return values
In new code, use return instead of quit, as quit can be used both for exiting current execution context and return a value.
'quit' has two different meanings :
- when use with no argument, it exits current execution context (e.g. loop)
- when use with an argument, it exits current routine/method and returns value
'return' is an addition to ObjectScript meant to improve code readability, as it implements only the second meaning.
Command arguments
The use of a comma-separated list of command arguments should be avoided, as for some commands, it gets confusing.
For example,
if a=0,b=1 {
...
}
It is much less readable (to the 'modern' reader) than
if (a=0)&&(b=1) {
...
}
Ternary operator - expressional 'if'
The $select function can be used as ternary if operator:
$select(<boolean expression>:<true value>,1:<false value>)
Switch/case
Either
- $case() when switch intent is to select a value
- if elseif elseif … when switch intent is to select behaviour
Command keywords
Command keywords are not case-sensitive, and most commands come in two variants, fully named and shorthand.
- Favor the use of full command keywords, except for the most common ones like 'set' and 'do'
- Use all lowercase for command keywords
- Avoid using legacy goto <label> command for flow control
Function names
As commands, function names are not case-sensitive and most functions come in two variants, fully named and shorthand.
- Favor the use of full function names instead of shorthand, e.g. use $extract() instead of $e
- Use all lowercase for intrinsic function names
- Use upper camel case for extrinsic function names
Method parameters and return values
- group optional parameters at the end
- if the method is a function that returns a data type or OREF, and returns a %Status, the %Status is returned as the last parameter
Interoperability production items
Interoperability productions can easily count a sizeable of business hosts. A consistent naming scheme helps a lot with readability across the various actors reading the names: developers, administrators and support staff.
Business services
Propose |
Name pattern |
Examples |
Receive messages from an application |
<format>From<application> |
SIUFromULTRAGENDA |
Receive deferred responses |
<application>Response |
DOCSHIFTERResponse |
REST or SOAP API |
<name>Service |
TerminologyService |
Business processes
Purpose |
Name pattern |
Examples |
Process requests Orchestration |
<name>Process |
AppointmentCancelProcess DocumentProcess |
Route messages |
<format>Router |
ADTRouter |
Business operations
Purpose |
Name pattern |
Examples |
Send messages to an application or application component. Optionally use suffixes to denote application subcomponents or environments |
<format>To<application>[_<component>] |
SIUToWISH HL7ToArchive |
Query external system and return responses |
<name>Operation |
ULTRAGENDAAPIOperation |
Duplex operation |
<name>Duplex |
|
Business duplexes
Classes extending Ens.BusinessDuplex are use
<name>Duplex
Some examples
Class method
/// <p>Purges all message bodies associated with sessionId and if purgeHeaders is set, purge headers too.</p>
/// <p><b>purged</b> returns the total count of items successfully purged, and the count by class name in the first subscript.</p>
/// <p>Stops and returns error status if any error occurs during purge.</p>
ClassMethod PurgeSessionMessageBodies(sessionId As %Integer, Output purged As %Integer, purgeHeaders As %Boolean = 0, noLock As %Boolean = 1) As %Status
{
#Dim sc as %Status
#Dim ex as %Exception.AbstractException
#Dim stmt as %SQL.Statement
#Dim rs as %SQL.StatementResult
s sc = $$$OK
try {
s stmt = ##class(%SQL.Statement).%New()
s rs = stmt.%ExecDirect(,"select"_$select(noLock:" %NOLOCK",1:"")_" ID as HeaderId,MessageBodyClassName as BodyClass,MessageBodyId as BodyId from Ens.MessageHeader where SessionId=?",sessionId)
while rs.%Next() {
if ($length(rs.BodyClass) > 1) && $$$ISOK($classmethod(rs.BodyClass,"%DeleteId",rs.BodyId)) {
d $increment(purged)
d $increment(purged(rs.BodyClass))
}
if purgeHeaders {
$$$TOE(sc,##class(Ens.MessageHeader).%DeleteId(rs.HeaderId))
d $increment(purged)
d $increment(purged("Ens.MessageHeader"))
}
}
}
catch (ex) {
s sc = ex.AsStatus()
}
return sc
}
ObjectScriptObjectScript
Outbound adapter
/// HL7 file outbound adapter, using <class>ks.lib.file.ba.UNCOutboundAdapter</class>
/// This adapter also adds expression parsing to <method>CreateFilename</method> : see <method>ks.lib.hl7.Utils.ParseExpressions</method>
Class ks.interop.hl7.ba.FileOutboundAdapter Extends ks.interop.file.ba.UNCOutboundAdapter
{
// keeping parameter names as in superclass for clarity
Method CreateFilename(ByRef pFileName As %String, ByRef pSpec As %String, ByRef pIsVMS As %Boolean, ByRef pDirectory As %String, ByRef pLocal As %Boolean) As %String
{
#Dim sc as %Status
#Dim ex as %Exception.AbstractException
s sc = $$$OK
try {
if $isobject(..BusinessHost.%RequestHeader) &&
$classmethod(..BusinessHost.%RequestHeader.MessageBodyClassName,"%Extends","EnsLib.HL7.Message") {
s msg = ##class(EnsLib.HL7.Message).%OpenId(..BusinessHost.%RequestHeader.MessageBodyId)
if $isobject(msg) {
s pSpec = ##class(ks.lib.hl7.Utils).ParseExpressions(msg,pSpec,.sc)
$$$TRACE("spec after HL7 expressions parsing : "_pSpec)
}
}
}
catch (ex) {
// do nothing, fall back to ##super
}
return ##super(.pFileName,.pSpec,.pIsVMS,.pDirectory,.pLocal)
}
}
ObjectScriptObjectScript
/// A string datatype definition which extends <class>%Library.String</class> with additional regex pattern validation. <br />
Class ks.lib.type.RegExString Extends %String
{
/// Set PATTERN to empty and final, as it is not relevant on
/// this type, but is inherited from <class>%Library.String</class>
Parameter PATTERN [ Final ];
/// Set VALUELIST to empty and final, as it is not relevant on
/// this type, but is inherited from <class>%Library.String</class>
Parameter VALUELIST [ Final ];
/// Set DISPLAYLIST to empty and final, as it is not relevant on
/// this type, but is inherited from <class>%Library.String</class>
Parameter DISPLAYLIST [ Final ];
/// Set a valid regex pattern for value validation
Parameter REGEX As STRING;
/// The XMLPATTERN to regex by default. Can be overridden.
Parameter XMLPATTERN = {..#REGEX};
ClassMethod IsValid(%val As %Library.RawString) As %Status [ ServerOnly = 0 ]
{
#Dim sc as %Status = $$$OK
#Dim ex as %Exception.AbstractException
try {
$$$TOE(sc,##class(%String).IsValid(%val))
if (..#REGEX '= "") {
if '$match(%val, ..#REGEX) {
s sc = $$$ERROR($$$DTPattern, %val, ..#REGEX)
}
}
}
catch (ex) {
s sc = ex.AsStatus()
}
q sc
}
}
ObjectScriptObjectScript
Class myhosp.interop.dt.ADTNToFHIR Extends Ens.DataTransform
{
Parameter TARGETFHIRVERSION = "R4";
ClassMethod Transform(source As EnsLib.HL7.Message, ByRef target As Ens.StreamContainer, aux) As %Status
{
#Dim sc as %Status = $$$OK
#Dim ex as %Exception.AbstractException
#Dim sda as %Stream.TmpCharacter
#Dim fhir as HS.FHIR.DTL.Util.API.Transform.SDA3ToFHIR
#Dim schema as HS.FHIRServer.Schema
#Dim stream as %Stream.Object
#Dim patientId as myhosp.type.WISH.MRN
#Dim encounterId as myhosp.type.WISH.NADM
#Dim exportType as %String
try {
s schema = ##class(HS.FHIRServer.Schema).LoadSchema(..#TARGETFHIRVERSION)
if '$isobject(schema) throw ##class(%Exception.General).%New("FHIR Schema "_..#TARGETFHIRVERSION_" not found")
s patientId = source.GetValueAt("PID:3.1")
s encounterId = source.GetValueAt("PV1:19.1")
$$$TOE(sc,##class(HS.Gateway.HL7.HL7ToSDA3).GetSDA(source,.sda,0))
s fhir = ##class(HS.FHIR.DTL.Util.API.Transform.SDA3ToFHIR).TransformStream(sda,"HS.SDA3.Container",..#TARGETFHIRVERSION,patientId,encounterId)
s stream = ##class(%Stream.GlobalCharacter).%New()
s stream.%Location = "^MyHosp.FHIR.Stream"
s exportType = $select($data(aux):$select($isobject(aux):$select(aux.RuleActionUserData="":aux.RuleUserData,1:aux.RuleActionUserData),1:aux),1:"")
if exportType="JSON" {
s str = fhir.bundle.%ToJSON(stream)
} else {
d ##class(HS.FHIRServer.Util.JSONToXML).JSONToXML(fhir.bundle, .stream, schema)
}
s target = ##class(Ens.StreamContainer).%New(stream)
} catch (ex) {
s sc = ex.AsStatus()
}
return sc
}
}
ObjectScriptObjectScript
There are a lot of things I disagree with, but lets start with package names.
Lower case is a major break with existing ISC packages and I don't see the reason for it.
Camel case it should be.
Commands and Functions should start with a uppercase, the same for variables and arguments.
It is stupid to make an exception for Set and Do to be abbreviated.
Using underscores in names is very cumbersome because you have to use quotes around it.
Great work! I'm all for lower case for the packages names.