在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))
要实现访问者模式,需要两个简单的接口
-
IVisitable
,以IVisitor
为参数的Accept
方法 -
IVisitor
与Visit
方法的每个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或自定义反序列化)
在你的例子中:
- 首先读取文本(显然是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");
}