If static classes are bad, what should I do instead?
Stack overflow is generally of the opinion that static classes/methods are bad. I understand the benefits around maintainability, flexibility, and testing.
But then, what to do instead? If I have a bunch of helper methods inside an instantiable class, when should I create it / how many of them should I create? I could for example stick it into a static variable, either in each of the individual classes that use it, or some general class they can access. I suspect that still breaks OOP principles. But then what? Instantiate the helper class every time one of its methods get used? What if that is really often, and happens across different classes?
I'm doing games programming if that makes a difference - I doubt this specific case is going to break performance, but games tend to be much more entangled across classes/layers, and it feels really inefficient to instantiate classes every time you want to call a commonly-used, class-independent method, so I'd like some help understanding the best approach.
Posted May 17, 2023, 11:09 AM UTC
Static state is bad.
Static method that is pure (output is dependent on input only) is ok.
That is my current principle, but also what the most upvoted answers in Stackoverflow argue against.
Stack overflow likes to disagree with everything and half the people commenting are giving their opinion which they formed at college 30 years ago.
I mean... Just like this sub.
Just like any public congregation of programmers.
How does the saying go "ask 10 programmers their opinion, you will get 11 different answers".
only 11, we must be getting better =)
"ask two programmers their opinion, you will get three different answers".
Got it.
[deleted]
They argue against static methods with side effects. All those arguments except "parameter creep" are related to side effects. (edit: also it's trully easier to use some oop patterns around non-static methods, like decorators, but in practice you either know that you will use them or not).
Your static methods should not write/read files, access DB, make network requests, etc.
yeah, static methods with side effects is bad in my opinion. That's probably why this topic would be hard to answer: too generic.
This really helps me to frame my thinking on this. I do think it's also about testing, as you can only create test stubs if you have an interface to work with, but I guess there are ways to get around this, like decorators as you say.
A pure method won’t need a test stub. Just test the method directly
[deleted]
Here's a tough thing about programming.
Sometimes there is an opinion of the crowd that is wrong. Often it's wrong because there is nuance to the answer that is not being discussed. One downside of StackOverflow that quickly became apparent is it rewards SHORT answers, not DETAILED answers. Answering quickly makes it more likely you get the coveted "accepted answer", which puts your answer at the top and means more upvotes. There are some other weird things about SO that lead to this but I digress.
The main thing is to make sure you understand why you are making a static method and when it's not a great idea. It's not just about static state, there are also maintainability and testability concerns. I've seen people make static methods for things that touch "volatile" resources like the file system or network resources. If you do that, you won't be able to make a class using that method use some kind of stubbed or mocked method when you try to test it. Static methods can't participate in polymorphism or IoC so once you use one it's set in stone.
Sometimes we have a method like
Math.Sin(). It doesn't have anything "volatile". For any possible input, I have ways to verify if its output is correct. Because of that it's hard to imagine when I'd use a fake or mocked version instead of the real thing. This is a case where the concerns above are addressed and a static method is appropriate.Then there are methods like
File.ReadAllText(). That breaks the rule. I don't think Microsoft made a poor decision making this a static method, but because file I/O is something I want to fake or mock in my unit tests it means I usually have to make someIFileSystemabstraction (or use a package with one) so I'm able to use instance methods that CALL it and can be faked or mocked. Not so in my smaller scripts and prototypes, but I don't hold those to the same standards as my professional code. I still think Microsoft was right, but I do wish a filesystem abstraction was not a third-party thing.So it's all case-by-case. You have to think about whether a method should be static, and you have to ask yourself if the downsides will cause problems. For a lot of methods, they won't. If you've thought about it and feel like it's OK, then do it. Be honest with yourself later if it does cause problems and switch it to instance if so.
Even when you're completely right about it, there are still some people who will criticize the decision. I personally almost never make methods static. I think the loss of the capability to use IoC is too heavy a price to pay. But I also acknowledge that's an extreme position. So if someone has otherwise good code and my only problem is their use of static methods, I keep it to myself until they ask me a question I can legitimately say that static methods can't answer.
Put another way, most times we say to never do something, we know there is some small % case where it's perfectly fine and maybe even advantageous. "Never do this" is a silly shortcut we take to the idea that it takes a lot of knowledge and intuition newbies and novices do not have to be right about when to do it. So the experts say "never do it" to reflect the idea that getting something wrong has heavy costs that may not be apparent until very late.
That's not a license to break every rule. It's a reminder to think HARD about everything, even simple decisions, when writing large-scale code or code you want to maintain for a long time. Sometimes that cognitive burden's so great we decide to abandon one of our tools so we can focus on a smaller set of patterns and practices and use familiarity to help us manage complexity.
I think they made a bad decision.
It'll be better to have something like
FileSystem.Current.ReadAllText(...), whereFileSystem.CurrentreturnsIFileSystemabstraction that can be mocked. They kind of did that with WinRT filesystem (interfaces and all that).The same goes for clock. Just now in .NET 8 they introduce clock abstraction, and asp.net core has at least 2 of them. This means that they realized and admitted the mistake.
Instead of
DateTime.UtcNowit would make sense to haveSystemClock.Default.UtcNow.That's not a bad opinion.
I think on a 3-point scale with "bad", "OK", and "good" what they did is "OK". More often than not I find there's some value in having an abstraction more domain-specific than just a wrapper around their methods. So I'm usually not abstracting "the file system" but "a repository-like object" and that means the concrete object can freely use the static methods.
But it wouldn't hurt if they had a more full-fledged abstraction around their filesystem access and I always do wish they'd make a new one.
Abstractions aren't free; they cause mental load. Adding a seemingly pointless abstraction that has no plausible use cause other than being replaced by a fake implementation for testing purposes doesn't seem hugely convincing.
In particularly, it helps testability only to a very limited extent - after all, if you want to refactor the code to allow replacing the text read by ReadAllText, you can trivially do so - if it's your code. If you can't alter the code; sometimes a more complex integration test with real file-system access is worth it (esp. since file systems are notoriously complex and have real-world tricky interactions your test might miss). Sometimes you want a "real" filesystem that is itself a fake meant for testing purposes (i.e. a RAMdisk, for instance?).
In general, I don't have great experiences with APIs that try to enable replacing hyper-complex dependencies in an attempt to allow testing. However, APIs that allow you capture the effects of that dependency and then depend on some simpler intermediate representation are great. And that's probably what you should be doing here, too: the intermediate representation here simply being
string.However, there's clearly nuance here - I'm sure you're right there are examples where being able to replace the file system for testing purposes would be useful, I'm just not so sure it's a net win. If the aim really is testability, then perhaps a more useful fundamental concept would be some built-in support in the core language itself to (potentially inefficiently) truly redefine actually static things in some context; i.e. have your statics and still have mocking.
In any case, whether the file system is the best example or not, there are certainly quite a few examples of APIs with poorly testable and poorly extensible designs. At least a few apis that take "mode" style enum parameters for instance surely must have had some way to replace those with a proper interface - and for some of those, this wouldn't even have made the syntax from the consumers perspective any different whatsoever; they can still have their well-known static implementations, without preventing alternatives.
And then theres all those APIs that are needlessly huge or messily complex. In theory you could make your own implementation or provide some mock, but in practice you'd need to subclass some monstrosity with dozens of methods, poorly hidden mutable state, many overloads, etc. I'd much rather have a clean, small API that's then helpfully extended with static utility methods that cannot tweak the internals and would equally apply to any replacement, than a huge interface / non-sealed base type with tons of hard-to-fully-understand let along really control corner cases. If you want to improve testability, that is where the BCL really fell down.
FileInfohas 55 public methods (counting overloads and property accessors).Streamhas 54 - that's just crazy; and fully 38 are non-sealed.Newer APIs do seem to be better in this regard; for instance the
PipeWriterhas "just" 15 virtual methods.I wish most things were more like, say
IEnumerable- a relatively small abstraction, with most of the complexity provided by transparent utilities around that abstraction. That would help me a lot more than worrying about a few global statics that I can easily replace with a parameter in the very odd case I care to.Great answer. I definitely have been liking how composition with shared instances of classes has a lot of the benefits of static classes, without sacrificing testability.
It's also worth noting that somethings have to be static, like extension methods, and they often match the litmus for when a static helper method is okay.
I think these are the crucial points for me. I just need to think it through, and it's more than just "no state = static good", or "static always bad", but in particular, whether the method is a good candidate to be set in stone, and won't need ever need variants, affect the state of something non-internal to it, or need some type of state adding.
I guess they’d hate linq methods then 🤷🏼♂️
LINQ methods are extension methods, while static, they only depend on their input, so they are effectively pure functions without side effects.
Yes I understand how they work, it was a tongue in cheek jab at the dislike of statics in general
The answers on this post seem better explained and more detailed than the majority on that StackOverflow page. And in fact, it's mostly the top-voted answer that comes out somewhat strongly against static classes. The subsequent ones are less opposed.
StackOverflow is just programmers upvoting answers that sound good, but the quality/detail/insight of which may not actually be that good.
It’s largely about testability, and when it’s not, it’s about testability as a proxy for code that’s maintainable. A static method that is a pure function that takes input, does something deterministic, and produces output (and of course has no implicit state) is a fine static method. But a whole bunch of interdependent static methods that can be referenced outside the static class? No please. That’s where it’s useful to have classes that implement interfaces that describe specific behaviors and hide the details from the reader and from consumers of the code.
Sometimes there’s no “good” place to put a helper because the existence of it is the problem in the first place. The other risk is that a few static methods here and there turns into a norm when Junior engineers get in there and follow the pattern that they know the best.
Yup!
Math.Sin = good static implementation.
DateTime.Now = bad static implementation.
Exactly this.