11

I noticed that nearly every time I see programmers using static classes in object oriented languages such as C#, they are doing it wrong. The major problems are obviously the global state and the difficulty to swap implementations at runtime or with mocks/stubs during tests.

By curiosity, I've looked at a few of my projects selecting ones which are well tested and when I made an effort to actually think about architecture and design. The two usages of static classes I've found are:

  • The utility class—something I would rather avoid if I was writing this code today,

  • The application cache: using static class for that is plainly wrong. What if I want to replace it by MemoryCache or Redis?

Looking at .NET Framework, I don't see any example of a valid usage of static classes either. For example:

  • File static class makes it really painful to switch to alternatives. For example, what if one needs to switch to Isolated Storage or store files directly in memory or needs a provider which can support NTFS transactions or at least be able to deal with paths longer than 259 characters? Having a common interface and multiple implementations appears as easy as using File directly, with the benefit of not having to rewrite most of the code base when requirements change.

  • Console static class makes testing overly complicated. How should I ensure within a unit test that a method outputs a correct data to console? Should I modify the method to send the output to a writeable Stream which is injected at run time? Seems that a non-static console class would be as simple as a static one, just without all the drawbacks of a static class, once again.

  • Math static class is not a good candidate either; what if I need to switch to arbitrary precision arithmetic? Or to some newer, faster implementation? Or to a stub which will tell, during a unit test, that a given method of a Math class was indeed called during a test?

On Programmer.SE, I've read:

Aside extension methods, what are the valid uses of static classes, i.e. cases where Dependency Injection or singletons are either impossible or will result in a lower quality design and harder extensibility and maintenance?

Arseni Mourzenko
  • 134,780
  • 31
  • 343
  • 513
  • [in Groovy](http://programmers.stackexchange.com/a/215915/31260)? "If the Groovy class you're testing makes calls a static method on another Groovy class, then you could use the ExpandoMetaClass which allows you to dynamically add methods, constructors, properties and static methods..." – gnat Dec 19 '14 at 19:23
  • 7
    ["Sometimes, the elegant implementation is just a function. Not a method. Not a class. Not a framework. Just a function."](https://twitter.com/id_aa_carmack/status/53512300451201024) - John Carmack – Corbin March Dec 19 '14 at 19:29
  • This doesn't answer your question (which is why it's a comment) and it'll likely be an unpopular opinion, but I think you're looking for a level of modularity that Java/C# simply can't provide (without jumping through more hoops than a circus animal). You've already identified that a static method is a hard-coded dependency, and more generally any class is a hard-coded dependency. – Doval Dec 19 '14 at 19:32
  • 1
    So you could put everything behind an interface and treat them as modules, but then you'll need adapters to convert from one interface to another when you bring in foreign code or when you want to glue different functions together into one interface or select a subset of an interface's functions. And you lose the ability to have useful binary methods (at least without cheating with `instanceof`) because given two instances of `IFoo` there's no guarantee they're backed by the same class. – Doval Dec 19 '14 at 19:34
  • It's really just easier to just accept that some level of rigidity is inevitable in those languages. – Doval Dec 19 '14 at 19:36
  • The answer to your question essentially boils down to accepting the fact that a lot of times YAGNI. All your what its: YAGNI. – whatsisname Dec 19 '14 at 19:39
  • @Doval: Generally, if an object doesn't need something, you shouldn't be giving it to it. If you need a specific type, you don't use an interface. If you need to transform objects between interfaces, you are, in the words of Attwood, Doing It Wrong. – Magus Dec 19 '14 at 19:41
  • P/Invoke uses static methods -- I'd put my stuff into a static class and if I needed actual `Object`s I'd make those use the static class under the hood. – paul Dec 19 '14 at 19:53
  • @Magus Just because you need some class or function to work with one specific `String` implementation (as opposed to every string-like thing implementing `IString` or whatever), it doesn't mean you should have to hard-code the name of that String implementation up front. There's languages with more flexible import systems where you can just say "I some `class` `T` with these methods" and plug in some particular choice of `T` later. But Java/C# can't do that, so instead you end up declaring an `interface` and drag in a dependency injection framework to try to do the same but at runtime. – Doval Dec 19 '14 at 20:04
  • @whatsisname - sorry, I didn't see your comment before making my answer. – Telastyn Dec 19 '14 at 20:04
  • 2
    @Magus That or you throw your arms up, say `YAGNI`, and accept that if you ever need a different kind of string, you'll just get your IDE to change every instance of `com.lang.lib.String` to `com.someones.elses.String` in the entire code base. – Doval Dec 19 '14 at 20:06
  • @Telastyn: no worries, great minds think alike :) – whatsisname Dec 20 '14 at 01:26
  • see also: [Significant amount of the time, I can't think of a reason to have an object instead of a static class. Do objects have more benefits than I think?](http://programmers.stackexchange.com/q/242940/31260) – gnat Dec 25 '14 at 08:20
  • You mention the "global state" problem. The global state is only a real problem if it's globally mutable, because this way it can create common coupling between multiple modules. Otherwise it's just a dependency like any other class/interface with the same visibility. And not everyone follows the unit testing approach where you mock out every little implementation detail. Sometimes testing higher-level modules is the best way to go, which hides the properly used static classes/methods. – astreltsov Sep 04 '15 at 15:35

4 Answers4

22

what if; what if; what if?

YAGNI

Seriously. If someone wants to use different implementations of File or Math or Console then they can go through the pain of abstracting that away. Have you seen/used Java?!? The Java standard libraries are a fine example of what happens when you abstract things for the sake of abstraction. Do I really need to go through 3 steps to build my Calendar object?

All that said, I'm not going to sit here and defend static classes strongly. Static state is solidly evil (even if I still use it occasionally to pool/cache things). Static functions can be evil due to concurrency and testability issues.

But pure static functions are not that terrible. There are elementary things that don't make sense to abstract out since there's no sane alternative for the operation. There are implementations of a strategy that can be static because they're already abstracted via the delegate/functor. And sometimes these functions don't have an instance class to be associated with, so static classes are fine to help organize them.

Also, extension methods are a practical use, even if they're not philosophically static classes.

Telastyn
  • 108,850
  • 29
  • 239
  • 365
  • 1
    Part of the question seems to have been the complaint that while there are people who say that there are valid uses for static classes, they don't back that statement up with examples. This answer could be improved if it did so. – Magus Dec 19 '14 at 20:29
  • @Magus - the 3 examples given in the question are perfectly fine uses, though the OP argues otherwise. – Telastyn Dec 19 '14 at 20:41
  • I don't think it's a bad answer, just that there might be room for improvement. If the examples in the OP are the only ones, that is fine. It would make sense to say so in the answer, for the sake of future searches. Either way, it has my upvote. – Magus Dec 19 '14 at 21:24
  • 1
    I wish frameworks provided somewhat better support for thread-static state, rather than viewing it as an afterthought. Something like `Console` really shouldn't be part of the system-wide state, but nor should it be part of an object that gets passed around constantly. Instead, it would seem like it would make more sense to say that each thread has a "current console" property which can be easily saved and restored if it's necessary to have a routine which writes to `Console` use something else instead. – supercat Dec 20 '14 at 01:16
  • @Telastyn Could you clarify what you mean by "extension methods[...] are not philosophically static classes"? The compiler turns `myList.Select(x => x + 1)` into `Enumerable.Select(myList, x => x + 1)` - extension methods _literally are_ static methods. Syntactic sugar is an important part of language design, certainly, but I don't believe that it changes anything quantitative about a design. – Benjamin Hodgson Sep 04 '15 at 00:45
  • 1
    @BenjaminHodgson - design is rarely quantitative. Anyways, extension methods could be implemented differently (think JavaScript prototype manipulation) and users would not be the wiser. That they are static functions is an implementation detail, not an intrinsic part of their nature. – Telastyn Sep 04 '15 at 00:59
18

In a word Simplicity.

When you decouple too much, you get the hello world from hell.

void main(String[] args) {
    TextOutputFactory outputFactory = new TextOutputFactory();
    OutputStream stream = outputFactory.CreateStdOutputStream();

    Encoding encoding = new EncodingFactory.CreateUtf8Encoding();
    stream.Encoding = encoding;

    SystemConstant constant = new SystemConstant();
    stream.LineEnding = constant.PlatformLineEnding;

    stream.FlushOnEachLine = true;

    String greeting = new FixedSizeInMemoryString(); // We may have to switch to a file based string later!"
    greeting.SetContent("Hello world");
    greeting.Initialize();

    stream.SendContent(greeting);
    stream.EndCurrentLine();

    ProcessCompletion completion = new ProcessCompletion();
    completion.Status = constant.SuccessStatus;
    completion.ExitProgram();
}

But hey, at least there is no XML configuration, which is a nice touch.

static class allow to optimize for the quick and common case. Most the point you mentionned can be very easily solved by introducing your own abstraction over them, or using stuff like Console.Out.

Laurent Bourgault-Roy
  • 5,606
  • 4
  • 20
  • 25
  • 5
    This reminds me of the GNU Hello World that accounts for various localization concerns. – Telastyn Dec 19 '14 at 22:22
  • 1
    *-1* For **lack of** XML configuration. *+2* For putting it in a nutshell. – s1lv3r Dec 31 '14 at 00:35
  • 1
    *Hello World* should really be generalized to allow for supraglobal scenarios. I mean, I might be greeting some *other* world, after all. Eventually. We would need to determine some common form of communication first... Primes in binary, sent by AM carrier wave signal... but what frequency? I know, the fundamental mode of silicon atoms! –  Feb 04 '16 at 18:55
10

The case for static methods: This:

var c = Math.Max(a, Int32.Parse(b));

is much simpler and clearer and more readable than this:

IMathLibrary math = new DefaultMathLibrary();
IIntegerParser integerParser = new DefaultIntegerParser();
var c = math.Max(a, integerParser.Parse(b));

Now add dependency injection to hide the explicit instantiations, and see the one-liner grow into tens or hundreds of lines - all to support the unlikely scenario that you invent your own Max-implementation which is significantly faster than the one in the framework and you need to be able to transparently switch between the alternative Max-implementations.

API design is a trade-off between simplicity and flexibility. You can always introduce additional layers of indirection (ad infinitum), but you have to weigh the convenience of the common case against the benefit of additional layers.

For example, you can always write your own instance wrapper around the file system API if you need to be able to transparently switch between storage providers. But without the simpler static methods, everybody would be forced to create an instance every time they had to do a simple thing like saving a file. It would be really a bad design trade-off to optimize for an extremely rare edge case, and making everything more convoluted for the common case. Same with Console - you can easily create your own wrapper if you need to abstract or mock the console, but quite often you use the console for quick solutions or debugging purposes, so you just want the simplest possible way to output some text.

Furthermore, while "a common interface with multiple implementations" is cool, there is not necessarily a "right" abstraction which unifies all the storage providers you want. You might want to switch between files and isolated storage, but someone else might want to switch between files, database and cookies for storage. The common interface would look different, and it is impossible to predict all possible abstractions users could want. It is much more flexible to provide static methods as the "primitives", and then let users build their own abstractions and wrappers.

Your Math example is even more dubious. If you want to change to arbitrary precision math you would presumably need new numeric types. This would be a fundamental change of you whole program, and having to change Math.Max() to ArbitraryPrecisionMath.Max() is certainly the least of you worries.

That said, I agree that stateful static classes are in most cases evil.

JacquesB
  • 57,310
  • 21
  • 127
  • 176
0

In general the only places where I would use static classes are those where the class hosts pure functions which don't cross system boundaries. I realize the latter is redundant as anything that crosses a boundary is likely by intent not pure. I come to this conclusion both from a distrust of global state and from an ease of testing perspective. Even where there is a reasonable expectation that there will be a single implementation for a class which crosses a system boundary (network, file system, I/O device), I choose to use instances rather than static classes because the ability to supply a mock/fake implementation in unit tests is critical in developing fast unit tests with no coupling to actual devices. In cases where the method is a pure function with no coupling to an external system/device I think a static class may be appropriate but only if it doesn't hinder testability. I find the uses cases to be relatively rare outside existing framework classes. In addition, there may be cases where you have no choice - for example, extension methods in C#.

tvanfosson
  • 2,249
  • 19
  • 18