Chapter 10 访问者模式

我们之前在《基本概念》中讲到:

TagUnion = TagGrp | TagCbnt | TagUnit

由于这是一个非常稳定的数据结构,所以我们很自然地想到应该为它提供一些基础设施以支持访问者模式

访问者

访问者模式的核心理念是,将行为与相对稳定的数据结构相剥离,并将新的行为抽象成对数据结构进行访问的新类。 在这里,TagUnion就是相对稳定的数据结构——它要么是TagUnit,要么是TagCbnt,要么是TagGroup

为了支持访问者模式,TagUnion应该可以接受一个访问者对自身进行访问。但是具体怎么访问,我们留待子类实现——因为只有子类才能知道自己是什么类型的节点:

public abstract record TagUnion
{
    /// 接受一个访问者对自己进行"访问"
    public abstract void Accept(ITagUnionVisitor visitor);
}

这里ITagUnionVisitor是一个接口抽象,规定了每种访问者应该支持访问 TagGrpTagCbnt、和TagUnit

public interface ITagUnionVisitor
{
    /// 访问 TagGrp 节点
    void Visit(TagUnion.TagGrp grp);

    /// 访问 TagCbnt 节点
    void Visit(TagUnion.TagCbnt cbnt);

    /// 访问 TagUnit 节点
    void Visit(TagUnion.TagUnit tag);
}

然后我们为每种数据节点提供实现——这里就是委托给访问者的访问方法,由于有了自身的类型信息,编译器可以清晰地知道它调用的是哪个重载方法:

public record TagUnit(ITag Value) : TagUnion 
{
    public override void Accept(ITagUnionVisitor visitor) 
    {
        visitor.Visit(this);  // 调用的是 Visit(TagUnion.TagUnit)方法
    }
};


public record TagCbnt(ITagCbnt Value) : TagUnion
{
    public override void Accept(ITagUnionVisitor visitor)
    {
        visitor.Visit(this); // 调用的是 Visit(TagUnion.Cbnt)方法
    }
}

public record TagGrp(ITagGrp Value): TagUnion
{
    public override void Accept(ITagUnionVisitor visitor)
    {
        visitor.Visit(this); // 调用的是 Visit(TagUnion.TagGrp)方法
    }
}

遍历器

让我们用访问者模式实现一个最简单的节点遍历器:

public class TagUnionTraverser : ITagUnionVisitor
{
    private Action<ITagGrp>? _procGrp;
    private Action<ITagCbnt>? _procCbnt;
    private Action<ITag>? _procUnit;

    /// <summary>
    /// c'tor
    /// </summary>
    /// <param name="procGrp">处理 ITagGrp节点本身,不处理子节点</param>
    /// <param name="procCbnt">处理 ITagCbnt 节点本身,不处理子节点</param>
    /// <param name="procTag">处理 ITag 节点本身</param>
    public TagUnionTraverser(
        Action<ITagGrp>? procGrp, 
        Action<ITagCbnt>? procCbnt, 
        Action<ITag>? procTag
    ){
        this._procGrp = procGrp;
        this._procCbnt = procCbnt;
        this._procUnit = procTag;
    }

    /// <inheritdoc/>
    public void Visit(TagUnion.TagGrp grp)
    {
        this._procGrp?.Invoke(grp.Value);
        foreach (var kvp in grp.Value.Children)
        {
            var child = kvp.Value;
            child.Accept(this);
        }
    }

    /// <inheritdoc/>
    public void Visit(TagUnion.TagCbnt cbnt)
    {
        this._procCbnt?.Invoke(cbnt.Value);

        foreach (var kvp in cbnt.Value.Children)
        {
            var key = kvp.Key;
            var child = cbnt[key];
            child.Accept(this);
        }
    }

    /// <inheritdoc/>
    public void Visit(TagUnion.TagUnit tag)
    {
        this._procUnit?.Invoke(tag.Value);
    }

}

于是我们可以这样使用这个遍历器:

var data = new TagUnion.TagGrp(grp);

// 构造遍历器对数据进行访问来统计 Tag 节点
var tagCount = 0;
var traverser = new TagUnionTraverser(
    null,                  // 什么都不做
    null,                  // 什么都不做
    tag => tagCount + 1    // 递增计数器
);

// 接受访问者
data.Accept(traverser);
Console.WriteLine($"tagCount = {tagCount}");