20、迭代器与访问者模式的深入解析

迭代器与访问者模式的深入解析

1. 迭代器模式

迭代器模式的目的是让客户端能够按顺序访问集合中的元素。在.NET FCL 中的集合类为集合操作提供了丰富的支持,包括迭代功能。当创建新类型的集合时,通常也需要为其创建一个迭代器。

以下是 CompositeEnumerator 类中相关方法的代码:

protected bool SubenumeratorNext()
{
    while (true)
    {
        //... as before
        ICompositeEnumerable c = 
(ICompositeEnumerable) _childEnumerator.Current;
        if (!_visited.Contains(c))
        {
            _subEnumerator = c.GetEnumerator(_visited);
            _subEnumerator.ReturnInterior = ReturnInterior;
        }
    }
}

public override bool MoveNext()
{
    if (!_visited.Contains(_head))
    {
        _visited.Add(_head);
        _current = _head;
        return true;
    }
    return SubenumeratorNext();
}

在实例化迭代器时,需要考虑在迭代过程中集合是否会发生变化。在单线程应用程序中,这种情况通常不太可能发生,但在多线程应用程序中,需要确保对集合的访问是同步的。为了在多线程应用程序中安全地进行迭代,可以通过锁定互斥对象来同步对集合的访问。可以在迭代时阻止所有访问,或者在克隆集合时短暂阻止访问。通过合理的设计,可以为迭代器代码的客户端提供线程安全。

2. 访问者模式
2.1 访问者模式概述

当需要扩展现有类层次结构的行为时,通常可以直接添加所需的方法。但有时,所需的行为可能与现有对象模型不一致,或者无法访问现有代码。在这种情况下,不修改类层次结构的类就可能无法扩展其行为。而访问者模式允许层次结构的开发者为其他开发者扩展层次结构的行为提供支持,其目的是在不改变层次结构类的情况下为层次结构定义新的操作。

2.2 访问者模式机制

访问者模式的机制包括以下两点:
- 在类层次结构中,为部分或所有类添加 Accept() 操作。该方法的每个实现都将接受一个自定义接口类型的参数。
- 创建一个接口,其中包含一组具有相同名称(通常为 Visit )但参数类型不同的操作。为层次结构中允许扩展的每个类声明一个这样的操作。

以下是 MachineComponent 类及其子类的 Accept() 方法代码:

public abstract class MachineComponent
{
    public abstract void Accept(MachineVisitor v);
}

public class Machine : MachineComponent
{
    public override void Accept(MachineVisitor v) 
    {
        v.Visit(this);
    }
}

public class MachineComposite : MachineComponent
{
    public override void Accept(MachineVisitor v) 
    {
        v.Visit(this);
    }
}

public interface IMachineVisitor
{
    void Visit(Machine m);
    void Visit(MachineComposite c);
}

虽然 Machine MachineComposite 类中的 Accept() 方法代码相同,但编译器会认为这两个方法存在差异。

2.3 普通访问者示例

假设在爱尔兰都柏林的工厂工作,开发者创建了新工厂机器组成的对象模型,并通过 ExampleMachine 类的静态 Dublin() 方法提供访问。想要在树视图中浏览这个结构,虽然无法访问 Machine 层次结构的源代码,但该层次结构支持访问者模式。

以下是创建可浏览机器层次结构视图的代码:

using System;
using System.Windows.Forms;
using Machines;
using UserInterface; 

public class TreeNodeVisitor : IMachineVisitor
{
    private TreeNode _tree = null;
    private TreeNode _current = null;

    public TreeNode TreeNode 
    {
        get 
        {
            return _tree;
        }
    }

    protected TreeNode AddNode(MachineComponent m) 
    {
        TreeNode newNode = new TreeNode(m.ToString());
        if (_current == null) 
        {
            _tree = newNode;
        }
        else
        {
            _current.Nodes.Add(newNode);
        }
        return newNode;
    }

    public void Visit(Machine m)
    {
        AddNode(m);
    }

    public void Visit(MachineComposite c)
    {
        TreeNode oldCurrent = _current;
        _current = AddNode(c);
        foreach (MachineComponent mc in c.Children)
        {
            mc.Accept(this);
        }
        _current = oldCurrent;
    }
}

public class ShowTreeNodeVisitor : Form
{
    public ShowTreeNodeVisitor()
    {
        TreeView view = new TreeView();
        view.Dock = DockStyle.Fill;
        view.Font = UI.NORMAL.Font; 
        TreeNodeVisitor v = new TreeNodeVisitor();
        v.Visit(ExampleMachine.Dublin());
        view.Nodes.Add(v.TreeNode);
        Controls.Add(view);
        Text = "Show Tree Node View"; 
    }

    public static void Main() 
    {
        Application.Run(new ShowTreeNodeVisitor());
    }
}

访问者模式中的双重分派机制确保了 TreeNodeVisitor 类中正确的 Visit() 方法被执行。通过这种方式,可以为访问的层次结构中的不同类型创建特定的访问者类方法,几乎可以添加任何行为。

3. 访问者模式的循环问题

ProcessComponent 层次结构中,流程模型自然会包含循环,访问者在遍历过程中必须注意避免无限循环。为了以“漂亮”或缩进的格式打印过程组件,可以创建一个访问者类 PrettyVisitor

以下是 PrettyVisitor 类的代码:

using System;
using System.Text;
using Processes;
using Utilities; 

public class PrettyVisitor : IProcessVisitor 
{
    public static readonly string INDENT_STRING = "    ";
    private StringBuilder _buf;
    private int _depth;
    private Set _visited;

    public StringBuilder GetPretty(ProcessComponent pc)
    {
        _buf = new StringBuilder();
        _visited = new Set();
        _depth = 0;
        pc.Accept(this);
        return _buf;
    }

    protected void PrintIndentedString(String s)
    {
        for (int i = 0; i < _depth; i++)
        {
            _buf.Append(INDENT_STRING);
        }
        _buf.Append(s);
        _buf.Append("\n");
    }

    public void Visit(ProcessStep s)
    {
        PrintIndentedString(s.Name);
    }

    public void Visit(ProcessAlternation a)
    {
        VisitComposite("?", a);
    }

    public void Visit(ProcessSequence s)
    {
        VisitComposite("", s);
    }

    protected void VisitComposite(
        String prefix, ProcessComposite c)
    {
        if (_visited.Contains(c))
        {
            PrintIndentedString(prefix + c.Name + "...");
        }
        else
        {
            _visited.Add(c);
            PrintIndentedString(prefix + c.Name);
            _depth++;
            foreach (ProcessComponent child in c.Children)
            {
                child.Accept(this);
            }
            _depth--;
        }
    }
}

public class ShowPrettyVisitor
{
    public static void Main()
    {
        ProcessComponent p = ShellProcess.Make();
        PrettyVisitor v = new PrettyVisitor();
        Console.WriteLine(v.GetPretty(p));
    }
}

通过使用 Set 对象跟踪已访问的节点,可以避免无限循环。运行上述程序可以得到更具信息性的输出,能够显示循环和交替步骤。

4. 访问者模式的争议

访问者模式是一个有争议的模式。一些开发者始终避免使用它,而另一些开发者则捍卫其使用并提出加强它的方法,但这些方法通常会增加复杂性。访问者模式存在一些设计问题,例如在 MachineComponent 层次结构中,层次结构的开发者区分了 Machine 节点和 MachineComposite 节点,但没有区分 Machine 的子类。如果在访问者中需要区分不同类型的机器,就需要使用 is 或其他技术来判断 Visit() 方法接收到的机器类型。在 ProcessComponent 层次结构中,层次结构的开发者意识到了流程模型中循环的危险,但能否将这些担忧传达给访问者开发者也是一个问题。

访问者模式的根本问题在于扩展层次结构的行为通常需要对层次结构的设计有一定的专业知识。如果缺乏这种专业知识,可能会陷入陷阱,例如在流程中不避免循环。如果对层次结构的机制有专业知识,可能会建立危险的依赖关系,一旦层次结构发生变化,这些依赖关系就会破裂。然而,在计算机语言解析器中,访问者模式似乎能很好地工作,并且不会产生下游问题。

以下是一个简单的 mermaid 流程图,展示访问者模式中 TreeNodeVisitor 的工作流程:

graph LR
    A[TreeNodeVisitor] --> B[Visit(ExampleMachine.Dublin())]
    B --> C{Child is Machine?}
    C -- Yes --> D[AddNode(Machine)]
    C -- No --> E[AddNode(MachineComposite)]
    E --> F[Change _current pointer]
    F --> G[foreach child in MachineComposite.Children]
    G --> H[child.Accept(TreeNodeVisitor)]
    H --> C

综上所述,迭代器模式和访问者模式都有其独特的应用场景和优缺点。在实际开发中,需要根据具体需求和情况选择合适的模式。

迭代器与访问者模式的深入解析

5. 迭代器模式的深入探讨

在迭代器模式中,除了前面提到的基本操作,还需要考虑一些特殊情况。例如,在多线程环境下,集合的并发修改问题是一个需要重点关注的方面。当多个线程同时对集合进行修改和迭代操作时,可能会导致迭代结果不准确或抛出异常。为了避免这种情况,可以采用以下几种方法:

  • 同步访问 :通过锁定互斥对象来确保在迭代过程中集合不会被其他线程修改。例如,可以使用 lock 语句来实现同步:
private readonly object _syncRoot = new object();
public bool MoveNext()
{
    lock (_syncRoot)
    {
        // 迭代逻辑
    }
}
  • 克隆集合 :在迭代之前先克隆集合,这样即使原始集合被修改,迭代的是克隆后的副本,不会受到影响。但这种方法会增加内存开销。

以下是一个简单的表格,对比了这两种方法的优缺点:
| 方法 | 优点 | 缺点 |
| ---- | ---- | ---- |
| 同步访问 | 实现简单,不增加额外内存开销 | 可能会导致线程阻塞,影响性能 |
| 克隆集合 | 迭代不会受到原始集合修改的影响 | 增加内存开销,可能影响性能 |

6. 访问者模式的应用拓展

访问者模式的应用场景不仅仅局限于前面提到的机器层次结构和流程模型。在很多其他领域,如文档处理、图形处理等,也可以使用访问者模式来实现新的操作。

例如,在文档处理中,可以定义一个文档元素的层次结构,如段落、标题、图片等。然后通过访问者模式来实现不同的操作,如统计字数、生成摘要等。

以下是一个简单的示例代码:

// 文档元素抽象类
public abstract class DocumentElement
{
    public abstract void Accept(DocumentVisitor v);
}

// 段落类
public class Paragraph : DocumentElement
{
    public string Text { get; set; }
    public override void Accept(DocumentVisitor v)
    {
        v.Visit(this);
    }
}

// 标题类
public class Heading : DocumentElement
{
    public string Title { get; set; }
    public override void Accept(DocumentVisitor v)
    {
        v.Visit(this);
    }
}

// 文档访问者接口
public interface DocumentVisitor
{
    void Visit(Paragraph p);
    void Visit(Heading h);
}

// 统计字数的访问者类
public class WordCountVisitor : DocumentVisitor
{
    private int _wordCount = 0;
    public int WordCount
    {
        get { return _wordCount; }
    }

    public void Visit(Paragraph p)
    {
        _wordCount += p.Text.Split(' ').Length;
    }

    public void Visit(Heading h)
    {
        _wordCount += h.Title.Split(' ').Length;
    }
}
7. 访问者模式的优化建议

为了减少访问者模式的脆弱性,可以采取以下一些优化建议:

  • 使用泛型 :在访问者接口中使用泛型可以减少代码的重复。例如:
public interface IGenericVisitor<T>
{
    void Visit(T item);
}
  • 提供默认实现 :在访问者接口中提供默认实现,这样可以避免在添加新的元素类型时需要修改所有的访问者类。例如:
public interface IMachineVisitor
{
    void Visit(Machine m);
    void Visit(MachineComposite c);
    default void VisitOther(object o) { }
}
8. 总结

迭代器模式和访问者模式都是非常有用的设计模式,它们各自解决了不同的问题。迭代器模式主要用于提供一种统一的方式来访问集合中的元素,而访问者模式则用于在不修改类层次结构的情况下为其添加新的操作。

在实际应用中,需要根据具体的需求和场景来选择合适的模式。同时,要注意访问者模式的脆弱性,通过合理的设计和优化来减少其带来的风险。

以下是一个 mermaid 流程图,展示文档处理中访问者模式的工作流程:

graph LR
    A[DocumentVisitor] --> B[Visit(DocumentElement)]
    B --> C{Element is Paragraph?}
    C -- Yes --> D[Visit(Paragraph)]
    C -- No --> E{Element is Heading?}
    E -- Yes --> F[Visit(Heading)]
    E -- No --> G[VisitOther(object)]

通过对这两种模式的深入理解和应用,可以提高代码的可维护性和可扩展性,使软件系统更加健壮和灵活。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值