设计模式解析设置文件 [英] Design Pattern to Parse Setting Files

查看:87
本文介绍了设计模式解析设置文件的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述



以下是一个例子:



我一直在尝试找到一个优雅的解决方案来解析设置文件。 pre> L09D21 =类型:OPT Z:0000 F:--- AZ --- S:+0 AVF:OFF描述:AHU-QCH 07.05EX PROBEOut :,
G195 = Out:LED0799,LED0814,标志:LN描述:EAF-QCH-B1-01反相:00 STO:35 SP:0 FStart:FStop:
SysEv01 =类型:FANLATCH输出:LED1165

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



我们当前的代码由多个/嵌套的switch语句组成,用于对文件/行的每个部分进行解码。 / p>

有没有一个设计模式,我可以看一下解决类似的问题?



我的感觉是那里是某种我现在看不到的多态解决方案

解决方案

我们来看一下最简单的一行:

  SysEv01 =类型:FANLATCH出:LED1165 

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

  public class设置
{
public string Name {get;组; }
public IDictionary< string,string>属性{get; }
}

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

  G195 = Out:LED0799,LED0814,标志:LN描述:EAF-QCH-B1-01反相:00 STO:35 SP: 0 FStart:FStop:

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



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

public string Name {get;私人集合
}

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

[TestClass]
public class ParserTests
{
[TestMethod]
public void Should_be_able_to_extract_name_from_a_line()
{
var line =G195 = Out:LED0799,LED0814,标志:LN描述:\EAF-QCH-B1-01\反相: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(预期一个等号,它位于第一个冒号之前);

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

返回设置;
}

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



代码现在是:

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

public string Name {get;私人集合}
public IDictionary< string,string>参数{get;组;
}

public class SettingsParser
{
public设置ExtractLine(string line)
{
var pos = line.IndexOfAny(new [] {'=',':'});
if(pos == -1 || line [pos] ==':')
throw new FormatException(预期一个等号,它位于第一个冒号之前);

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

返回设置;
}

private IDictionary< string,string> ExtractParameters(string paramString)
{
var keyValues = paramString.Split('');
var items = new Dictionary< string,string>();
foreach(keyValue中的var keyValue)
{
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));
}

返回项目;
}
}

并为此测试:

  [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类设置
{
public Setting(string name)
{
if(name == null)throw new ArgumentNullException(name);
Name = name;
}

public string Name {get;私人集合}
public IDictionary< string,string>参数{get;组;
}

public class SettingsParser
{
public设置ExtractLine(string line)
{
var pos = line.IndexOfAny(new [] {'=',':'});
if(pos == -1 || line [pos] ==':')
throw new FormatException(预期一个等号,它位于第一个冒号之前);

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

返回设置;
}

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; //不再有属性
var name = paramString.Substring(oldPos,pos - oldPos);


oldPos = pos +1; //设置该值从name和冒号开始
if(oldPos> = paramString.Length)
{
items.Add(name,paramString.Substring(oldPos));
break; //最后一个项目而没有值
}
if(paramString [oldPos] =='')
{
//跳转到引用之前
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; //不再有项目
}

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


oldPos = pos + 1;
}

返回项目;

}

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

返回新的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,标志:LN描述:\EAF-QCH-B1-01\反相: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:格式错误的名称=值;

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

[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 =新的SettingsParser();
var actual = sut.ExtractLine(line);

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

更新回应评论



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



那么你可以这样做:

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

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

public void注册(字符串名称,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;

});


I have been trying to find an elegant solution to parse settings files.

Below is an example:

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

Each line could have a different mapping and multiple of the same line types can be in the file. (These setting come from hardware devices that we need to configure)

Our current code consists out of Multiple/nested switch statements that decode each part of the file/line.

Is there a design pattern that I could look at that solves a similar problem?

My feeling is that there is some kind of polymorphic solution that I am not seeing at the moment

解决方案

Let's look at the simplest line:

SysEv01=Type:FANLATCH Out:LED1165

From that we can read that we have a setting name and then a bunch of properties. The setting name is delimited using = and the properties are separated by a white space. Finally we can also see that property name/value are separated by colon.

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

Let's look at the most complex line to verify this:

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

Seems to apply. What's interesting is that the value can be omitted, so we have to take that into account when parsing. another thing is that property values can be wrapped with quotes ("EAF-QCH-B1-01").

So let's write a simple parser and test it. The easiest way to being is to just parse a single line to get the different parts from it. Let's start by just getting the setting name and a string for all contents:

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);
    }
}

We have a minor problem with that code, and that's if the line is malformed. Let's make sure that we get a equals sign and that's it's found before a colon.

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;
}

Now let's continue to extract the parameters. To take the simplest possible approach we just split the string on space and then go through each entry and split it on colon.

the code is now:

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;
    }
}

And the test for this:

[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"]);
}

Fast forward and you got this solution. The code uses a simple loop and string.IndexOf as it have to take into account the following scenarios:

  • Property without a value
  • Quoted property values
  • Single property
  • Multiple properties

Code:

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"]);
    }
}

Update in response to comments

imho the business entities should be built by a builder class that in turn uses the parser as they are two distinct responsibilities. I would use a Dictionary<string, Func<object>> to provide factories for each parameter type.

Then you can do something like this:

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;
    }

}

Which allows you to register new commands without using a switch statement:

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

});

这篇关于设计模式解析设置文件的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆