Why and when to use static classes and methods
A friend asked me a question, a while ago, stating:
Hey Jan, One stupid question around which I have thought a lot and often get stuck while deciding. When to make a function static/non-static specifically the helpers or utility ones.
I read 2 arguments
- All the functions that don’t need to use a state of the object, (don’t update any variable value of the object ) should be static.
- All the functions that should/can be test independently and have some logic. Not only transformation or mapping. Should be non-static.
These 2 arguments are against each other and are often confusing, and mostly in the context of our repo. We often use multiple resources and their clients to implement a logic. Now these mostly don’t update any state of an object, mostly the value of the attributes of the class are read, never updated. And it does make sense to test them independently
One example Reading from the storage account, filter data based on some logic and then updating that in, let’s say, a Function App.
Now, how do you decide?
Let me start by: There’s no such thing as a stupid question in engineering! These are the type of questions everyone is wondering about, but only few dare to ask. Iris Classon has done an entire blog-series on these ‘not so stupid questions’.
The TL;DR; of my answer is
I hardly ever use static classes or methods, only if they do a simple transformation or calculation.
As you all might know by now, I like making bold statements and writing long-format texts afterwards, nuancing my point to a more complete answer.
The first part of my story
I’m very much for the usage of static
classes and/or methods when they are used for relative simple tasks. Like the often mentioned Math
class, similar examples are often used in tutorials too.
When looking through the codebase I’m currently working on, we also have a couple of examples that can/should be static
from my perspective, like:
- The
MetricsCalculator
to calculate the metrics we get from some internal data. - The
CopilotUtilities
with it’s multiple helper functions for the copilot template for factory operations on Azure AI - The
QueryParser
class with helper functions to parse data.
All these classes are doing very straightforward computation and create new objects as an output, not relying on anything else as what the parameters supply. Very much like the first argument in the above quote. However, I do have some issues with these three examples that I’ll cover later.
The second argument in the above quote is interesting.
If you let that one sink in and gloss over our codebase, or any other for that matter, we can indeed make everything in our codebase static
as most classes just pass around data and don’t hold or modify state. Should we do this? My answer is a hard NO!
Why is this you ask?
One of the reasons is that I’m kind of traumatized on this topic.
In one of the first projects where I was hired to be the solution- and software architect, the former engineers had decided to make everything static. The reason for this was both argument 1 & 2, but mainly (and I kid you not):
“Marking it static makes it so easy to invoke all the methods from everywhere we like.”
Yes, I’ve cried myself to sleep for the first weeks on this project.
On a bit more serious note, I do think static
code has it’s place on an OOP-world. However, it should be added very careful and with enough thought. The same applies to the Singleton-pattern. This pattern is often considered as an anti-pattern, but I do think it has value in certain scenarios.
Structure your code
There’s a post on Stack Overflow going over several reasons why using static
classes and methods might be a bad idea: https://stackoverflow.com/a/241372/352640. It also links to a post by Steve Yegge on the singleton pattern, which has a lot of touch points to the topic. The Stack Overflow post has some great reasons inside it, especially for production systems and need to be maintained for years to come.
Suddenly we need to change the functionality slightly. Most of the functionality is the same, but we have to change a couple of parts nonetheless. Had it not been a static method, we could make a derivate class and change the method contents as needed. As it’s a static method, we can’t. Sure, if we just need to add functionality either before or after the old method, we can create a new class and call the old one inside of it - but that’s just gross.
Indeed! While a class might not need polymorphism at this point in time, it might in the future. The use of common OOP-constructs is often necessary not directly from the start, but the need grows over time.
Interface woes and Testing in the post. I understand why this is called out, but don’t really see this as an issue. Static code is easy to test, and if you need to mock it for some reason that’s a code-smell in the overall softare design in my opinion. For example, if Math.Add(x, y)
needs to be mocked or replaced via some strategy pattern there’s probably something wrong with the rest of the code. Same for the samples I provided in our codebase. If the MetricsCalculator
-methods needs to be mocked or replaced in our unit-test code, we’re doing something wrong in our testing strategy. A (unit) test should validate the logic of a unit, not exact lines of code (but that’s a different discussion/post) on making your tests anti-fragile.
The Foster blobs topic is nice:
As static methods are usually used as utility methods and utility methods usually will have different purposes, we’ll quickly end up with a large class filled up with non-coherent functionality - ideally, each class should have a single purpose within the system. I’d much rather have a five times the classes as long as their purposes are well defined.
Take for example the CopilotUtilities
. Based on the name, can you tell what this class it’s purpose is? What type of methods will it contain? I don’t. And based on the methods added to this class, it looks like no one in our team knows. This class is the proof of why having static utility classes is a bad practice. Same applies for (regular) classes with -er
in their name, like ...Manager
, ...Orchestrator
, ...Controller
, and classes called ...Service
. It’s impossible to tell what they are doing.
The other main topics of the SO post are also nice to read.
But I’d also like to mention the built-in .NET dependency injection and the service lifetimes we have to our disposal. With this, creating objects is low-effort and when the service lifetime is set up proper you will hardly notice the performance impact. It’s a micro-optimization to use static code compared to using objects, and you probably don’t need that nanosecond optimization in the domain we’re at.