Introduction
While this article title may sound controversial, since there is clearly nothing wrong with using string in your code, below I’ll show the cases 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 the full code on
Github.
The Code
Recently, I was tasked to write code that 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 its 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 use OOD terminology to 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 the
single responsibility principle. This is the reason why we will 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, a 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 the Parse method in order to construct specific types from the general input.