解析设置文件的设计模式

我一直试图找到一个优雅的解决方案来解析设置文件。

以下是一个例子:

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

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

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

是否有一种我可以看到的设计模式解决了类似的问题?

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

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

 SysEv01=Type:FANLATCH Out:LED1165 

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

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

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

 G195=Out:LED0799,LED0814,Flags:LN 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:LN 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 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 ExtractParameters(string paramString) { var keyValues = paramString.Split(' '); var items = new Dictionary(); 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 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 ExtractParameters(string paramString) { var oldPos = 0; var items = new Dictionary(); 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 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(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:LN 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>为每个参数类型提供工厂。

然后你可以做这样的事情:

 public class CommandBuilder { ParameterParser _parser = new ParameterParser(); Dictionary> _builders = new Dictionary>(); public IEnumerable 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 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; }); 

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

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

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

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

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