Last time in this series, I began an exploration of how a method might be impacted by the scope of the variables that it uses. The idea is that it’s easier to comprehend a method that uses more narrowly scoped variables. If method A uses nothing but local variables, method B uses class level fields, and method C uses global state, time to comprehend will increase from A to B to C. Let’s introduce this hypothesis into the running experiment for a time to comprehend metric.
But before we can do that, let’s take a look at how those concepts are represented in NDepend. Let’s simplify the query we used last time and use that as a starting point. This is a query that simply looks at the fields used somewhere in a method.
1 2 3 4 5 6 7 8 |
// Method fields used JustMyCode.Methods.Select( m => new { m, m.FieldsUsed, } ) |
Recall the Board constructor we were looking at last time.
1 2 3 4 5 6 7 |
public Board(int boardSize = DefaultBoardSize) { VerifyBoardSizeOrThrow(boardSize); _boardSize = boardSize; _pieces = new Piece[boardSize, boardSize]; } |
The constructor uses two class-level fields: _boardSize and _pieces. If we run this query in the query editor, we see that there are two fields. But, here’s another cool feature of NDepend — you can actually drill into that field to see what the fields in question are. Here’s what that looks like.
That’s good progress. We can address the “class field” part of the equation when we start adding this consideration to our metric. But what about global state? Well, let’s see what happens to FieldsUsed if we add a global field. I added a little global variable repository to the code base and then amended the constructor to use it.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public class GlobalDumpingGround { public static int AGlobalVariable; } public class Board { public Board(int boardSize = DefaultBoardSize) { VerifyBoardSizeOrThrow(boardSize); _boardSize = GlobalDumpingGround.AGlobalVariable; _pieces = new Piece[boardSize, boardSize]; } |
Now when I run NDepend’s analysis, I see that there are 3 fields, and it lists the field from GlobalDumpingGround. This is good news, but not unexpected. After all, there’s nothing in the naming of “FieldsUsed” to suggest that it’s only talking about fields for that method’s class. So now we have a small task of figuring out how to distinguish local fields from global variables.
This might seem very simple, but there is a bit of subtlety to it. It isn’t just a matter of “fields from this class versus from others.” After all, I could have an instance field from some other class that’s visible to my method. You don’t see this a lot because standard practice is to have fields be private, but it is possible. This means that we need to put qualifiers on both types of field for our experimental purposes here. Let’s take a look at that.
1 2 3 4 5 6 7 8 9 |
// Method usage scope JustMyCode.Methods.Select( m => new { m, LocalFields = m.FieldsUsed.Where(f => f.ParentType == m.ParentType && !f.IsPubliclyVisible), GlobalFields = m.FieldsUsed.Where(f => f.IsPubliclyVisible && f.IsStatic) } ) |
For local fields, we’re going to look at fields that share a parent type with the method, meaning they belong to the class in question. We also want them not to be publicly visible, to capture the general usage practice of encapsulation. For global fields, we want them to be publicly visible and static, since this is essentially the definition of global scope.
When we apply this to the board constructor, the results are what we expect: 2 local fields and 1 global.
That’s perfectly fine and good, but what about properties? Working with a local or global property is probably no different when it comes to time to comprehend, and this query will fail to take properties into account. Let’s add those and see what things look like.
Here’s the revised code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public class GlobalDumpingGround { public static int AGlobalVariable; public static int AGlobalProperty { get; set; } } public class Board { public const int DefaultBoardSize = 8; private readonly int _boardSize; private readonly Piece[,] _pieces; public Board(int boardSize = DefaultBoardSize) { VerifyBoardSizeOrThrow(boardSize); GlobalDumpingGround.AGlobalProperty = GlobalDumpingGround.AGlobalVariable; _boardSize = GlobalDumpingGround.AGlobalProperty; _pieces = new Piece[Size, Size]; } |
The constructor now worries about a global variable, a global property, 2 local fields, and a local property. So let’s make an update to the query we’re running.
1 2 3 4 5 6 7 8 9 10 11 |
// Method usage scope JustMyCode.Methods.Select( m => new { m, LocalProperties = m.MethodsCalled.Where(m1 => (m1.IsPropertyGetter || m1.IsPropertySetter) && m1.ParentType == m.ParentType), GlobalProperties = m.MethodsCalled.Where(m1 => (m1.IsPropertyGetter || m1.IsPropertySetter) && m1.IsPubliclyVisible && m1.IsStatic), LocalFields = m.FieldsUsed.Where(f => f.ParentType == m.ParentType && !f.IsPubliclyVisible), GlobalFields = m.FieldsUsed.Where(f => f.IsPubliclyVisible && f.IsStatic) } ) |
And, finally, let’s see what NDepend tells us.
We still have two local fields and one global field. But we now have a local property being accessed here. That’s not a surprise, since we’re initializing the Pieces array with the local property Size. But why are there 2 global properties when the only one we’re dealing with is AGlobalProperty? Well, that’s a tricky bit, and it’s occurring because even though we reason about AGlobalProperty as a single variable, it’s actually two methods as far as the IL is concerned: a getter and a setter. And, we’re using both of them.
And that brings me to an explanation of the CQLinq here. I’m searching for methods, because that’s what properties are under the hood. Specifically, I’m searching for locals as getters and setters that share a parent type with the calling method. And for globals, I’m just looking for getter/setter methods that are publicly visible and static. I don’t care what the parent type is — a global is a global.
There are, of course, a lot of refinements and different ways we could spin these queries. When reasoning about a problematic global state, we might say that we don’t care about public statics that are only readable, because the mutability is what creates the problem. When applying this sort of querying to your code base, you may do just that.
But for the purposes of the experiment, what I’m interested in is mainly the effect of these scopes on readability. Now that you’ve seen a primer on the way the concepts can be quantified, stay tuned for another round of experimentation.
Comments:
Comments are closed.