Leaning on the compiler with intentional compilation errors in refactoring
Recently I had to make a major and subtle change in our codebase. We have a highly used object that does something (allows us to read structured data from a buffer). That object is created at least once per document load, and it is very widely used. It also has a very short lifetime, and it is passed around a lot between various methods. The problem we have is that we don’t want to pay the GC cycles for this object, since that take away from real performance.
So we want to make it a structure, but at the same time, we don’t want to pass it to methods, because it isn’t a small struct. The solution was to make sure that it is a struct that is always passed by reference. But I did mention that it is widely used, right? Just changing it to struct is going to have a high cost of copying it, and we want to make sure that we don’t switch one cost with another.
The problem is that there is no difference in the code between passing a class argument to a function and passing a struct argument. So the idea is that we’ll lean on the compiler. We’ll change the object name to by ObjStruct, and then start going over each usage scenario, fixing it in turn. At the same time, because of things like “var” automatic variables and the lack of ref returns in the current version of C#, we make sure that we change a method like this:
TableValueReader GetCurrentReaderFor(IIterator it);
Into:
void GetCurrentReaderFor(IIterator it, out TableValueReader result);
And that means that the callers of this method will break as well, and so on and so forth until we reach all usage locations. That isn’t a particularly fun activity, but it is pretty straightforward to do, and it allows you to make large scale changes easily and safely.
Note that this requires two very important features:
- Reasonable compilation timeframes – you probably wouldn’t use this approach in C++ if your build time was measured in multiple minutes
- An actual compiler – I don’t know how you do large scale refactoring in dynamic languages. Tests can help, but the feedback cycle is much faster and more straightforward when you deliberately let the compiler know what you want it to do.
Comments
"I don’t know how you do large scale refactoring in dynamic languages."
I don't know either, in dynamic langues if it works you don't touch it any more. But I have a theory: There's a new JS library every day, simply because no one dares to modify existing ones :)
I would probably write a test for this, with the added benefit of making it future proof.
inspiration can be found at http://www.strathweb.com/2015/09/using-roslyn-and-unit-tests-to-enforce-coding-guidelines-and-more/
Seems like the ideal situation to create a Roslyn analyzer for - because it'll also stop someone from writing a new TableValueReader GetCurrentReaderFor(IIterator it); method tomorrow, after the refactoring is complete.
Damien, That has another issue, because this is going to slow down compilation speeds, and over time you'll have a LOT of such analyzers and slower and slower system.
Well, you wouldn't necessarily write an analyzer for TableValueReader. You'd write an analyzer for large structs (probably as a first cut, those that you choose to mark with an attribute)
+1 Jonas, Something similar to a sample from NServiceBus convention test asserting struct sizes https://github.com/Particular/NServiceBus/blob/develop/src/NServiceBus.Core.Tests/StructConventionsTests.cs
That is a good approach to do large scale refactorings. Break the method signature by changing one of the parameters or the method name and lets the compiler figure out who is affected.
Comment preview