繁体   English   中英

在 C# 中实现访问者模式

[英]Implementing visitor Pattern in C#

我是这种模式的新手,请有人帮助我吗?

我有一个这样的对象:

public class Object
    {
        public string Name { get; set; }
        public object Value { get; set; }
        public List<Object> Childs { get; set; }
    }

这是一个 JSON 示例:

  {
    "Name": "Method",
    "Value": "And",
    "Childs": [{
        "Name": "Method",
        "Value": "And",
        "Childs": [{
            "Name": "Operator",
            "Value": "IsEqual",
            "Childs": [{
                "Name": "Name",
                "Value": "5",
                "Childs": []
            }]
        },
        {
            "Name": "Operator",
            "Value": "IsEqual",
            "Childs": [{
                "Name": "Name",
                "Value": "6",
                "Childs": []
            }]
        }]
    },
    {
        "Name": "Operator",
        "Value": "IsEqual",
        "Childs": [{
            "Name": "Name",
            "Value": "3",
            "Childs": []
        }]
    }]
}

我的问题是如何制作访问者模式以获得最后的字符串:

(Name IsEqual 3)And((Name IsEqul 5)And(Name IsEqual 6))

要实现访问者模式,您需要两个简单的接口

  1. IVisitable带有一个以IVisitor作为参数的Accept方法。
  2. IVisitorIVisitable每个实现提供了许多Visit方法

所以访问者模式的基本思想是根据实现的类型动态改变行为。

对于您的情况,您想要访问的东西(可访问的)是Object类,它显然没有不同的衍生物,并且您想根据属性值而不是类型来更改行为。 所以访问者模式并不是你真正需要的,我强烈建议你考虑递归方法的答案。

但是如果你真的想在这里使用访问者模式,它可能看起来像这样。

interface IVisitable { void Accept(IVisitor visitor); }

interface IVisitor {
    void VisitAnd(Object obj);
    void VisitEquals(Object obj);
}

由于Object类是一个简单的 POCO,我假设您不会想要实现一个接口并将方法添加到这个类中。 所以你需要一个适配器,该适配对象ObjectIVisitable

class VisitableObject : IVisitable {
    private Object _obj;

    public VisitableObject(Object obj) { _obj = obj; }

    public void Accept(IVisitor visitor) {
        // These ugly if-else are sign that visitor pattern is not right for your model or you need to revise your model.
        if (_obj.Name == "Method" && _obj.Value == "And") {
            visitor.VisitAnd(obj);
        }
        else if (_obj.Name == "Method" && _obj.Value == "IsEqual") {
            visitor.VisitEquals(obj);
        }
        else
            throw new NotSupportedException();
        }
    }
}

public static ObjectExt {
    public static IVisitable AsVisitable(this Object obj) {
        return new VisitableObject(obj);
    }
}

最后访问者实现可能看起来像这样

class ObjectVisitor : IVisitor {
    private StringBuilder sb = new StringBuilder();

    public void VisitAnd(Object obj) {
        sb.Append("(");
        var and = "";
        foreach (var child in obj.Children) {
            sb.Append(and);
            child.AsVisitable().Accept(this);
            and = "and";
        }
        sb.Append(")");
    }

    public void VisitEquals(Object obj) {
        // Assuming equal object must have exactly one child 
        // Which again is a sign that visitor pattern is not bla bla...
        sb.Append("(")
          .Append(obj.Children[0].Name);
          .Append(" Equals ");
          .Append(obj.Children[0].Value);
          .Append(")");
    }
}

JSON 清楚地表示令牌树(可能由解析器生成)。

访问者模式使用多态。

为了被访问者模式使用,您必须反序列化它以获得具有不同访问行为的对象:

  • 方法令牌
  • 操作员令牌
  • 名称令牌

然后 IVisitor 应该为每个实现 Visit 方法:

public interface IVisitor
{
    void Visit(MethodToken token) { /* */ }
    void Visit(OperatorToken token) { /* */ }
    void Visit(NameToken token) { /* */ }
}

public interface IVisitable
{
    void Accept(IVisitor visitor);
}

public class MethodToken : IVisitable
{
    public void Accept(IVisitor visitor)
    {
        visitor.Visit(this);
    }
}

补充说明:

Object是一个非常糟糕的名字,尤其是在 C# 中,因为Object是每个类的基类,更不用说冲突了,它没有传达任何特殊含义...... token 呢?

public class Token
{
    public string Name { get; set; }
    public string Value { get; set; }
    public List<Token> Children { get; set; }
}

关于财产 Childs...

访客目的

如果您不知道何时/为什么使用螺丝刀(顺便说一下,它可能很危险),则不应使用螺丝刀。

访问者模式有助于避免“丑陋”/难以维护/阅读十几个开关案例或更糟糕的if else if else同时为您提供强大的类型检查优势。 它还有助于将相关代码(高内聚)保持在一个类(访问者)中。 当然,一旦实现,对象树(这里是令牌)可以被多种访问者访问,只要他们实现了IVisitor接口。

在你的情况,你必须先各自将Token来的强烈亚型Token (通过字典映射,以避免任何IF /开关或定制反序列化)

在你的情况下:

  1. 首先读取文本(显然是json格式)并将其转换为对象。 我们通常称之为反序列化。 这是可能的,因为文本已经使用众所周知的正确结构化格式进行格式化,很容易找到词法分析器/解析器。 (否则,您将不得不编写自己的词法分析器/解析器或使用诸如 lex/yacc 之类的东西)。

但是,我们必须将文本的每个部分部分反序列化为正确的类型。 我们将使用Newtonsoft.Json来做到这一点:

// We define a base class abstract (it cannot be instantiated and we can enforce implementation of methods like the Accept()
public abstract class BaseToken : IVisitable
{
    public string Value { get; set; }
    public List<BaseToken> Children { get; } = new List<BaseToken>();
    public abstract void Accept(IVisitor visitor);
}

阅读文本并解析 Json:

// Load text in memory
var text = File.ReadAllText("path/to/my/file.json");
// Get Token instance
var jsonToken = JObject.Parse(text);
  1. 我们必须处理JToken提取正确的类实例
// Get the strong typed tree of token
var token = CreateToken(jsonToken);

CreateToken方法:

private static BaseToken CreateToken(JToken jsonToken)
{
    var typeOfToken = jsonToken["Name"];
    if (typeOfToken == null || typeOfToken.Type != JTokenType.String)
    {
        return null;
    }

    BaseToken result;
    switch (typeOfToken.ToString())
    {
        case "Method":
        {
            result = jsonToken.ToObject<MethodToken>();
            break;
        }
        case "Operator":
        {
            result = jsonToken.ToObject<OperatorToken>();
            break;
        }
        default:
        {
            result = jsonToken.ToObject<NameToken>();
            break;
        }
    }

    var jChildrenToken = jsonToken["Childs"];
    if (result != null &&
        jChildrenToken != null &&
        jChildrenToken.Type == JTokenType.Array)
    {
        var children = jChildrenToken.AsJEnumerable();
        foreach (var child in children)
        {
            var childToken = CreateToken(child);
            if (childToken != null)
            {
                result.Children.Add(childToken);
            }
        }
    }

    return result;
}

如您所见,文本上仍有一些开关模式。

  1. 然后调用令牌访问者
// Create the visitor
var tokenVisitor = new TokenVisitor();
// Visit the tree with visitor
token.Accept(tokenVisitor);
// Output the result
Console.WriteLine(tokenVisitor.Output);

TokenVisitor代码

internal class TokenVisitor : IVisitor
{
    private readonly StringBuilder _builder = new StringBuilder();
    // invert the order of children first
    private int firstIndex = 1;
    private int secondIndex = 0;

    // Keep track of name tokens
    private readonly HashSet<BaseToken> _visitedTokens = new HashSet<BaseToken>();

    public string Output => _builder.ToString();
    
    public void Visit(MethodToken token)
    {
        // Store local to avoid recursive call;
        var localFirst = firstIndex;
        var localSecond = secondIndex;
        // back to normal order of children
        firstIndex = 0;
        secondIndex = 1;
        RenderChild(token.Children, localFirst);
        _builder.Append(token.Value);
        RenderChild(token.Children, localSecond);
    }

    private void RenderChild(List<BaseToken> children, int index)
    {
        if (children.Count > index)
        {
            _builder.Append("(");
            children[index].Accept(this);
            _builder.Append(")");
        }
    }

    public void Visit(OperatorToken token)
    {
        if (token.Children.Count > 0)
        {
            token.Children[0].Accept(this);
            _builder.Append(" ");
        }
        _builder.Append(token.Value);
        if (token.Children.Count > 0)
        {
            _builder.Append(" ");
            token.Children[0].Accept(this);
        }
    }

    public void Visit(NameToken token)
    {
        if (_visitedTokens.Contains(token))
        {
            _builder.Append(token.Value);
        }
        else
        {
            _visitedTokens.Add(token);
            _builder.Append(token.Name);
        }
    }
}

上述实现旨在满足您的期望(即准确输出预期的字符串)。 它可能不是防弹的。 你可以在GitHub 上找到完整的代码

首先,您在结果中的顺序错误。其次,有时您会错过结果中的括号。最终它应该是:

(((Name IsEqual 5) And (Name IsEqual 6)) And (Name IsEqual 3))

要完成此任务,您应该使用递归函数。

  static IEnumerable<string> ReturnString(Obj val)
        {
            foreach (Obj node in val.Childs)
                yield return ConvertToString(node);
        }

        static string ConvertToString(Obj val)
        {
            switch(val.Name)
            {
                case "Operator":
                    {
                        return string.Format("({0} {1} {2})", val.Childs[0].Name, val.Value, val.Childs[0].Value);
                    }
                case "Method":
                    {
                        IEnumerable<string> coll = ReturnString(val);
                        StringBuilder final = new StringBuilder();
                        final.Append("(");

                        IEnumerator<string> e = coll.GetEnumerator();
                        e.MoveNext();
                        final.Append(string.Format("{0}", e.Current, val.Value));

                        while (e.MoveNext())
                        {
                           final.Append(string.Format(" {0} {1}", val.Value, e.Current));
                        }

                        final.Append(")");


                        return final.ToString();
                    }
                case "Name":
                    return  Convert.ToString(val.Value);
           }
           return "-";
        }

以下是您在代码中的示例:

string s = ConvertToString(new Obj
            {
                Name = "Method",
                Value = "And",
                Childs = new List<Obj>
                        {
                            new Obj()
                            {
                                Name = "Method",
                                Value = "And",
                                Childs = new List<Obj>
                                {
                                     new Obj()
                                    {
                                        Name = "Operator",
                                        Value = "IsEqual",
                                        Childs = new List<Obj>
                                        {
                                           new Obj()
                                           {
                                               Name="Name",
                                               Value="5",
                                               Childs=null
                                           }
                                        }
                                    },
                                    new Obj()
                                    {
                                    Name = "Operator",
                                        Value = "IsEqual",
                                        Childs = new List<Obj>
                                        {
                                           new Obj()
                                           {
                                               Name="Name",
                                               Value="6",
                                               Childs=null
                                           }
                                        }
                                    }
                                }
                            },
                            new Obj()
                            {
                                Name = "Operator",
                                Value = "IsEqual",
                                Childs = new List<Obj>
                                {
                                   new Obj()
                                   {
                                       Name="Name",
                                       Value="3",
                                       Childs=null
                                   }
                                }
                            }
                        }
            });

这可能不是您想要的。 但是不使用访问者模式创建您想要的输出的一种方法是将以下方法添加到Object类,如下所示:

public string Format()
{
    if (Name == "Operator")
    {
        if(Childs == null || Childs.Count != 1)
            throw new Exception("Invalid Childs");

        Object chlid = Childs[0];

        return chlid.Name + " IsEqual " + chlid.Value;

    }

    if (Name == "Method")
    {
        if(Childs == null || Childs.Count == 0)
            throw new Exception("Invalid Childs");

        var str = " " + Value + " ";

        return string.Join(str, Childs.Select(x => "(" +  x.Format() + ")"));
    }

    throw new Exception("Format should only be invoked on Operator/Method");
}

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM