在C#中实现访问者模式

8

我对这个模式很陌生,能有人帮我吗?

我有一个像这样的对象:

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

4
你的问题不够清晰,请加以改进。你是什么意思,指Name和Value可以是方法或运算符。而你所说的“ex”又是什么意思?“And”和“IsEqualTo”与你的问题有什么关联? - Yacoub Massad
好的,有一个模式,但问题是什么? - Matyas
现已更新为JSON格式,猜测问题现在更清晰了。 - moodle
你确定这个 JSON 示例是正确的吗?你使用了一个具有子运算符的运算符(IsEqual)。 - Yacoub Massad
是的,先生。我收到了这样的动态JSON,并被要求实现这种模式,但我对此完全不熟悉。 - moodle
显示剩余6条评论
4个回答

26

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

  1. IVisitable,其中包含一个将 IVisitor 作为参数的 Accept 方法。
  2. IVisitor,包含多个 Visit 方法,分别用于实现 IVisitable 的每个实现类。

因此,访问者模式的基本思想是根据实现类型动态更改行为。

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

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

interface IVisitable { void Accept(IVisitor visitor); }

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

由于Object类是一个简单的POCO,我假设您不想实现一个接口并将方法添加到这个类中。因此,您需要一个适配器对象,将Object适配为IVisitable

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

1
点赞这个伟大而简单的模式示例。谢谢! - Scott Fraley
正如@Fab在下面所述。访问者模式使用多态性。此答案中的实现没有使用多态性,而是使用if-else。这不是访问者设计模式。 - kub1x

15

JSON可以清晰地表示一个令牌树(可能由解析器生成)。

访问者模式使用多态性。

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

  • MethodToken
  • OperatorToken
  • NameToken

然后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);
    }
}
< p > < em > < strong > 补充说明: < p > < code > Object 是一个非常糟糕的名称,尤其是在 C# 中,因为 < code > Object 是每个类的基类,更不用说冲突了,它并没有传达任何特殊的含义...那令牌呢?
public class Token
{
    public string Name { get; set; }
    public string Value { get; set; }
    public List<Token> Children { get; set; }
}

关于Childs属性...

访问者模式的目的

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

访问者模式有助于避免使用十几个switch case或更糟糕的if else if else(难以维护、阅读困难),同时还可以带来强类型检查的优势。它还有助于将相关代码(高内聚)放在一个类(访问者)中。当然,一旦实现,对象树(此处是标记)的访问者可以由多种类型的访问者访问,只要它们实现了IVisitor接口。

在您的情况下,您必须首先将每个Token转换为Token的强子类型(通过字典映射避免任何if/switch或自定义反序列化)

在您的情况下:

  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上找到完整的代码。


与其他设计模式不同,这是访问者设计模式。 - kub1x
最佳答案是这个。 - anar khalilov

0

首先,你的结果顺序有误。其次,有时候你会在结果中漏掉括号。最后应该是这样的:

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

这个回复如何回答这个问题? - Payedimaunt

-1

这可能不是你想要的。但是,如果不使用访问者模式创建所需的输出,一种方法是将以下方法添加到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");
}

问题是关于“如何实现访问者模式”。 - Payedimaunt

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接