在c#中实现访问者模式

本文关键字:访问者 模式 实现 | 更新日期: 2023-09-27 18:15:50

我是这个图案的新手,有人能帮我吗?

我得到了一个像这样的对象:

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

在c#中实现访问者模式

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

  1. IVisitable,以IVisitor为参数的Accept方法
  2. IVisitorVisit方法的每个IVisitable的实现

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

对于你的情况,你想要访问的东西(visitable)是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(")");
    }
}

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

访问者模式使用多态性

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

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

附加备注:

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

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

关于属性children…

访问目的

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

访问者模式在提供强类型检查优势的同时,可以避免"丑陋"/难以维护/读起来很痛苦的切换案例或更糟糕的if else if else。它还有助于将相关代码(高内聚)保存在一个类(Visitor)中。当然,一旦实现,对象树(这里是令牌)可以被多种访问者访问,只要它们实现了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);
  • 我们必须处理JToken 提取正确的类实例:
  • // Get the strong typed tree of token
    var token = CreateToken(jsonToken);
    

    CreateToken method:

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

    正如你所看到的,文本上仍然有一些切换模式。

  • 然后调用令牌访问者:
  • // 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
                                       }
                                    }
                                }
                            }
                });
    

    这可能不是您想要的。但是,要在不使用Visitor模式的情况下创建输出,有一种方法是将以下方法添加到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");
    }