There seems to be a generous use of ClassMethods in ObjectScript code generally. I hope my own experiences aren't representative, but I bet they are. Forgive me for giving away the ending of this article, but in short: don't use them. Unless you can make a pretty convincing case that you have to, just never use them.1
What is a ClassMethod? In an ObjectScript class, you can define methods in two different ways: in a Method, you must instantiate an instance of the class to call the method, and in a ClassMethod, you can call the method without instantiating the class. Of course, in a ClassMethod, you don't have access to any properties of the object (because there's no object), but you can access globals (they are global, after all) and Parameters (which are class constants).
It seems that the default development practice is to prefer ClassMethods to Methods if it doesn't reference class properties. After all, if there is no dependency on the state of the class, why not reflect the lack of dependency by declaring it a ClassMethod? That sounds like "functional programming" and that's really fashionable now, right?
The answer is: because the problem with "state" in functional programming is global state, and by declaring your method a ClassMethod, you have forced your function's definition into your user's global state. Take the following example:
Class MyClass extends %RegisteredObject {
ClassMethod MyMethod As %String {
...
}
}
Now, consider how this method would be used:
Class MyOtherClass extends %RegisteredObject {
ClassMethod MyOtherMethod() {
...
Return ##class(MyClass).MyMethod()
}
}
In this snippet, the phrase "##class(MyClass).MyMethod" is, for all intents and purposes, a global variable, and now MyOtherClass does have dependencies on the global state. All of our concerns about "functional programming" are now thwarted.
So, what? What's the difference? Consider the possibility that MyMethod's logic accesses a network resource to get its value. You've now made it much more difficult to test MyOtherClass.MyOtherMethod, because there's no way to stop it from accessing the network resource when you run your %UnitTest.TestCase, which you definitely wrote so that you aren't in danger of introducing code with a bug in it, right? But, if you wrote MyClass with a Method instead, it's easy:
Class MyOtherClass extends %RegisteredObject {
Property myClassAccessObject = "";
Method OnNew(accessObject As MyClass = "") As %Status {
if accessObject '= "" {
Set ..myClassAccessObject = accessObject
} else {
Set ..myClassAccessObject = ##class(MyClass).%New()
}
Return $$$OK
}
Method MyOtherMethod() {
Set value = ..myClassAccessObject.MyMethod()
Return value
}
}
Class MockMyClass extends %RegisteredObject {
Property calls As %Integer;
Property returnValue As %String;
Method OnNew(returnValue As %String = "") As %Status {
Set ..calls = 0
Set ..returnValue = returnValue
}
Method MyMethod() As %String {
Set ..calls = ..calls + 1
Return ..returnValue
}
}
Class MyOtherClassTest extends %UnitTest.TestCase {
Method TestSimple() {
Return ..ExerciseMethod("simple value")
}
Method TestComplex() {
Return ..ExerciesMethod("more complicated value")
}
Method ExerciseMethod(expectedValue As %String) As %Status
Set mock = ##class(MockMyClass).%New(expectedValue)
Set sut = ##class(MyOtherClass).%New(mock) // Because there's no point in fixing it in MyClass and not MyOtherClass
S actual = sut.MyOtherMethod()
$$$AssertEquals(mock.calls, 1)
$$$AssertEquals(actual, expectedValue)
}
}
Now, it's easy to test multiple different values coming from the network resource in your system without having to make changes to your production data every time you make a change to MyOtherClass and run your tests.
I hear you asking, "but what if MyClass doesn't access a network resource? This seems like a lot of bother when I know what MyClass does, and it doesn't do that." The point is that with those 5 simple letters, C-l-a-s-s, you've guaranteed that MyClass will never access network data, for the duration of your software's existence, or take a long time to run, or required complex setup to get it to return a value that you want it to return so that you can test MyOtherClass with different return values from MyClass. You haven't just violated good functional programming practices, you've pretended to know the future by putting limits on your future self, and now you've gotten in trouble with the Alethi church.2
Seriously, it looks like a pain to do all of that plumbing code of instantiating the default value and implementing an OnNew method, but think about your code for a minute. I'm guessing it doesn't take a lot of looking to see a lot of code that's only there to work around the fact that you aren't doing this. Or your day is full of activity messing around with downstream systems to try and test your code with the right values. Or your day is full of pointless runaround because your code is buggy because you think you can make "one small fix" without testing because it's such a pain to exercise it. The process of development takes more brain space and concentration, and it's exhausting to get it to work.
So when should you use ClassMethods? Don't! Seriously! More seriously, the rule of thumb is that Methods should be the default and ClassMethods should only be used when you are using a Singleton pattern, because that's essentially what you've established by defining a ClassMethod: a Singleton with no state. Is this an example of "Speculative Generality", the "code smell" where you're making things more complicated by anticipating more than will ever happen? No! It's the opposite; by making it a ClassMethod, you have made assumptions about what the code will never do in ways that your users will have to code around and compensate for. If you can't justify it with the Singleton pattern or some other really good reason, just don't use ClassMethods. Use Methods.
1. This is a well-written article talking about the same subject. I don't know anything about the author; I'm linking because it agrees with me. It was written in 2022, but it also references a book by the great Robert Martin that dates to 2008, and I learned this as a "best-practice" as soon as I started working professionally, and all I'll say is I started college in a year starting with "19".
2. This is a joke from the Stormlight Archive books by Brandon Sanderson. I'm not apologizing, and I'm not taking it out.