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);
	}
}
The code does it’s job. However, it left me unsatisfied because the knowledge about the permission is scattered all over the place i.e. magic number 9 or 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);
	}
}
What happened is that class 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;
}
This makes 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);
}
This violates single responsibility principle. This is the reason why we’ll split this code into two methods.
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;
}
Let’s have a look at how they are called
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.