C#解析的开闭原则

C# Open but closed principle for parsing

自定义解析应该使用什么来提供可扩展性而不必继承?换句话说,我正在寻找等效于解析自定义对象的 IFormatProvider(用于输出)。

与以下代码相反:

var str = String.Format(new MyFormatProvider(), "{0:custom}", obj);

其中 MyFormatProvider 实现 IFormatProvider

Open/Closed 基本上只是说新的或更改的行为应该是可添加的,而无需修改现有代码。在 OOP 中,这通常通过继承或接口(一种特殊的 "inheritance")来完成。

你需要对你的问题域更具体一些以获得更具体的答案,但作为使用接口的示例如下:

 interface IObjectParser {
     object Parse(object obj, propertyName string);
 }

 class ReflectionParser : IObjectParser {
     public object Parse(object obj, propertyName string) {
         return obj.GetType().GetProperty(propertyName).GetMethod().Invoke(obj);
     }
 }

 object parsedValue = new ReflectionParser().Parse(new MyClass(), "MyProperty");

然后,如果我们想添加一种新类型的解析器:

 class DatabaseParser : IObjectParser {
     public object Parse(object obj, propertyName string) {
         return ExecuteQuery(
            --Note the potential for SQL injection
            string.Format("SELECT {1} FROM {0} WHERE Id = @id", obj.GetType().Name, propertyName),
            ((dynamic)o).Id
         );           
     }
 }

 object parsedValue = new DatabaseParser(new MyClass(), "columnName");

这并不是 open/closed 所特有的,但是由于您提到了 IFormatProvider,它使用另一种技术在格式字符串中传输可变数量的参数,同时仍然坚持强类型。

我不确定它是否有规范名称,但我将其称为 "stringly typed"(您可以对对象数组等执行相同的操作 - 但使用 string 是相当普遍的,并且是一个很好的双关语)。你可以在 JavaScript 的 window.open windowFeatures and MVC's HtmlHelpers htmlAttributes 中看到这种类型的 API (它使用匿名类型来达到同样的效果),我会说它实际上是一个 anti-pattern 在大多数情况下因为它打破了 Liskov 替换原则,但它确实有它的利基用途。

IFormatProvider(从技术上讲,ICustomFormatter 负责这部分,IFormatProvider 是工厂)必须支持未知数量的可能格式。为此,它会在“:”之后插入任何自定义格式说明 - 预计适当的 ICustomFormatter 将知道如何处理这些值。

IObjectParser 案例中的一个示例如下:

 class ByteArrayParser : IObjectParser {
     public object Parse(object obj, propertyName string) {
         var bytes = obj as byte[];
         // we've tunneled multiple parameters in the propertyName string
         var nameParameters = propertyName.Split(":");
         // note we've lost our strong typing, and coupled ourselves to the propertyName format
         int index = int.Parse(nameParameters[0]);
         string readAsType = nameParameters[1];

         using (var ms = new MemoryStream(bytes))
         using (var br = new BinaryReader(ms))
         {
             ms.Position = index;
             switch (readAsType) {
                 case "float":
                     return br.ReadSingle();
                 case "int":
                     return br.ReadInt32();
                 case "string"
                     // we can even have yet another parameter only in special cases
                     if (nameParameters.Length > 2) {
                        // it's an ASCII string
                        int stringLength = int.Parse(nameParameters[2]);
                        return Encoding.ASCII.GetString(br.ReadBytes(stringLength));
                     } else {
                        // it's BinaryReader's native length-prefixed string
                        return br.ReadString();
                     }
               default:
                     // we don't know that type
                    throw new ArgumentOutOfRangeException("type");
             }
         }
     }
 }

使用此 class 时,您必须以一种只能通过文档发现的方式专门格式化 propertyName(就像 String.Format)。

 // note again, we have to know ByteArrayParser's specific format and lose strong typing
 object parsedIntValue = new ByteArrayParser().Parse(myBytes, "4:int");
 object parsedSingleValue = new ByteArrayParser().Parse(myBytes, "4:float"); 
 object parsedStringValue = new ByteArrayParser().Parse(myBytes, "4:string"); 
 object parsedAsciiStringValue = new ByteArrayParser().Parse(myBytes, "4:string:15");