Class vs Routine in ObjectScript. What Do You Use And Why?

Hi, Community!

Have a question for general discussion. 

In ObjectScript we have cls for classes and mac code, which both compile into int code. 

Is there any reason when you use mac instead of cls  for non-persistent classes?

For me the benefits for cls are:

1. Inheritance and other OOP features

2. Auto-documented code 

For mac one visible benefit is easier call in terminal:

do method^Utils(p1,p2)

vs

do ##class(Package.Utils).method(p1,p2)

What is your choice and why?

Comments

Another + for classes would be intellisense in studio. 

Although, I do like the public output variable list separation to input variable in mac files like so:

TEST (invar) [outvar] public {

}

This can be done also in the CLS.

TEST(invar) [outvar] public {

}

=>

ClassMethod TEST(invar) [PublicList=outvar] {

}

PublicList

Classes only.

Each method, that could be called from a terminal is documented with a sample call.

Classes and Methods forever!

#1) for documentation
#2) for all the possibilities and structural controls of  OO development.
 

#3)
.mac & .int is a left over from a previous millennium,
a (failing) attempt to mimic OO with the mindset of procedural methodology.

I'm personally disappointed that Atelier still supports mac.

It was a historical requirement. Accepted. For last millennium. Eventually still for some internals.
Definitely not for public use.

I really enjoy fluent interfaces made possible by using classes and objects.

DEV>write {}.%Set("a","1").%Set("b",2).%Set("c","thought it would be 3!").%ToJSON()

{"a":"1","b":2,"c":"thought it would be 3!"}

If I'm writing something that will  maybe run 2 or 3 times, and is less than 10 lines of code, I might use a .mac.  For anything more than that, I would use a class, as the time saved in setup is pretty negligible compared to the rest of the work done

You should not be able to do it if you want to pass only compiled objects.

I'm sorry.

Class only.

But, classes can not release OBJ only.

and subsequently you can export compiled classes and only import deployed code on another system. So you never get source code on a customer's system.

Hi, Fabian!

Could you share please the call to export the compiled classes without source code?

I think it really depends on your coding preferences. There are benefits to writing using Objects (CLS) and benefits to writing in (MAC) and which you use may simply depend on what your coding preferences are.

Do you like to code using objects, then CLS is the way to go. Are you a procedural programmer, then (MAC) is the way to go.

Using CLS, you program using objects with properties and methods. You gain the benefit of being able to access your object data via three different mechanisms, direct global access, object access, or sql. You implement methods and typically your methods are logically organized into the classes that they pertain to. You gain inheritance, XML support, JSON support, and so much more.

All that said, if you come from a background of procedural programming and don't have a need or desire to work in objects, there's nothing wrong with using MAC routines.

I often see user classes that extend either %RegisteredObject or %Persistent that implement only class methods with no properties at all. This produces larger-than-needed runtimes.

For classes that implement only classmethods (static) there is no reason to inherit an instantiable interface such as that provided by %RegisteredObject.

It isn't necessary but it is good practice to define such classes to be abstract.

Using a simple helper class such as the one used by the various $system.<class> classes implemented by ISC it is possible to provide help at the command prompt for all methods implemented in the class.

I tend to lean toward classes-only but I do have requirements that can only be met by using routines. I'm not a fan of the ##class() syntax and, outside of instantiation, there aren't very good alternatives to that syntax. 

Oh, thanks, Dan! That's interesting.

So, practically speaking: 

Class My.Utils {

ClassMethod Foo {

///

}

}

Works faster than:

Class My.Utils Extends %RegisteredObject {

ClassMethod Foo {

///

}

}

Right?

Not necessarily "faster" at execution time but the routine generated by compiling a class that unnecessarily extends %RegisteredObject or %Persistent will be much larger than the routine generated by a class that implements only static members and is abstract. That's less space consumed. The class descriptor/dispatch table will be smaller.

Thanks, Dan! 

That's helpful.

And from that class descriptor/dispatch table, can we say in general that call:

do foo^utils(p1) 

would be always faster than

do ##class(My.Utils).foo(p1) 

assuming that the code in foo() is same?

It seems logical that a class method call will be slightly slower than a direct routine function call but I'm not sure there is much difference. I've never conducted any benchmarks in a pure environment. It would be interesting for someone to test this.

There are extra commands executed in order to call the class method, however the impact is negligible and the ultimate code executed is really identical assuming the method and routine code is the same 

Yes, Not extending %RegisteredObject will produce less code however the code you are executing will be identical.

Still can be so:

Class dc.test Abstract ]
{

ClassMethod Test("#")
{
  "Test_",s
}

ClassMethod mac() [ ProcedureBlock = 0 ]
{
sub1(s=1)
  "sub1_",s
  q
sub2(s=2)
  "sub2_",s
  q
procPrivate(s=3) {
  "procPrivate_",s
}
procPublic(s=3) public {
  "procPublic_",s
}
}

}

Result:

USER>zTest^dc.test.1(1)
Test_1
USER>sub1^dc.test.1
sub1_1
USER>sub2^dc.test.1
sub2_2
USER>sub1^dc.test.1(10)
sub1_10
USER>sub2^dc.test.1(10)
sub2_10
USER>procPrivate^dc.test.1(10)
 
D procPrivate^dc.test.1(10)
^

USER>procPublic^dc.test.1(10)
procPublic_10
USER>

Yes, that is true today but dispatching directly to labels in a class runtime is very fragile.

True! Nice hack ;) Thank you, Vitaly!

But obviously, you cannot use this calls in a code, because dc.test.1 can be dc.test.2 etc...

USER>zload dc.test.1
 
USER>do sub1()
sub1_1
USER>do sub2()
sub2_2
USER>do sub2(10)
sub2_10
USER>do procPrivate(10)
 
DO procPrivate(10)
^
<NOLINE>^dc.test.1
USER>do procPublic(10)
procPublic_10
USER>do sub1
sub1_1

Another reason to opt for Objects over Routines is that Objects provide an automated documentation mechanism that Routines dont.  All elements of a class are documented in the class documentation and the developer can add their own text documentation as well

Also, Classes lend themselves much better to programmatic access and manipulation of their content (you can do this with routines too but it is harder to do so due to their unstructured nature).

For example, the server-side Source Control hooks we use can programmatically insert an RCS Keyword as a class parameter ("SrcVer") into any class which doesn't already have it defined.   This is extremely powerful because it allows source control to create an automatic 'watermark' in every class created for our internal applications which can be programmatically access or from terminal:

SSO>write ##class(AppS.WebClient).#SrcVer
$Id: //custom_ccrs/us/ISCX/SSO/BASE/cls/AppS/WebClient.xml#6 $

This would require much more plumbing to do automatically with a routine!

Routines are preferable if need to call routine from the other namespace.

Why? Can't class methods be called from another namespace? Or it is easier with routine?

Im pretty sure that class methods cant be called in another namespace without wrapping the method call in a call that actually switches namespaces, calls them method and then switches back.

do ["namespace"]obj.method 

isn't supported where

do ["namespace"]tag+offset^routine

is

Yes, that is true, however, that doesnt allow you to execute a class method from namespace A in namespace B.

When you :

do ["b"]tag^routine

Cache actually executes tag^routine in namespace b vs the namespace you are currently in.

Creating a package map for namespace a that maps a particular package from namespace b only makes that class available to namespace a.  When I execute methods within the mapped class those methods are executed in namespace a and not b.

I might be misunderstanding you, but I do not agree.

Class is in A and mapped to B.

ClassMethod t1() As %Status
{
w $NAMESPACE,!
q 1
}

A>d ##class(Test.PopNicki).t1()
A

A>zn "B"

B>d ##class(Test.PopNicki).t1()
B

B>
 

Kenneth, I don't think you are correct here. Apart from invalid syntax you gave, even with the correct syntax the routine is fetched from the specified namespace and executed in the current namespace. See the following example using a simple test routine (YJM) that I created in my USER namespace and then executed from my SAMPLES namespace:

USER>zl YJM zp
YJM      w !,"Runs in ",$namespace,! q
 

USER>d ^YJM

Runs in USER

USER>zn "SAMPLES"

SAMPLES>d ^YJM

D ^YJM
^
<NOROUTINE>^YJM *YJM
SAMPLES>d ^|"USER"|YJM

Runs in SAMPLES

SAMPLES>w $zv
Cache for Windows (x86-64) 2017.2.1 (Build 801U) Wed Dec 6 2017 09:07:51 EST
SAMPLES>d ["USER"]YJM^YJM

D ["USER"]YJM^YJM
^
<SYNTAX>^YJM
SAMPLES>

Interesting, I dont see anywhere in the docs where it would indicate that D ["SAMPLES"]YJM^YJM will work

The docs would indicate that you can do this:

DO ^["SAMPLES"]Y2K

However, that throws a SYNTX error too

I didn't expect D ["SAMPLES"]YJM^YJM to work, but that's the syntax you used in your earlier comment. Probably a typo.

I agree, it's interesting that the older-style square-bracket extended reference syntax doesn't work in this context. You have to use the newer-style vertical-bar syntax.

Okay, looked at the docs more and my eyes were playing tricks on me.

Looks like you cant call a routine at a tag in another namespace, but can call a routine at the top in another namespace.

The syntax is:

do ^|"namespace"|routine

so:

do ^|"SAMPLES"|YJM

I disagree with your assertion that you can't call at a tag in a way that you can call from the top:

USER>zp
YJM      w !,"Runs in ",$namespace,! q
         ;
SUB      ; A subroutine tag
         w !,"SUB^YJM runs in ",$namespace,!
         q
 
 
USER>zn "samples"
 
SAMPLES>d SUB^|"USER"|YJM
 
SUB^YJM runs in SAMPLES
 
SAMPLES>

I find it confusing that you talk about calling a routine in a namespace. As I see it you're fetching it from another namespace (i.e. the one the routine lives it), but you're running it in your current namespace.

You also need to be aware of what happens if the code you fetch from the other namespace makes its own calls to other code. Here's an illustration:

USER>zp
YJM      w !,"Runs in ",$namespace,! q
         ;
SUB      ; A subroutine tag
         w !,"SUB^YJM runs in ",$namespace,!
         q
         ;
Test1    ;
         w !,"About to call local line label YJM",!
         d YJM
         q
         ;
Test2    ;
         w !,"About to call a line label in a specific routine",!
         d SUB^YJM
         q
 
USER>d Test1^YJM
 
About to call local line label YJM
 
Runs in USER
 
USER>d Test2^YJM
 
About to call a line label in a specific routine
 
SUB^YJM runs in USER
 
USER>zn "SAMPLES"
 
SAMPLES>d Test1^|"USER"|YJM
 
About to call local line label YJM
 
Runs in SAMPLES
 
SAMPLES>d Test2^|"USER"|YJM
 
About to call a line label in a specific routine
 
 d SUB^YJM
 ^
<NOROUTINE>Test2+2^YJM *YJM
SAMPLES 2d0>
 
SAMPLES 2d0>q
 
SAMPLES>

if ever this should be of practical use
then I'd suggest to move such a "everybody's darling" routine to %SYS and name it %zYJM

Classes all the way.

I have yet to see a compelling argument to use routines the later versions of cache.

In practice, the majority of artefacts in our Caché instances are .int files.  We have used classes for mapping globals, .NET web development and writing stored procedures. It is nice to be able to call a classmethod from within a .NET application using a tool-generated proxy class. I detest code that jumps all over the place and Xecute statements within globals. Please stop coding this madness! Let's make the world a better place...

I confess some of the .NET software we have calls a classmethod which in turn calls a legacy .int routine with a Do statement and many subsequent Do statements. Not ideal but necessary if you want to avoid re-writing everything on a massive system. You can also use ClassExplorer with your classes and the "comment-style" self-documentation for classes is useful.  Much of the code we support uses the legacy line-based syntax and there is truly no replacement for well-structured, easy-to-read code. It just makes debugging less of a headache. I love line-spacing and curly braces - the simple things you take for granted in other programming languages. I also can't see anyone doing RESTful API development without classes.

Having human-readable, intuitively named class packages rather than hundreds of obscurely named .int routines and globals makes life so much easier. Having a class in a package in a BIO namespace called BIO.Request with CRUD methods reads much better than an interactive programme invoked by do ^REQ. I also like being able to export self-contained class packages rather than trying to work out what .int files and globals I need to export from memory.

I used both of routine and cls. But I did like for cls . We are appliying FHIR concept to class easily and  generated the structure of the data flow.