Moving Methods out of Classes
In today’s article I’ll share a technique that can help you reason about your classes (and structs). The core idea is to move some methods out of the class into helper functions. Doing this can really simplify the class and simplify the functions so they’re much more easily understood by readers (including yourself!), more easily written, and more easily extended. Read on to learn more about this technique!
Today’s article is inspired by C.4 from the C++ Core Guidelines.
“Normal” classes are written as monoliths. All their data and all their functionality are put into the class. This implies that all the methods can read or write any of the fields. The fields become “global” within the class. Wouldn’t it be nice if there was a way to limit access to the fields to just some of the methods?
There is a really nice, elegant way to do this in C#. We just need to reorganize a little from the monolithic approach.
Let’s start out with a little example class. Actually, a little example struct but the principal applies equally to both. Here’s a Fraction
that has private numerator
and denominator
fields, a constructor, and properties to provide access to the numerator and denominator. The “invariant” of the type is that the denominator must never be zero, which would be an invalid fraction. So the constructor and the denominator property throw an exception if the caller ever tries to set a zero denominator.
Here’s how Fraction
looks so far:
public struct Fraction { private int numerator; private int denominator; public Fraction(int numerator, int denominator) { this.numerator = numerator; if (denominator == 0) { throw new ArgumentException("Denominator can't be zero", "denominator"); } this.denominator = denominator; } public int Numerator { get { return numerator; } set { numerator = value; } } public int Denominator { get { return denominator; } set { if (value == 0) { throw new ArgumentException("Denominator can't be zero", "value"); } denominator = value; } } }
And here’s a little script to make a fraction and print it:
public class TestScript : MonoBehaviour { void Start() { var frac = new Fraction(2, 4); PrintFraction(frac); } void PrintFraction(Fraction fraction) { Debug.Log(fraction.Numerator + " / " + fraction.Denominator); } }
So far the struct does only two things. First, it holds the numerator and denominator. Second, it ensures that the denominator is never zero. So far, so good. Now the natural progression of the Fraction
type is to add on lots of functionality, just like you’d get from Unity’s Vector3
. We’d like to have functions like Add
, Subtract
, Multiply
, Divide
, and Reduce
available to make using the Fraction
type more convenient.
The normal, monolithic approach is simple: add these as methods of the class or struct directly. If we did this, we’d have to look at all of these functions with suspicion. Are they violating the invariant of the type by setting the denominator to zero? In a simple type like this it’s easy to tell by visual inspection. In a more complex type there will be functions calling each other, the functions may use other types, they may call callbacks or dispatch events, or even be used by multiple threads at once. All of these factors often make it really hard to tell if the invariant is still being ensured. You have to spend your time analyzing methods.
This brings us to today’s technique. The alternative approach is to move these methods out of the class or struct and implement them as static functions of another class. As a first stab, here’s a function to multiply two fractions and return the result as a new fraction:
public static class FractionUtils { public static Fraction Multiply(Fraction fraction, Fraction other) { var numerator = fraction.Numerator * other.Numerator; var denominator = fraction.Denominator * other.Denominator; return new Fraction(numerator, denominator); } }
And here’s how you’d use it:
var frac = new Fraction(1, 2); var multiplied = FractionUtils.Multiply(frac, new Fraction(1, 2));
It’s definitely more awkward to use this style than a method that’s directly in the class, but that’s easily fixed by using C#’s extension methods feature:
public static class FractionExtensions { public static Fraction Multiply(this Fraction fraction, Fraction other) { var numerator = fraction.Numerator * other.Numerator; var denominator = fraction.Denominator * other.Denominator; return new Fraction(numerator, denominator); } }
Now you can use it just like it was a method of the class:
var frac = new Fraction(1, 2); var multiplied = frac.Multiply(new Fraction(1, 2));
The key difference between this extension method outside of the struct is that it doesn’t have access to the private variables. It only uses the public API like any other function in any other class. Because of this, we can be sure that it can’t violate the invariant of the class by setting the denominator to zero. By the very nature of being a function outside of the class, it’s no longer a suspect when we’re trying to figure out how the denominator got to be zero. We don’t even need to look at the function’s code!
At the same time, we’ve shrunk the amount of code that is responsible for ensuring the invariant to just a few simple functions: the constructor and two properties. If you ever ask yourself “can the denominator ever be zero?” then you don’t need to look at a bunch of functions like Multiply
because they’re simply unable to access the private denominator
variable.
In practice, this can be a radical simplification of both parts of the code: the type that maintains the invariant and the functions that add features to the type. Of course it has some downsides though. One of them relates to performance. If Multiply
was a method then it could directly set the denominator
field. As a function outside the struct it has to use the Denominator
property, which is a function call that does an if
check. Those are costs you could skip with a method, but also at the expense of making the function responsible for ensuring the invariant. Thanks to extension methods, you can move the function into the class any time you need to enhance the performance and keep using the same syntax.
Another disadvantage is that the C# language won’t let us put overloaded operators or the ToString
method in an extension function. Similarly, extension methods can’t be virtual
and therefore can’t be overridden by deriving classes. If you want to use any of these features, methods are the only way to go for these functions. It’s not an all-or-nothing proposition though. You’re free to keep most of the functionality outside of the class as extension functions but use methods for overloaded operators, ToString
, and virtual
functions.
That wraps up today’s discussion of this technique. Let me know what you think of the idea and if you use it in the comments!