Refactoring string into the specific type
Introduction
While the article title may sound controversial as there is clearly nothing wrong with using string
in your code below I’ll show the case where string
type doesn’t clearly communicate all the necessary properties of a domain in question. Then I’ll show how this can be handled. You can watch full code on Github.
The code
Recently I was tasked to write the code which converts Linux permissions to their octal representation. Nothing too fancy, just a static class that does the job. Here’s the code
internal class PermissionInfo
{
public int Value { get; set; }
public char Symbol { get; set; }
}
public static class SymbolicUtils
{
private const int BlockCount = 3;
private readonly static Dictionary<int, PermissionInfo> Permissions = new Dictionary<int, PermissionInfo>() {
{0, new PermissionInfo {
Symbol = 'r',
Value = 4
} },
{1, new PermissionInfo {
Symbol = 'w',
Value = 2
}},
{2, new PermissionInfo {
Symbol = 'x',
Value = 1
}} };
public static int SymbolicToOctal(string input)
{
if (input.Length != 9)
{
throw new ArgumentException("input should be a string 3 blocks of 3 characters each");
}
var res = 0;
for (var i = 0; i < BlockCount; i++)
{
res += ConvertBlockToOctal(input, i);
}
return res;
}
private static int ConvertBlockToOctal(string input, int blockNumber)
{
var res = 0;
foreach (var (index, permission) in Permissions)
{
var actualValue = input[blockNumber * BlockCount + index];
if (actualValue == permission.Symbol)
{
res += permission.Value;
}
}
return res * (int)Math.Pow(10, BlockCount - blockNumber - 1);
}
}
ConvertBlockToOctal
method which relies on permissions being in a certain well-defined order. This left me wondering whether the string is the best way to represent Linux permission.
The question is rather rhetorical since Linux permission possesses additional constraints which string as a general datatype doesn’t. So the idea is to impose those restrictions on my input datatype or using OOD terminology encapsulate them.
Testing
Automated tests are the necessary prerequisite for each refactoring. For this task, my test-suite isn’t really exhaustive, but it is enough to cover the case provided in the spec.
[Fact]
public void HandlesCorrectInput()
{
SymbolicUtils.SymbolicToOctal("rwxr-x-w-").Should().Be(752);
}
Extracting SymbolicPermission class
internal class SymbolicPermission
{
private struct PermissionInfo
{
public int Value { get; set; }
public char Symbol { get; set; }
}
private const int BlockCount = 3;
private const int BlockLength = 3;
private const int MissingPermissionSymbol = '-';
private readonly static Dictionary<int, PermissionInfo> Permissions = new Dictionary<int, PermissionInfo>() {
{0, new PermissionInfo {
Symbol = 'r',
Value = 4
} },
{1, new PermissionInfo {
Symbol = 'w',
Value = 2
}},
{2, new PermissionInfo {
Symbol = 'x',
Value = 1
}} };
private string _value;
private SymbolicPermission(string value)
{
_value = value;
}
public static SymbolicPermission Parse(string input)
{
if (input.Length != BlockCount * BlockLength)
{
throw new ArgumentException("input should be a string 3 blocks of 3 characters each");
}
for (var i = 0; i < input.Length; i++)
{
TestCharForValidity(input, i);
}
return new SymbolicPermission(input);
}
public int GetOctalRepresentation()
{
var res = 0;
for (var i = 0; i < BlockCount; i++)
{
res += ConvertBlockToOctal(_value, i);
}
return res;
}
private static void TestCharForValidity(string input, int position)
{
var index = position % BlockLength;
var expectedPermission = Permissions[index];
var symbolToTest = input[position];
if (symbolToTest != expectedPermission.Symbol && symbolToTest != MissingPermissionSymbol)
{
throw new ArgumentException($"invalid input in position {position}");
}
}
private static int ConvertBlockToOctal(string input, int blockNumber)
{
var res = 0;
foreach (var (index, permission) in Permissions)
{
var actualValue = input[blockNumber * BlockCount + index];
if (actualValue == permission.Symbol)
{
res += permission.Value;
}
}
return res * (int)Math.Pow(10, BlockCount - blockNumber - 1);
}
}
SymbolicPermission
now holds all the knowledge about Linux permission structure. The heart of this code is the Parse
method that checks whether the string input matches all the necessary requirements. Another point to highlight is the use of a private constructor.
private SymbolicPermission(string value)
{
_value = value;
}
Parse
the single entry point thus disabling the possibility to create permission that doesn’t match all the required constraints.
Now the usage of the old static method looks as simple as
public static int SymbolicToOctal(string input)
{
var permission = SymbolicPermission.Parse(input);
return permission.GetOctalRepresentation();
}
Bonus: refactoring SRP violation
At this point, ConvertBlockToOctal
method not only converts a block of permissions to its octal representation but also extracts it from the provided input.
private static int ConvertBlockToOctal(string input, int blockNumber)
{
var res = 0;
foreach (var (index, permission) in Permissions)
{
var actualValue = input[blockNumber * BlockCount + index];
if (actualValue == permission.Symbol)
{
res += permission.Value;
}
}
return res * (int)Math.Pow(10, BlockCount - blockNumber - 1);
}
private string GetBlock(int blockNumber)
{
return _value.Substring(blockNumber * BlockLength, BlockLength);
}
private int ConvertBlockToOctal(string block)
{
var res = 0;
foreach (var (index, permission) in Permissions)
{
var actualValue = block[index];
if (actualValue == permission.Symbol)
{
res += permission.Value;
}
}
return res;
}
public int GetOctalRepresentation()
{
var res = 0;
for (var i = 0; i < BlockCount; i++)
{
var block = GetBlock(i);
res += ConvertBlockToOctal(block) * (int)Math.Pow(10, BlockCount - i - 1);
}
return res;
}
Conclusion
Often string is a jack-of-all-trades type that does not represent all the necessary constraints that actual type in question possesses. As one of the possible solutions in this article, I propose crafting dedicated types and using Parse
method in order to construct specific type from the general input.