Click here to Skip to main content
15,879,348 members
Articles / Programming Languages / C#
Tip/Trick

Refactoring String into the Specific Type

Rate me:
Please Sign up or sign in to vote.
5.00/5 (3 votes)
10 May 2020CPOL2 min read 5.2K   5   1
An example when string is too broad type to capture all domain requirements and how to handle it
In this post, you will see cases where string type doesn’t clearly communicate all the necessary properties of a domain in question, and how this can be handled.

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 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 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:

C#
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 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.

C#
[Fact]
public void HandlesCorrectInput()
{
    SymbolicUtils.SymbolicToOctal("rwxr-x-w-").Should().Be(752);
}

Extracting SymbolicPermission Class

C#
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.

C#
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:

C#
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.

C#
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.

C#
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:

C#
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.

History

  • 10th May, 2020: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Team Leader
Ukraine Ukraine
Team leader with 8 years of experience in the industry. Applying interest to a various range of topics such as .NET, Go, Typescript and software architecture.

Comments and Discussions

 
Suggestionstring is *never* the appropriate type, except for text Pin
Stefan_Lang6-Jul-20 2:25
Stefan_Lang6-Jul-20 2:25 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.