Recently I noticed that in a codebase I worked on, the canonical way to format money had changed from a global formatMoney
function to an extension method on money: money.format
. A special formatter, formatMoneyWithoutCurrency
, suffered an equal fate. Almost immediately, something felt off. The Money
class was up to that point purely functional, but this extension method started adding methods to the class' public interface that were rendering based.
There is nothing fundamentally wrong with having a format
method on a class, but it does expand a class' responsibilities beyond its original scope of being a functional type. In most languages, formatting is generally done by creating an instance of some sort of formatter, and giving the value you want to format as input. The formatter's responsibility is exactly to turn a value into a string, whereas the original class' responsibility is to just do whatever it actually does (being money in the example above).
The global formatMoney
method is in fact a helper method. If we got rid of the abstraction, we would get something similar to MoneyFormat.forLocale(currentLocale).format(money)
instead. However, for performance reasons we want to cache the creation of the formatter, and for consistency reasons, we want to make sure every instance of the formatting uses the same basic formatting configuration, hence the helper method.
The abstraction to a global method makes sense, though I would argue in hindsight that having the money formatter be a global getter moneyFormatForLocale
instead would make the formatting much more explicit. It is also in line with, say, Numberformat.percentage.format(num)
. One can always discuss what is the best way to do it, and there is no single right answer, but I hope we can all agree on one thing: there already is a workable solution without extension methods for formatting money that reveals intent (it is clear what it is trying to do with the money), is extensible (I could easily have a completely different formatter for my money if I want), and avoids unnecessary repetition of code (I only need to specify the format I want). So where did the extension method come in?
What are extension methods good for?
Let's take a step back and look at when it actually does make sense to use extension methods. Extension methods are probably most commonly known to be part of C#. Older versions of C# noteworthily does not have the ability to add behaviour to interfaces. This is still something that makes sense to do in some circumstances. LINQ for example is a query language that can be used on all IEnumerable
instances in C#, and is completely built with extension methods. This avoids situations such as on the equivalent of LINQ in Java - streams - where you need to specifically call a stream()
method on your collection to enter "query mode", and in the end turn it back into a collection to pass it further.
Extension methods can also be used to expand types (not just interfaces) for which the source isn't necessarily under your control, without having to create a subclass for that type. Subclasses allow you to add functionality too, but you don't get the functionality unless you wrap the parent class instances, which may be returned to you from the same library that defines it, and you can't inherit from structs.
Extension methods have allowed for libraries in C# that significantly increase code readability and developer velocity, such as for example the aforementioned System.Linq, and FluentAssertions. Their usefulness has made it so that other relatively modern languages such as Kotlin and Dart have also adopted them. Is it always necessarily a good thing to use extension methods though?
A final use case that extension methods can be used for, is to add domain-specific behaviour to a generic class. It is basically adding a bit of extra flavour to an existing helper class. For example, we may have something that helps us create notifications, a NotificationFactory
. This type is widely useful, but within a certain domain we maybe want to build a notification with a very specific style very often. Having a notificationFactory.createForDomain()
method, exposing only a subset of parameters, could be incredibly helpful. A solution without extension methods would be to use inheritance, or - even better - composition. An extension method takes away that need, as we don't really need to store additional state. That probably summarises what extension methods are: a substitute for inheritance or composition where no additional state is needed.
What are extension methods NOT good for?
While an extension method is not defined within the type that the extension method is defined on, it is important to remember that the extension method will still be part of its public interface to some extent. That is why one should still consider whether an extension method actually makes sense there. Extension methods shouldn't increase the responsibilities or alter the purpose of the type. If you need a type to behave differently, a new type is more appropriate.
Extension methods should also not be used for behaviour that is not unique defined for the type the method is on. To come back to my format
example: formatting a price is not a single, uniquely defined behaviour. Not only is it locale-dependent, it is also context-dependent. The fact that a formatWithoutCurrencyCode
also exists already exposes this problem. While using a formatter class using the type as input is more verbose, it is also more explicit, and leaves programmers open to implement other formatting methods if so required. Compare a formatting method to for example a reversed
extension method on a list. Reversing a list is well defined, and make sense as part of the public interface of a list.
Should we still use extension methods?
Extension methods are a useful part of the languages. They make it possible to quickly add new methods to a type's public interface. Because we don't actually open the file the type is in, we may be less primed to critically think about whether the type is really the right place to put a new method on. We should scrutinise the addition of an extension method as much as a normal method: does it fall within the responsibilities of the type, and is the method well defined?
Another class of extension methods that exists is the type that makes code read more like natural language. FluentAssertions allows you to write test assertions of the form actual.Should().Be(expected)
, which reads nicely, but one could argue that Should
is hardly part of the class's responsibility (nor perhaps a well-defined behaviour). Based on the arguments presented above, I believe Google's Truth framework in Java does it slightly better with syntax as assertThat(actual).equals(expected)
.
All in all, when to use or not use extension methods comes down to personal preference, much like any code pattern. That doesn't mean it is not important to blindly add new methods to a type because it's an extension method. At the very least, extension methods should not add ambiguous behaviour, such as formatting, to a type. As with everything: extension methods provide great power to the programmer, and with great power comes great responsibility.