Recently I was reviewing a piece of code that was performing some computation on file sizes. The author of that code followed good programming practices and created a separate type for representing file sizes:
public struct FileSize {
public static FileSize FromBytes(ulong bytes)
=> new FileSize(bytes);
// Other factory methods...
private ulong _bytes;
public ulong TotalBytes
=> _bytes;
public FileSize(ulong bytes) {
_bytes = bytes;
}
// Other stuff, equatable, comparable, blah...
public override string ToString()
=> $"{_bytes:##,#B}";
public static FileSize operator+(FileSize left, FileSize right)
=> new FileSize(checked(left._bytes + right._bytes));
// Other operators...
}
Yet when it came to computing a total size of set of files I saw code like this:
var totalSize = fileSizes.Aggregate((acc, curr) => acc + curr);
What is wrong with this code?
It forces readers to concentrate on irrelevant details like how
to sum a list of FileSize
s. As a programmer, reading a lot
of code I would prefer to see something like:
var totalSize = fileSizes.Sum();
Which is shorter, easier to read and allows me to concentrate on the actual business problem that I try to solve.
As an another example, imagine what would happen if people started writing:
var listOfNumbers = Enumerable.Range(0, 10)
.Aggregate(new List<int>(), (list, el) => {
list.Add(el);
return list;
});
instead of:
var listOfNumbers = Enumerable.Range(0, 10)
.ToList();
I hope that you agree with me that it would not be niceā¦
The general rule that is violated by both these examples
is called Single level of abstraction principle, you can read
more about it
here.
In short it states that, all statements of a method should belong
to the same level of abstraction.
In other words we should not mix low and high level operations
in a single method. In our example Aggregate
and details how
to use it are low level, computing a total size of set of files is
on the other hand a high level one.
Fortunately for us, we may quickly add appropriate Sum
method
to our program:
public static class EnumerableOfFileSize {
public static FileSize Sum(this IEnumerable<FileSize> sizes)
=> sizes.Aggregate(FileSize.FromBytes(0),
(total, curr) => total + curr);
/* Or in more imperative style:
public static FileSize Sum(this IEnumerable<FileSize> sizes) {
ulong total = 0;
foreach (var size in sizes) {
total = checked(total + size.TotalBytes);
}
return new FileSize(total);
}
*/
}
After this change we achieved code that is easy to read and also hides irrelevant details. Yay!