Proposal for ObjectScript naming conventions and coding guidelines
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.StatementResults sc = $$$OKtry {
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
}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.AbstractExceptions sc = $$$OKtry {
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)
}
}/// 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 validationParameter REGEX As STRING;/// The XMLPATTERN to regex by default. Can be overridden.Parameter XMLPATTERN = {..#REGEX};ClassMethod IsValid(%valAs%Library.RawString) As%Status [ ServerOnly = 0 ]
{
#Dim sc as%Status = $$$OK#Dim ex as%Exception.AbstractExceptiontry {
$$$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
}
}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%Stringtry {
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
}
}Comments
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.
A case(!) for lowercase package names was made by @Evgeny Shvarov in his 2021 post at https://community.intersystems.com/post/naming-convention-objectscript-…
Great work! I'm all for lower case for the packages names.
I develop and publish VS Code extensions targeting users of the InterSystems platforms. Occasionally an extension needs some support classes installed on the servers it works with. I propose establishing a naming convention for these classes, as follows:
- First dot-piece of the package name should be vscode
- Second dot-piece should be derived from the "publisher" property in the extension's package.json manifest, as follows:
- If publisher is "intersystems-community" then use dc
- Otherwise use the publisher string, transformed if necessary to conform to package-naming constraints (e.g. remove punctuation). Uppercase characters in the publisher string may be retained or folded to lowercase at the choice of the publisher, but the transformation should be applied consistently for all classes published by that publisher. For example, if the extension uses publisher ID Acme-Nadir their class names might begin vscode.AcmeNadir. or vscode.acmeNadir. or vscode.acmenadir.
- Third dot-piece should be derived from the "name" property of the extension, using the same transform guidance as above.
- Additional dot-pieces of the package name can be added at the choice of the extension author.
- Classnames should follow the convention proposed by @Robert Barbiaux in this article, i.e. be upper camel case (aka Pascal case).
FWIW, this is my favorite set of guidelines of the sort I've read so far. I particularly appreciate that you don't throw out postconditionals entirely (and agree on the appropriate uses you've described).
We can discuss a lot about this. However, the problem lies in how developers enforce these rules. When you have 1-2-3 developers with a strict rule of approving all the changes by one another developer, you probably will be able to maintain it. With a big development team and a legacy codebase, it will be mission impossible.
Unless, if we would be able to have linters set in with forced checks in CI, and it would fix some issues on the fly or enforce developer do it. As well as proper Formatter setup in the developer environment (nowadays that would be VSCode), with a bunch of rules defined for the project.
But we don't have any linters (ObjectScriptQuality is quite bulky and may still require some improvements), or good formatters (the one with InterSystems Language Server extension is quite primitive), and no way to build new tools, and especially inject to CI process.
That leads us to the point that our community requires open-source ObjectScript parser that can help developers to build all sorts of tools, such as linters, formatters, and all sorts of scanners of the code. It would even help with AI today, so LLM would better understand our code. Until then, all these discussions are mostly useless.
Great points, Dima. But I think any discussions make sense - this is how people communicate and exchange ideas, as you did in your post as well. We could request ObjectScriptQuality to add a profile that will enforce the rule and also, there is a linter in VSCOde ObjectScript - maybe it is possible to request the change in it too.
The idea of an open-source parser for ObjectScript - a good one, do we have it published on the ideas portal?
The idea is there for a few years already
Voted.
"all these discussions are mostly useless. " - we have implemented linting as part of server-side source control hooks. See https://openexchange.intersystems.com/package/isc-codetidy
It's based on closed code, which does not support AST, and requires running in IRIS.
We need full AST, and have been able to run without IRIS, just run in any of CI environments.
This is great, @Ben Spead! Could you please provide an example of how any developer can leverage the code guidelines @Robert Barbiaux suggests in this post?
I see that coding guidlines for ObjectScript linter should be a document (json? yaml?) you include in your code repository or reference any file in a github/gitlab and VSCode linter follows it on-the-go.
Or as some ObjectScript.Quality like ruleset, is it possible @Daniel Tamajon ?
Or as a codetidy feature by @Timothy Leavitt, but it's not easy to use.
There are some naming conventions in the documentation already. Here's one example. I think there used to be more. I know there was one for Zen pages that suggested naming methods based on what was client side or server side or a Zen method. I thought there was also one that explained how things were named in the system classes, but I can't find that one in the current documentation.
I have no strong opinion about whether they are (or were) right or wrong. I think we just need to be aware that some of these are still out there, and people may be using them.
Without someone providing an accepted linter you won't get everyone to agree on case etc...
My own preferences do not all match the above but if we had a defined ruleset that was easily managed and enforceable in the IDE then fine with me.
However, I think we should all be able to agree that moving away from shortened commands and names benefits code maintenance and readability plus aligns better with other languages, especially if we use classes, braces, and other "modern" objectscript styles. Yes I know that I am showing my age, but note I avoided mentioning the "M" word 😂