解析设置文件的设计模式

时间:2014-05-30 04:42:36

标签: c# parsing design-patterns

我一直在努力寻找一种解析设置文件的优雅解决方案。

以下是一个例子:

L09D21=Type:OPT Z:0000 F:---A-Z--- S:+0 AVF:OFF Desc:"AHU-QCH 07.05EX PROBE" Out:,
G195=Out:LED0799,LED0814,Flags:L-N Desc:"EAF-QCH-B1-01" Invert:00 STO:35 SP:0 FStart: FStop: 
SysEv01=Type:FANLATCH Out:LED1165

每一行可以有不同的映射,并且文件中可以有多个相同的行类型。 (这些设置来自我们需要配置的硬件设备)

我们当前的代码由多个/嵌套的switch语句组成,它们解码文件/行的每个部分。

我能看到的设计模式是否解决了类似的问题?

我的感觉是,目前我还没有看到某种多态解决方案

3 个答案:

答案 0 :(得分:2)

让我们看看最简单的一行:

SysEv01=Type:FANLATCH Out:LED1165

从中我们可以看到我们有一个设置名称,然后是一堆属性。设置名称使用=分隔,属性由空格分隔。最后,我们还可以看到属性名称/值由冒号分隔。

public class Setting
{
   public string Name { get; set; }
   public IDictionary<string, string> Properties{ get; }
}

让我们看看最复杂的一行来验证这一点:

G195=Out:LED0799,LED0814,Flags:L-N Desc:"EAF-QCH-B1-01" Invert:00 STO:35 SP:0 FStart: FStop: 

似乎适用。有趣的是,值可以省略,因此我们必须在解析时考虑到这一点。另一件事是属性值可以用引号("EAF-QCH-B1-01")包装。

所以让我们编写一个简单的解析器并测试它。最简单的方法是解析单行以从中获取不同的部分。让我们首先获取设置名称和所有内容的字符串:

public class Setting
{
    public Setting(string name)
    {
        if (name == null) throw new ArgumentNullException("name");
        Name = name;
    }

    public string Name { get; private set; }
}

public class SettingsParser
{
    public Setting ExtractLine(string line)
    {
        var pos = line.IndexOfAny(new[] {'='});
        var setting = new Setting(line.Substring(0, pos));
        return setting;
    }
}

[TestClass]
public class ParserTests
{
    [TestMethod]
    public void Should_be_able_to_extract_name_from_a_line()
    {
        var line = "G195=Out:LED0799,LED0814,Flags:L-N Desc:\"EAF-QCH-B1-01\" Invert:00 STO:35 SP:0 FStart: FStop: ";

        var sut = new SettingsParser();
        var actual = sut.ExtractLine(line);

        Assert.AreEqual("G195", actual.Name);
    }
}

我们对该代码存在一个小问题,那就是该行格式错误。让我们确保我们得到一个等号,并且它是在冒号之前找到的。

public Setting ExtractLine(string line)
{
    var pos = line.IndexOfAny(new[] {'=', ':'});
    if (pos == -1 || line[pos] == ':')
        throw new FormatException("Expected an equals sign and that it's positioned before the first colon");

    var setting = new Setting(line.Substring(0, pos));

    return setting;
}

现在让我们继续提取参数。为了采用最简单的方法,我们只需将字符串拆分为空格,然后遍历每个条目并将其拆分为冒号。

代码现在是:

public class Setting
{
    public Setting(string name)
    {
        if (name == null) throw new ArgumentNullException("name");
        Name = name;
    }

    public string Name { get; private set; }
    public IDictionary<string,string> Parameters { get; set; }
}

public class SettingsParser
{
    public Setting ExtractLine(string line)
    {
        var pos = line.IndexOfAny(new[] {'=', ':'});
        if (pos == -1 || line[pos] == ':')
            throw new FormatException("Expected an equals sign and that it's positioned before the first colon");

        var setting = new Setting(line.Substring(0, pos));
        setting.Parameters= ExtractParameters(line.Substring(pos + 1));

        return setting;
    }

    private IDictionary<string, string> ExtractParameters(string paramString)
    {
        var keyValues = paramString.Split(' ');
        var items = new Dictionary<string, string>();
        foreach (var keyValue in keyValues)
        {
            var pos = keyValue.IndexOf(':');
            if (pos == -1)
                throw new FormatException("Expected a colon for property " + keyValue);

            items.Add(keyValue.Substring(0, pos), keyValue.Substring(pos + 1));
        }

        return items;
    }
}

对此的测试:

[TestMethod]
public void Should_be_able_to_extract_a_single_parameter()
{
    var line = "G195=Out:LED0799";

    var sut = new SettingsParser();
    var actual = sut.ExtractLine(line);

    Assert.AreEqual("LED0799", actual.Parameters["Out"]);
}

[TestMethod]
public void should_be_able_to_parse_multiple_properties()
{
    var line = "G195=Out:LED0799 Invert:00";

    var sut = new SettingsParser();
    var actual = sut.ExtractLine(line);

    Assert.AreEqual("00", actual.Parameters["Invert"]);
}

快进,你就得到了这个解决方案。代码使用一个简单的循环和string.IndexOf,因为它必须考虑以下场景:

  • 没有值的属性
  • 引用的属性值
  • 单一财产
  • 多个属性

代码:

public class Setting
{
    public Setting(string name)
    {
        if (name == null) throw new ArgumentNullException("name");
        Name = name;
    }

    public string Name { get; private set; }
    public IDictionary<string,string> Parameters { get; set; }
}

public class SettingsParser
{
    public Setting ExtractLine(string line)
    {
        var pos = line.IndexOfAny(new[] {'=', ':'});
        if (pos == -1 || line[pos] == ':')
            throw new FormatException("Expected an equals sign and that it's positioned before the first colon");

        var setting = new Setting(line.Substring(0, pos));
        setting.Parameters= ExtractParameters(line.Substring(pos + 1));

        return setting;
    }

    private IDictionary<string, string> ExtractParameters(string paramString)
    {
        var oldPos = 0;
        var items = new Dictionary<string, string>();
        while (true)
        {
            var pos = paramString.IndexOf(':', oldPos);
            if (pos == -1)
                break;  // no more properties
            var name = paramString.Substring(oldPos, pos - oldPos);


            oldPos = pos +1; //set that value starts after name and colon
            if (oldPos >= paramString.Length)
            {
                items.Add(name, paramString.Substring(oldPos));
                break;//last item and without value
            }
            if (paramString[oldPos] == '"')
            {
                // jump to before quote
                oldPos += 1;
                pos = paramString.IndexOf('"', oldPos);
                items.Add(name, paramString.Substring(oldPos, pos - oldPos));
            }
            else
            {
                pos = paramString.IndexOf(' ', oldPos);
                if (pos == -1)
                {
                    items.Add(name, paramString.Substring(oldPos));
                    break;//no more items
                }

                items.Add(name, paramString.Substring(oldPos, pos - oldPos));
            }


            oldPos = pos + 1;
        }

        return items;

    }

    public KeyValuePair<string, string> ExtractValue(string value, int pos1, int pos2)
    {
        var keyValue = value.Substring(pos1, pos2 - pos1 + 1);
        var colonPos = keyValue.IndexOf(':');
        if (colonPos == -1)
            throw new FormatException("Expected a colon for property " + keyValue);

        return new KeyValuePair<string, string>(keyValue.Substring(0, colonPos),
            keyValue.Substring(colonPos + 1));
    }
}

[TestClass]
public class ParserTests
{
    [TestMethod]
    public void Should_be_able_to_extract_name_from_a_line()
    {
        var line = "G195=Out:LED0799,LED0814,Flags:L-N Desc:\"EAF-QCH-B1-01\" Invert:00 STO:35 SP:0 FStart: FStop: ";

        var sut = new SettingsParser();
        var actual = sut.ExtractLine(line);

        Assert.AreEqual("G195", actual.Name);
    }

    [TestMethod, ExpectedException(typeof(FormatException))]
    public void Setting_name_is_required()
    {
        var line = "G195 malformed";

        var sut = new SettingsParser();
        sut.ExtractLine(line);
    }


    [TestMethod, ExpectedException(typeof(FormatException))]
    public void equals_must_be_before_first_colon()
    {
        var line = "G195:malformed name=value";

        var sut = new SettingsParser();
        sut.ExtractLine(line);
    }

    [TestMethod]
    public void Should_be_able_to_extract_a_single_parameter()
    {
        var line = "G195=Out:LED0799";

        var sut = new SettingsParser();
        var actual = sut.ExtractLine(line);

        Assert.AreEqual("LED0799", actual.Parameters["Out"]);
    }

    [TestMethod]
    public void should_be_able_to_parse_multiple_properties()
    {
        var line = "G195=Out:LED0799 Invert:00";

        var sut = new SettingsParser();
        var actual = sut.ExtractLine(line);

        Assert.AreEqual("00", actual.Parameters["Invert"]);
    }

    [TestMethod]
    public void should_be_able_to_include_spaces_in_value_names_if_they_are_wrapped_by_quotes()
    {
        var line = "G195=Out:\"LED0799 Invert:00\"";

        var sut = new SettingsParser();
        var actual = sut.ExtractLine(line);

        Assert.AreEqual("LED0799 Invert:00", actual.Parameters["Out"]);
    }

    [TestMethod]
    public void second_parameter_value_should_also_be_able_To_be_quoted()
    {
        var line = "G195=In:Stream Out:\"LED0799 Invert:00\"";

        var sut = new SettingsParser();
        var actual = sut.ExtractLine(line);

        Assert.AreEqual("LED0799 Invert:00", actual.Parameters["Out"]);
    }

    [TestMethod]
    public void allow_empty_values()
    {
        var line = "G195=In:";

        var sut = new SettingsParser();
        var actual = sut.ExtractLine(line);

        Assert.AreEqual("", actual.Parameters["In"]);
    }

    [TestMethod]
    public void allow_empty_values_even_if_its_not_the_last()
    {
        var line = "G195=In: Out:Heavy";

        var sut = new SettingsParser();
        var actual = sut.ExtractLine(line);

        Assert.AreEqual("", actual.Parameters["In"]);
    }
}

更新以回应评论

imho业务实体应该由构建器类构建,而构建器类又使用解析器,因为它们是两个不同的职责。我会使用Dictionary<string, Func<object>>为每个参数类型提供工厂。

然后你可以这样做:

public class CommandBuilder
{
    ParameterParser _parser = new ParameterParser();
    Dictionary<string, Func<Setting, Command>> _builders = new Dictionary<string, Func<Setting, Command>>();

    public IEnumerable<Command> Build(string config)
    {
        var settings = _parser.Parse(config);
        foreach (var setting in settings)
        {
            yield return _builders[setting.Name].Build(setting);
        }
    }

    public void Register(string name, Func<Setting, Command> builder)
    {
        _builders[name] = builder;
    }

}

允许您在不使用switch语句的情况下注册新命令:

var b = new CommandBuilder();
b.Register("SysEv01", setting => {
    var sysEvent = new SysEventCommand();
    sysEvent.Type = setting.Properties["Type"];
    sysEvent.OutPort = setting.Properties["Out"];
    return sysEvent;

});

答案 1 :(得分:1)

判断值类型的复杂性,我假设必须有来自硬件制造商的库读取此格式。如果不是基于完整的规范,编写自己的解析器将是不可靠的。

但是如果你想继续,我建议你写一个抽象的解析器类,它包含两个部分,第一个从左到右移动每个字符的方法,就像任何.NET读者一样但没有流。其次是暂时保存符号的缓冲区。完成后,您可以在解析器类中实现它并使用其方法来计算字符串。想象一下,每个字符或单词将决定解析过程中的下一个动作,它由一个方法表示。该方法可以返回结果类,或者如果不期望出现符号则抛出异常。我建议不要使用结果类,因为要解析每个元素的实例化和验证它们的开销。对于递归格式,请确保实现最大深度以防止堆栈溢出。

永远不要使用一种方法来完成所有工作,无论格式如何。它可以防止编译器进行内联等优化,这对于像解析器这样的高性能程序至关重要。涉及嵌套切换语句或本地状态变量的方法几乎总是指示错误的解析器设计。另外,请不要使用正则表达式解析器,其中任何一个都应该负责该过程。最好不要使用正则表达式进行解析。

答案 2 :(得分:0)

看起来像一个基于行的配置,带有':'作为每个参数的拆分分隔符。 所以解析器/正则表达式将是: 1.行开始直到'=' - &gt;部分名称 2.':'向后到sperator char(',','')是参数名称。 3.值将持续到下一场比赛2。

不在编写代码的地方,但应该这样做。 您可以将这些内容放入字典中以便更方便地访问。