ClassMethods Are Not Your Friends
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 Article.MyClass Extends %RegisteredObject
{
Method MyMethod() As %String
{
return "Hello" // This is the "mystery code" that could be from a network access, or whatever.
}
}
Class Article.MyOtherClass extends %RegisteredObject {
Property myClassAccessObject As Article.MyClass;
Method %OnNew(accessObject As Article.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 Article.MockMyClass extends %RegisteredObject {
Property calls As %Integer;
Property returnValue As %String;
Method %OnNew(returnValue As %String = "") As %Status {
Set ..calls = 0
Set ..returnValue = returnValue
Return $$$OK
}
Method MyMethod() As %String {
Set ..calls = ..calls + 1
Return ..returnValue
}
}
Class Article.MyOtherClassTest extends %UnitTest.TestCase {
Method TestSimple() {
d ..ExerciseMethod("simple value")
}
Method TestComplex() {
d ..ExerciseMethod("more complicated value")
}
Method TestNative() {
Set sut = ##class(Article.MyOtherClass).%New()
s actual = sut.MyOtherMethod()
d $$$AssertEquals(actual, "Hello")
}
Method ExerciseMethod(expectedValue As %String)
{
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
Set actual = sut.MyOtherMethod()
d $$$AssertEquals(mock.calls, 1)
d $$$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.
Comments
Nice article to blow my mind on a Friday!
Should the OnNew() methods be %OnNew()?
Shouldn't this:
Property myClassAccessObject = "";
Be this?
Property myClassAccessObject as MyClass;
Why, yes! It should be!
Sorry; I'm not sure why I seem to get that mixed up. Please don't tell the me-of-10-minutes-ago that we making fun of someone for such a newb mistake as checking in code they didn't exercise properly.
I can't seem to cut-and-paste to my browser for some reason, so I'll try cut-and-pasting the updated article code into my browser and make another edit if needed.
The code from the original version of the article has been updated. The Spectre of Dumb Typos lies in wait for us all. Specifically:
- The constructor method is "%OnNew", not "OnNew".
- There was a typo in one of the method calls.
- There was some design confusion on whether "ExerciseMethod" should return a value or not, so apparently I compromised by doing both.
- Since I was editing it anyway, I adopted the best-practice of putting the classes in a namespace.
- I added a test, "TestNative", that isn't strictly a "unit test" and is more of an "integration test" or "system test" and made a call directly to the default implementation of Article.MyClass to provide an example of the behavior outside of the test harness.
I respectfully disagree with your overall premise that Classmethods are somehow bad and not to be used. ClassMethods, more commonly referred to as Static methods, are a part of every object-oriented language I have ever worked with. Let me expand on why this is, provide a perspective on why the approach suggested introduces issues, and finally propose an alternative.
The Indispensable Role of ClassMethods
It is vital to understand why ClassMethods are an absolute necessity in a well-designed object model. In InterSystems ObjectScript, a ClassMethod is not an architectural loophole; it is the natural, logical manager of a class template.
- Clear Cognitive Scoping: It maintains a clean boundary by separating what an object does (instance methods) from how the system manages that type of object (ClassMethods).
- Lifecycle and Persistence Control: You cannot ask an object to instantiate or retrieve itself. Operations like
%New(),%OpenId(), or%DeleteId()must exist at the class level because the specific instance does not yet exist or is locked away in a database. - Deterministic Object-to-Object Operations: Operations that involve multiple instances—such as cloning an object or comparing two distinct objects—belong to the class, not a single instance. A
ClassMethodacts as a privileged guardian, safely inspecting the internal properties of multiple instances at once without breaking encapsulation. - Utility Methods: While not directly an object issue, Classes and ClassMethods provide a centralized place to manage and access general purpose utility functions. Classes of utilities can be placed in there own packages for organization. A perfect example of this is the %SYSTEM.Status class that we have all likely dealt with.
The Downside of Banning ClassMethods: Constructor Injection Flaws
First, when testing the first approach should always be to test the way the code will be used IMHO. In most cases your Test environment will provide an implementation of all the resources that are to be referenced. This allows code to be fully tested exactly in the manner it will be executed in Production. Now we all know that this is not always possible. This is where Mocking the results is the appropriate response. That should be the fall back rather than the preferred design..
When developers rigidly ban ClassMethods in favor of constructor injection to achieve total mock isolation, they introduce what is known as Test-Induced Design Damage. This forces you to permanently disfigure your production architecture solely to satisfy a testing tool.
If you abandon a straightforward ClassMethod call and force dependency injection via %OnNew, you create immediate architectural problems across your codebase.
Impact on the Calling Class (MyOtherClass)
- Architectural Pollution: The class is forced to maintain internal instance properties solely to hold references to stateless helper objects, cluttering the production model.
- Constructor Bloat: The
%OnNewconstructor becomes completely overloaded with setup logic, requiring a safety valve of boilerplate code to handle both production defaults and incoming mock objects. The example in the post is for a single mocked method. Now apply this to a real application where you many have dozens of these methods to deal with. - Indirection Over Clarity: Readability, direct domain operations are replaced by an indirect layer of property-hopping, making the execution flow harder for a developer to follow at a glance.
Impact on the Dependency Class (MyClass)
- Forced Statefulness: Naturally stateless, deterministic utility logic is forced to become instantiable, creating unnecessary memory allocation and object overhead in the production environment.
- Loss of Domain Authority: The class can no longer act as a centralized, top-down orchestrator for its own lifecycle, outsourcing its domain tasks to external objects.
- File Proliferation: Developers are forced to write, track, and maintain an entirely separate mock class file (
MockMyClass.cls) just to simulate a single piece of stateless behavior during a test.
The Pragmatic Alternative: Environment/Control Variables
Instead of corrupting your application's architecture to accommodate tests, a more reasonable approach treats the ClassMethod as a black box. If a specific class method truly interacts with an external, unmockable resource (like a live web service with no test environment of its own), the method itself can handle its testing context via an environment or process-level control variable.
This can be implemented in many ways including, but not limited to:
- A Global (process-private) entry
- A proerty in a control file
- An OS level environment variables
objectscript implementation for MyClass.MyMethod
ClassMethod MyMethod() As %String
{
// 1. Check if the process is currently in a testing context using a utility method
If ##class(App.Environment).IsTesting() {
// create a mock results as appropriate. This can be done directly here or
// via a separate mock results management api.
Set MockResult = ….
Return MockResult
}
// 2. Real production logic lives safely underneath
# do the real work production a Result
Return Result
}This pattern keeps the production architecture clean, preserves the logical purpose of the ClassMethod MyMethod(), and isolates the impact of mocking to that ClassMethod. The ability to mock for Unit Testing is managed with and by the method to be mocked. Control over use of Mocking is maintained external to the code being tested enabling easy setup of the Unit Testing code. Finally, the black-box nature of the MyClass.MyMethod() is restored.
I agree with Rich and see that it would be obtuse/challenging to create SQL Stored Procedures if we didnt have ClassMethods. Does this mean we simply cannot have SQL Stored Procedures?
I'm not quite understanding the nature of the question.
If you are asking if banning Classmethods would block stored procedures then the answer is technically yes for the most part. This would block any method based stored procedure as these have to be Classmethods.
However there is still Query type stored procedures which are a different kind of class element. That is a bit of a fudge as the method type ends up much like a ClassMethod anyway.
Also there are Custom Query type query methods where you define Execute, Fetch, and Close methods each of which has to be a ClassMethod.
Of course this is all a bit procedureal as there is nothing beyond policy that would actually block the usage of ClassMethods in IRIS itself.
As someone once said "It's important to know the rules so that you can break them properly."
sorry.. I was supporting your arguement not suggesting your position was incorrect. The original position is that class methods are not your friend and maybe shouldn't be used. I was suggesting without classmethods we wouldnt have SQLProcs that are functions.
When thinking about IRIS we have both Objects and SQL. I tend to lean towards the SQL world. Not because I dont use Object but I'm mostly focused on storage, performance of retrieve/reports/obtaining a list of records. I think of IRIS in terms of performance as
- directy global access/hand written objectscript code.. likely fastest but relatively hardest to write as you have to have intimate knowledge of the structure of globals, what indices are available, what are the statistics between globals. It's not hard, its just more intense to write. At the same time for better or worse this code could be stuck in time. For example, if you weren't aware of $SORTBEGIN/$SORTEND but did not use it.. welp it will never be utilized. On the otherhand if back in the day you had a SQL statement which didnt even have this function but then there is an upgrade and these functions exist then the SQL code is regenrated and you get the benefit.
- SQL ..more readable, easier and faster to write.. optimized to just what you specify. If a table has 200 columns(and yes plenty do) if I SELECT A,D,E FROM Table the generated code is optimized to only get those columns.. but not nearly as fast as #1..but maybe with PARALLEL query support it could in fact be much faster. In a SELECT statement LOCKS are not by default implemented.
- Objects.. even more readible, maybe faster to write but maybe slowest(and when I say slowest this is a relative positions..I'm not saying Objects are slow). When you %OpenId an object it has to load into memory all 200 properties(taking that table/object from #2) so that you can later write oref.property199. It simply doesn't "know" what you are going to use later so all of the properies have to be "swizzled" into memory. %OpenId actually has 3 parameters in the formal parameter list including the Concurency option. Many folks do not bother to specify the Concurrency option. Do you always needs a lock? Not sure but I dont think many even consider this.
I may be miststating some things above but after 35 years working on InterSystems technologies this is how I think.
Strangley enough I often times see people fall into a trap where they think every method in a business host class(Service,Process,Operation) has to be an instance method but when you study the code there is nothing in the method that depends on an instance being defined. When this strategy is done it's basically reducing the amount of reusability so I actually look more towards ClassMethods unless they truly are instance methods.
can you provide the link to the article mentioned in footnote 1?