49、访问者模式与多重分派详解

访问者模式与多重分派详解

1. 访问者模式的多线程限制与解决方案

在多线程程序中,访问者模式存在一定限制。由于多个线程不能使用同一个访问者对象来访问不同的宠物对象,所以该访问者并非线程安全的。常见的解决办法是为每个线程使用一个访问者对象,通常是在调用访问者的函数栈上创建的局部变量。若此方法不可行,还有更复杂的方案可赋予访问者线程局部状态,但对这些方案的分析暂不展开。

有时,我们希望在多次访问中累积结果,此时将结果存储在访问者对象中的技术就十分适用。同样,我们也可以利用这种方法将参数传递给访问者操作,而无需将参数添加到 visit() 函数中,只需将参数存储在访问者对象内部,就能方便地从访问者中访问这些参数。这种技术在每次调用访问者时参数不变,但不同访问者对象的参数可能不同的情况下尤为有效。

2. 访问复杂对象

2.1 访问复合对象的一般思路

访问复杂对象时,当访问对象本身,我们通常不清楚如何处理每个组件或包含的对象。但针对该对象类型编写的访问者专门用于处理该类对象。因此,处理组件对象的正确方式是逐个访问它们,将问题委托给专门的访问者,这在编程及其他领域都是一种强大的技术。

2.2 简单容器类示例

Shelter 类为例,它可包含任意数量代表等待领养宠物的宠物对象:

// Example 06
class Shelter {
public:
    void add(Pet* p) {
        pets_.emplace_back(p);
    }
    void accept(PetVisitor& v) {
        for (auto& p : pets_) {
            p->accept(v);
        }
    }
private:
    std::vector<std::unique_ptr<Pet>> pets_;
};

该类本质上是一个适配器,使宠物对象的向量可被访问。需注意,此类对象拥有其包含的宠物对象,当 Shelter 对象被销毁时,向量中的所有 Pet 对象也会被销毁。任何包含唯一指针的容器都拥有其包含的对象,这是将多态对象存储在 std::vector 等容器中的正确方式。

Shelter::accept() 方法决定了如何访问 Shelter 对象。我们不直接在 Shelter 对象上调用访问者,而是将访问委托给每个包含的对象。由于我们的访问者已能处理 Pet 对象,无需额外操作。例如,当 Shelter FeedingVisitor 访问时,庇护所中的每只宠物都会被喂食,无需编写特殊代码。

2.3 家庭对象示例

考虑一个代表有两只家庭宠物(一只狗和一只猫)的家庭对象:

// Example 07
class Family {
public:
    Family(const char* cat_color, const char* dog_color) :
        cat_(cat_color), dog_(dog_color) {}
    void accept(PetVisitor& v) {
        cat_.accept(v);
        dog_.accept(v);
    }
private: // Other family members not shown for brevity
    Cat cat_;
    Dog dog_;
};

使用 PetVisitor 层次结构中的访问者访问家庭时,会将访问委托给每个 Pet 对象,访问者已有处理这些对象的能力。当然, Family 对象也可接受其他类型的访问者,需为其编写单独的 accept() 方法。

3. 使用访问者模式进行序列化和反序列化

3.1 序列化和反序列化问题概述

序列化时,每个对象需转换为位序列,这些位需存储、复制或发送。操作的第一部分取决于对象本身(每个对象的转换方式不同),第二部分取决于序列化的具体应用(保存到磁盘与通过网络发送不同)。实现依赖于两个因素,这正是访问者模式提供的双重分派的用武之地。若能序列化某个对象并反序列化(从位序列重建对象),则在该对象包含在其他对象中时,也应使用相同方法。

3.2 二维几何对象层次结构示例

考虑以下二维几何对象的层次结构:

// Example 08
class Geometry {
public:
    virtual ~Geometry() {}
};
class Point : public Geometry {
public:
    Point() = default;
    Point(double x, double y) : x_(x), y_(y) {}
private:
    double x_ {};
    double y_ {};
};
class Circle : public Geometry {
public:
    Circle() = default;
    Circle(Point c, double r) : c_(c), r_(r) {}
private:
    Point c_;
    double r_ {};
};
class Line : public Geometry {
public:
    Line() = default;
    Line(Point p1, Point p2) : p1_(p1), p2_(p2) {}
private:
    Point p1_;
    Point p2_;
};

所有对象都派生自抽象的 Geometry 基类,较复杂的对象包含一个或多个较简单的对象。最终,所有对象都由双精度数组成,因此会序列化为数字序列。关键在于知道哪个双精度数代表哪个对象的哪个字段,以便正确还原原始对象。

3.3 序列化和反序列化的实现步骤

3.3.1 声明基访问者类
// Example 08
class Visitor {
public:
    virtual void visit(double& x) = 0;
    virtual void visit(Point& p) = 0;
    virtual void visit(Circle& c) = 0;
    virtual void visit(Line& l) = 0;
};

这里可访问双精度值,每个访问者需适当处理它们(写入、读取等)。访问任何几何对象最终都会访问其组成的数字。

3.3.2 基类和派生类接受访问者
// Example 08
class Geometry {
public:
    virtual ~Geometry() {}
    virtual void accept(Visitor& v) = 0;
};

虽然无法为双精度类型添加 accept() 成员函数,但这并不影响。派生类的 accept() 成员函数会按顺序访问每个数据成员:

// Example 08
void Point::accept(Visitor& v) {
    v.visit(x_); // double
    v.visit(y_); // double
}
void Circle::accept(Visitor& v) {
    v.visit(c_); // Point
    v.visit(r_); // double
}
void Line::accept(Visitor& v) {
    v.visit(p1_); // Point
    v.visit(p2_); // Point
}
3.3.3 具体访问者类负责序列化和反序列化

以将所有对象序列化为字符串为例:

// Example 08
class StringSerializeVisitor : public Visitor {
public:
    void visit(double& x) override { S << x << " "; }
    void visit(Point& p) override { p.accept(*this); }
    void visit(Circle& c) override { c.accept(*this); }
    void visit(Line& l) override { l.accept(*this); }
    std::string str() const { return S.str(); }
private:
    std::stringstream S;
};
// Example 08
Line l(...);
Circle c(...);
StringSerializeVisitor serializer;
serializer.visit(l);
serializer.visit(c);
std::string s(serializer.str());
3.3.4 反序列化访问者
// Example 08
class StringDeserializeVisitor : public Visitor {
public:
    StringDeserializeVisitor(const std::string& s) {
        S.str(s);
    }
    void visit(double& x) override { S >> x; }
    void visit(Point& p) override { p.accept(*this); }
    void visit(Circle& c) override { c.accept(*this); }
    void visit(Line& l) override { l.accept(*this); }
private:
    std::stringstream S;
};
// Example 08
Line l1;
Circle c1;
// s is the string from a serializer
StringDeserializeVisitor deserializer(s);
deserializer.visit(l1); // Restored Line l
deserializer.visit(c1); // Restored Circle c

3.4 处理未知对象类型的反序列化

当反序列化时不知道会遇到什么对象,对象存储在可访问的容器中,需确保对象按相同顺序序列化和反序列化。以 Intersection 类为例,它存储表示两个其他几何图形交集的几何图形:

// Example 09
class Intersection : public Geometry {
public:
    Intersection() = default;
    Intersection(Geometry* g1, Geometry* g2) :
        g1_(g1), g2_(g2) {}
    void accept(Visitor& v) override {
        g1_->accept(v);
        g2_->accept(v);
    }
private:
    std::unique_ptr<Geometry> g1_;
    std::unique_ptr<Geometry> g2_;
};

该对象的序列化很直接,将细节委托给包含的对象。但反序列化会失败,因为几何指针为空,尚未分配对象,也不知道应分配何种类型的对象。需要先在序列化流中编码对象类型,再根据这些编码类型构造对象,这可借助工厂模式解决。

3.4.1 定义类型标签和工厂构造函数
// Example 09
class Geometry {
public:
    enum type_tag {POINT = 100, CIRCLE, LINE, INTERSECTION};
    virtual type_tag tag() const = 0;
};
class Visitor {
public:
    static Geometry* make_geometry(Geometry::type_tag tag);
    virtual void visit(Geometry::type_tag& tag) = 0;
    ...
};

在每个派生的 Geometry 类中定义 tag() 方法,例如 Point 类:

// Example 09
class Point : public Geometry {
public:
    ...
    type_tag tag() const override { return POINT; }
};
3.4.2 定义工厂构造函数
// Example 09
Geometry* Visitor::make_geometry(Geometry::type_tag tag) {
    switch (tag) {
        case Geometry::POINT: return new Point;
        case Geometry::CIRCLE: return new Circle;
        case Geometry::LINE: return new Line;
        case Geometry::INTERSECTION: return new Intersection;
    }
}
3.4.3 序列化和反序列化类型标签
// Example 09
class Intersection : public Geometry {
public:
    void accept(Visitor& v) override {
        Geometry::type_tag tag;
        if (g1_) tag = g1_->tag();
        v.visit(tag);
        if (!g1_) g1_.reset(Visitor::make_geometry(tag));
        g1_->accept(v);
        if (g2_) tag = g2_->tag();
        v.visit(tag);
        if (!g2_) g2_.reset(Visitor::make_geometry(tag));
        g2_->accept(v);
    }
    ...
};
// Example 09
class StringSerializeVisitor : public Visitor {
public:
    void visit(Geometry::type_tag& tag) override {
        S << size_t(tag) << " ";
    }
    ...
};
// Example 09
class StringDeserializeVisitor : public Visitor {
public:
    void visit(Geometry::type_tag& tag) override {
        size_t t;
        S >> t;
        tag = Geometry::type_tag(t);
    }
    ...
};

3.5 无环访问者模式

3.5.1 传统访问者模式的问题

传统访问者模式虽能将算法实现与数据对象分离,允许根据运行时的具体对象类型和操作选择正确的实现,但存在一些问题。我们需维护两个平行的类层次结构(可访问对象和访问者),且两者之间的依赖关系复杂,形成循环依赖。每次向层次结构中添加新对象,都需更新每个访问者。此外,传统访问者模式必须处理对象类型和访问者类型的所有可能组合,即便某些组合无意义。

3.5.2 无环访问者模式的实现

无环访问者模式旨在打破依赖循环并允许部分访问。其基可访问类与常规访问者模式相同:

// Example 10
class Pet {
public:
    virtual ~Pet() {}
    virtual void accept(PetVisitor& v) = 0;
    ...
};

但基访问者类没有为每个可访问对象提供 visit() 重载,甚至没有 visit() 成员函数:

// Example 10
class PetVisitor {
public:
    virtual ~PetVisitor() {}
};

对于原始层次结构中的每个派生类,我们声明相应的访问者类,其中包含 visit() 函数:

// Example 10
class Cat;
class CatVisitor { 
public:
    virtual void visit(Cat* c) = 0;
};
class Cat : public Pet {
public:
    Cat(std::string_view color) : Pet(color) {}
    void accept(PetVisitor& v) override {
        if (CatVisitor* cv = dynamic_cast<CatVisitor*>(&v)) {
            cv->visit(this);
        } else { // Handle error
            assert(false);
        }
    }
};

每个访问者只能访问其设计的类。新的 accept() 函数会使用 dynamic_cast 检查访问者类型是否正确,若正确则接受访问,否则处理错误。具体访问者类需派生自公共的 PetVisitor 基类和特定类的基类,如 CatVisitor

// Example 10
class FeedingVisitor : public PetVisitor,
                       public CatVisitor,
                       public DogVisitor {
public:
    void visit(Cat* c) override {
        std::cout << "Feed tuna to the " << c->color()
                  << " cat" << std::endl;
    }
    void visit(Dog* d) override {
        std::cout << "Feed steak to the " << d->color()
                  << " dog" << std::endl;
    }
};

若访问者不设计用于访问层次结构中的某些类,可省略相应的访问者基类,无需实现虚函数重载:

// Example 10
class BathingVisitor : public PetVisitor,
                       public DogVisitor { // But no CatVisitor
public:
    void visit(Dog* d) override {
        std::cout << "Wash the " << d->color()
                  << " dog" << std::endl;
    }
    // No visit(Cat*) here!
};

无环访问者模式的调用方式与常规访问者模式相同:

// Example 10
std::unique_ptr<Pet> c(new Cat("orange"));
std::unique_ptr<Pet> d(new Dog("brown"));
FeedingVisitor fv;
c->accept(fv);
d->accept(fv);
BathingVisitor bv;
//c->accept(bv); // Error
d->accept(bv);

3.6 总结

本文详细介绍了访问者模式在多线程环境中的限制及解决方案,深入探讨了如何访问复杂对象,包括复合对象和二维几何对象的序列化与反序列化。同时,针对传统访问者模式的问题,引入了无环访问者模式并阐述其实现方式。通过这些内容,我们能更好地理解和应用访问者模式,处理复杂的对象访问和操作需求。

3.7 流程图

graph TD;
    A[开始] --> B[访问复杂对象];
    B --> C{是否为容器类};
    C -- 是 --> D[委托访问包含对象];
    C -- 否 --> E[按对象结构访问];
    D --> F[序列化/反序列化];
    E --> F;
    F --> G{是否已知对象类型};
    G -- 是 --> H[常规序列化/反序列化];
    G -- 否 --> I[编码类型标签并使用工厂模式];
    H --> J[结束];
    I --> J;

3.8 表格

模式 优点 缺点
传统访问者模式 分离算法与数据,支持双重分派 维护两个平行类层次结构,依赖循环,需处理所有组合
无环访问者模式 打破依赖循环,允许部分访问 需使用 dynamic_cast 进行类型检查

4. 访问者模式的应用场景总结

4.1 适用场景

  • 需要对不同类型对象进行多种操作 :当有一组不同类型的对象,并且需要对这些对象执行多种不同的操作时,使用访问者模式可以将操作的实现与对象本身分离。例如,在上述的宠物示例中,我们可以有喂食、洗澡等不同操作,通过访问者模式可以方便地添加新的操作,而无需修改宠物类的代码。
  • 对象结构相对稳定,但操作经常变化 :如果对象的结构(即对象的类型和层次结构)相对固定,而对这些对象的操作经常需要修改或扩展,访问者模式是一个很好的选择。因为可以通过创建新的访问者类来实现新的操作,而不会影响到对象本身的代码。
  • 需要对对象进行复杂的处理 :对于一些复杂的对象,如包含多个子对象的复合对象,使用访问者模式可以将处理逻辑委托给访问者,使代码更加清晰和易于维护。例如,在几何对象的序列化和反序列化中,通过访问者模式可以方便地处理不同类型的几何对象及其组合。

4.2 不适用场景

  • 对象结构经常变化 :如果对象的结构经常发生变化,每次添加新的对象类型都需要修改所有的访问者类,这会导致代码的维护成本增加,此时访问者模式可能不是最佳选择。
  • 操作简单且固定 :如果对对象的操作非常简单,并且不会经常变化,使用访问者模式可能会使代码变得过于复杂,增加不必要的开销。

5. 访问者模式与其他设计模式的比较

5.1 与适配器模式的比较

  • 适配器模式 :主要用于将一个类的接口转换成客户希望的另一个接口,使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。在上述的 Shelter 类示例中, Shelter 类本质上是一个适配器,它将 std::vector<std::unique_ptr<Pet>> 转换为可访问的对象,使得可以使用访问者模式对宠物对象进行操作。
  • 访问者模式 :重点在于将操作的实现与对象本身分离,允许在不修改对象类的情况下添加新的操作。虽然 Shelter 类使用了适配器模式来使容器可访问,但核心的操作逻辑是通过访问者模式实现的。

5.2 与工厂模式的比较

  • 工厂模式 :主要用于创建对象,将对象的创建过程封装起来,使得代码更加灵活和可维护。在处理未知对象类型的反序列化时,我们使用了工厂模式来根据类型标签创建相应的几何对象。例如, Visitor 类中的 make_geometry 静态函数就是一个工厂方法,根据传入的类型标签创建不同的几何对象。
  • 访问者模式 :侧重于对已创建的对象进行操作,而不是对象的创建。在序列化和反序列化过程中,访问者模式负责对对象进行具体的操作,如将对象转换为字符串或从字符串中恢复对象。

6. 访问者模式的性能考虑

6.1 优点

  • 可扩展性 :访问者模式具有良好的可扩展性,当需要添加新的操作时,只需要创建一个新的访问者类,而不需要修改现有的对象类。这使得代码的维护和扩展更加方便。
  • 代码复用 :不同的访问者可以共享相同的对象结构,提高了代码的复用性。例如,在宠物示例中, FeedingVisitor BathingVisitor 可以对相同的宠物对象进行不同的操作。

6.2 缺点

  • 类型检查开销 :在无环访问者模式中,需要使用 dynamic_cast 进行类型检查,这会带来一定的性能开销。特别是在频繁调用的情况下,可能会影响程序的性能。
  • 维护成本 :传统访问者模式需要维护两个平行的类层次结构,并且存在依赖循环,这会增加代码的维护成本。每次添加新的对象类型都需要更新所有的访问者类。

7. 总结与建议

7.1 总结

访问者模式是一种强大的设计模式,它可以将操作的实现与对象本身分离,使得代码更加灵活和可维护。通过本文的介绍,我们了解了访问者模式在多线程环境中的限制及解决方案,掌握了如何访问复杂对象,包括复合对象和二维几何对象的序列化与反序列化。同时,我们还学习了无环访问者模式,它可以打破传统访问者模式的依赖循环,允许部分访问。

7.2 建议

  • 选择合适的模式 :在实际应用中,需要根据具体的需求和场景选择合适的设计模式。如果需要对不同类型的对象进行多种操作,并且对象结构相对稳定,访问者模式是一个不错的选择。但如果对象结构经常变化,可能需要考虑其他设计模式。
  • 注意性能问题 :在使用无环访问者模式时,要注意 dynamic_cast 带来的性能开销。可以根据实际情况进行优化,例如减少不必要的类型检查。
  • 结合其他模式 :在处理复杂问题时,访问者模式可以与其他设计模式结合使用,如适配器模式和工厂模式,以提高代码的灵活性和可维护性。

8. 流程图

graph TD;
    A[需求分析] --> B{是否适用访问者模式};
    B -- 是 --> C[设计对象层次结构];
    B -- 否 --> D[选择其他模式];
    C --> E[创建访问者基类];
    E --> F[创建具体访问者类];
    F --> G[对象接受访问者];
    G --> H[执行操作];
    D --> I[结束];
    H --> I;

9. 表格

模式特点 传统访问者模式 无环访问者模式
类层次结构 两个平行类层次结构,依赖循环 打破依赖循环
操作扩展性 需修改所有访问者类 可部分访问,无需修改无关访问者类
类型检查 使用 dynamic_cast
代码维护成本 相对较低
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值