Today, we’ll be talking about something controversial: static methods. I have yet to read anything that says static methods are good and useful, other than Effective Java recommending them in the use of static factory methods. There are some really interesting (and somewhat defective) arguments out there against them that rarely, if ever, even get explained. Notably, I’m providing a rebuttal to the article, Utility Classes Have Nothing to do With Functional Programming.
Today, we’re going to look at the good and bad of static methods; what they’re good for and what they’re not.
Static Methods are What Pure Functional Languages Use
As you probably know, functional programming has returned to our world as the “in” thing to do, and most OO languages have been adopting first-class functions (or some way to represent them, anyway) in order to be more of a hybrid between OO and FP.
In pure FP, there aren’t any classes; there are data types and functions. Those functions are not attached to any types, just namespaces. The same goes for static methods. They aren’t attached to types, they just have a namespace. Some might think that they are attached to types, since you have to put them in classes. But when it comes to static methods, the classes that are using them aren’t acting as classes; they only serve as namespaces for the static methods, a way to import them and differentiate them from other functions that might have the same name and arguments in a different namespace.
As long as both are able to do the same thing (which they practically can, when higher-order methods are available), they are the same. Things like currying only make functional programming more convenient are aren’t strictly necessary, in my opinion. So this doesn’t disqualify static methods from being the same thing.
There is one difference – an important one – between functions in pure functional programming and static methods: static methods can have side effects. For the sake of argument, we’ll assume we’re good programmers, though, and that we make our static methods pure. Or, at least mostly pure. There shouldn’t be any external side effects, at least, and these are the side effects I’ll refer to from now on. At the end, we’ll come back to this and look at the kind of difference it makes.
Now that we know that FP functions and static methods are essentially the same thing, let’s look at the arguments, shall we?
Imperative vs Declarative: In the linked article above, the author argues that FP functions are declarative while static methods are imperative. In truth, either can be either.
People tout that FP is declarative while OO is imperative. I’ve looked at this for a while and have come to the conclusion each simply makes one or the other the obvious way to do it. All the declarative building blocks of FP can be done in OO, and FP has the many of the same pieces available that make up imperative code. This is why Java’s Stream API is declarative; even if you don’t use lambdas or method references, you can use it declaratively with well-named functions and classes.
Even the other guy mentioned that you can be declarative in OO.
Testability: The complaint isn’t so much that static methods are hard to test (the claim is that FP is easy to test, so this better not be the case), but that things that use them are harder to test, since the calls are hard-coded and cannot be swapped out for others during testing. I have several rebuttals to this:
- Some parts of your functionality shouldn’t or don’t need to be swapped out, even for testing. If there are no side effects caused by the static method, there is almost no reason that it needs to be swapped out. If FP can handle this and still be called testable, so can static methods.
- You can swap out static methods. The trick is to take in a function as a parameter, where the static method can be supplied as an argument. “But then the static method gets hard-coded at the call site,” one might say. I say back, “Something has to get hard-coded somewhere.” Even with DI systems, we either tell it explicitly which specific class to use to fulfill a dependency, or it’s able to deduce it somehow; in either case, a specific class is used and therefore “hard-coded”. At some point, the dependency implementations must be chosen.
- If we can accept types that cannot be subclassed (String, for example) and use their methods, then there’s no fundamental difference between those methods and static methods, other than encapsulation. Sure, the primary object is passed as an argument instead of being what the method is called from, but that’s a semantic difference, not functional.Polymorphism doesn’t apply in those cases, so you can’t swap anything out other than the class’instance, which can also be done with static methods. No one says not to use those, though; in fact, preventing subclassing is often lauded as a virtue.
So, yeah, static methods are not fundamentally less testable.
Readability: By this, the original author talks about how huge utility classes get, collecting dozens and dozens of static methods. To that, I say that these utility classes are usually for classes that areextremely generic, and there’s a lot you should be able to do with them, which is what makes the utility classes big. Most of those utility methods should have been on the original class, making themreally big instead.
Efficiency: Next, he seems to claim that static methods are inherently eager and cannot be lazy. While this isn’t true, making them lazy largely just turns the static methods into static factory methods, since they’d have to create an object of a type that makes the operation lazy. FP languages have less of a problem with this because it’s the language that provides the laziness, so we can still write as if it’s eager, and it won’t be. Though, some forms of laziness can be used without language conventions or specialized objects. Simply accepting a value-generating function instead of a straight value as an argument that will only be used if/when needed is laziness.
Laziness isn’t always the ideal, though. In those instances when we’re definitely going to need the calculation, and right away, the overhead of creating the lazy operation wrapper supersedes the benefit of being able to put off the operation.
Now that I’ve said all that, it’s time for us to pull back a bit. I’m not saying to switch to all static methods; I don’t think being a purist in either direction is the way to go. Personally, I actually lean away from static methods as anything other than factory methods, as “replacements” for constructors. Also, they can be good for fluent interfaces, such as in matcher and testing libraries.
If you’re going to try and go mostly FP in a hybrid language, you may end up using more static methods, but there’s certainly nothing wrong with doing a normal method call. Go full on static should make us change our approach though. Generally, FP languages provide very powerful generic data structures and use them for just about everything, making new types only when it becomes less tedious to do so. This allows for reuse of functions more easily, keeping to total count down.
There’s an interesting inverse dynamic between functions using a family of types in FP compared to families of types with a shared interface of methods in OO. In true FP, it’s easier to add new functionality to the family of types, because you only need to write a single function to handle the different possibilities, but adding a new type to the family requires a change to all the already created functions. In OO, polymorphism allows us to add a type to a family without it affecting practically any old code, but adding a method to the family requires a change to every type in the family. In this sense, neither is inherently better than the other.
We probably shouldn’t overload our code bases with a ton of utility classes, as the article says (though ones for classes as generic as Strings, Dates, and Files probably aren’t as bad as people make them out to be), but neither should we ignore the fact that, sometimes, a static method serves a purpose better than objects and normal methods. Pragmatism should be used; do what’s best for the situation.
Static Method Guidelines
Whatever you do, don’t directly use static methods that are impure, that have side effects; any side effect calls should be swappable for testing. It’s slightly okay to create impure static methods, though it’s a dangerous idea, since it’s less obvious how to factor those method dependencies out for swappability.
When making our own static methods, keep them short, like anything else; this helps to keep the testability of the methods high.
If we’re working in a language with static methods, we should probably lean toward instances and methods and polymorphism, as that is where the language likely shines.