C++ 访问者模式:对复杂对象结构进行并发操作

好的,没问题。

C++ 访问者模式:对复杂对象结构进行并发操作

各位朋友,大家好!今天咱们聊聊C++中的访问者模式。这玩意儿听起来高大上,但其实没那么可怕。想象一下,你有一个动物园,里面有各种各样的动物:狮子、老虎、猴子、大象等等。现在你需要对这些动物做一些操作,比如喂食、打扫卫生、检查健康状况。如果你为每种动物都写一个函数,那代码岂不是要爆炸?这时候,访问者模式就派上用场了。它可以让你在不修改动物类的前提下,对它们进行各种操作。更厉害的是,它还能方便地进行并发操作,让你的程序跑得更快。

什么是访问者模式?

简单来说,访问者模式是一种行为型设计模式,它允许你定义一个新的操作而不需要修改对象的结构。它将数据结构与数据操作分离,使得你可以在不修改对象结构的情况下,定义作用于这些对象的新操作。

访问者模式的核心角色

  • Element(元素): 定义一个 accept() 方法,接受访问者对象的访问。在我们的例子中,动物园里的每种动物都是一个元素。
  • ConcreteElement(具体元素): 实现 accept() 方法,通常会将访问者对象作为参数传递给该方法。例如,狮子类、老虎类等。
  • Visitor(访问者): 定义一个访问每个具体元素的接口。每个具体元素都需要有一个对应的 visit() 方法。
  • ConcreteVisitor(具体访问者): 实现访问者接口,定义对每个具体元素的操作。例如,喂食访问者、清洁访问者、健康检查访问者。
  • ObjectStructure(对象结构): 包含元素的集合,并提供遍历这些元素的方法。在我们的例子中,动物园就是一个对象结构。

访问者模式的UML类图

@startuml
class Element {
    + accept(visitor: Visitor)
}

class ConcreteElementA {
    + accept(visitor: Visitor)
}

class ConcreteElementB {
    + accept(visitor: Visitor)
}

interface Visitor {
    + visit(element: ConcreteElementA)
    + visit(element: ConcreteElementB)
}

class ConcreteVisitorA {
    + visit(element: ConcreteElementA)
    + visit(element: ConcreteElementB)
}

class ConcreteVisitorB {
    + visit(element: ConcreteElementA)
    + visit(element: ConcreteElementB)
}

class ObjectStructure {
    + add(element: Element)
    + remove(element: Element)
    + accept(visitor: Visitor)
}

Element <|-- ConcreteElementA
Element <|-- ConcreteElementB
Visitor <|-- ConcreteVisitorA
Visitor <|-- ConcreteVisitorB
ObjectStructure --* Element : contains
Element -- Visitor : accept >

@enduml

代码示例:动物园的例子

#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
#include <thread>
#include <mutex>

// 前置声明
class Animal;
class Lion;
class Tiger;
class Monkey;

// 访问者接口
class AnimalVisitor {
public:
    virtual void visit(Lion* lion) = 0;
    virtual void visit(Tiger* tiger) = 0;
    virtual void visit(Monkey* monkey) = 0;
    virtual ~AnimalVisitor() {}
};

// 元素接口
class Animal {
public:
    virtual void accept(AnimalVisitor* visitor) = 0;
    virtual std::string getName() const = 0;
    virtual ~Animal() {}
};

// 具体元素:狮子
class Lion : public Animal {
private:
    std::string name;
public:
    Lion(std::string name) : name(name) {}
    void accept(AnimalVisitor* visitor) override {
        visitor->visit(this);
    }
    std::string getName() const override {
        return name;
    }
};

// 具体元素:老虎
class Tiger : public Animal {
private:
    std::string name;
public:
    Tiger(std::string name) : name(name) {}
    void accept(AnimalVisitor* visitor) override {
        visitor->visit(this);
    }
    std::string getName() const override {
        return name;
    }
};

// 具体元素:猴子
class Monkey : public Animal {
private:
    std::string name;
public:
    Monkey(std::string name) : name(name) {}
    void accept(AnimalVisitor* visitor) override {
        visitor->visit(this);
    }
    std::string getName() const override {
        return name;
    }
};

// 具体访问者:喂食
class FeedingVisitor : public AnimalVisitor {
public:
    void visit(Lion* lion) override {
        std::cout << "Feeding " << lion->getName() << " with meat." << std::endl;
    }
    void visit(Tiger* tiger) override {
        std::cout << "Feeding " << tiger->getName() << " with meat." << std::endl;
    }
    void visit(Monkey* monkey) override {
        std::cout << "Feeding " << monkey->getName() << " with bananas." << std::endl;
    }
};

// 具体访问者:清洁
class CleaningVisitor : public AnimalVisitor {
public:
    void visit(Lion* lion) override {
        std::cout << "Cleaning " << lion->getName() << "'s cage." << std::endl;
    }
    void visit(Tiger* tiger) override {
        std::cout << "Cleaning " << tiger->getName() << "'s cage." << std::endl;
    }
    void visit(Monkey* monkey) override {
        std::cout << "Cleaning " << monkey->getName() << "'s cage." << std::endl;
    }
};

// 对象结构:动物园
class Zoo {
private:
    std::vector<Animal*> animals;
    std::mutex mtx; // 互斥锁,用于线程安全
public:
    void addAnimal(Animal* animal) {
        std::lock_guard<std::mutex> lock(mtx); // 加锁
        animals.push_back(animal);
    }

    void removeAnimal(Animal* animal) {
        std::lock_guard<std::mutex> lock(mtx); // 加锁
        auto it = std::remove(animals.begin(), animals.end(), animal);
        animals.erase(it, animals.end());
    }

    void accept(AnimalVisitor* visitor) {
        std::vector<std::thread> threads;
        for (Animal* animal : animals) {
            threads.emplace_back([animal, visitor, this]() {
                std::lock_guard<std::mutex> lock(mtx); // 加锁,保证访问的原子性
                animal->accept(visitor);
            });
        }

        for (auto& thread : threads) {
            thread.join();
        }
    }
};

int main() {
    Zoo* zoo = new Zoo();
    zoo->addAnimal(new Lion("Simba"));
    zoo->addAnimal(new Tiger("Rajah"));
    zoo->addAnimal(new Monkey("Abu"));

    FeedingVisitor* feedingVisitor = new FeedingVisitor();
    CleaningVisitor* cleaningVisitor = new CleaningVisitor();

    std::cout << "Feeding the animals:" << std::endl;
    zoo->accept(feedingVisitor);

    std::cout << "nCleaning the cages:" << std::endl;
    zoo->accept(cleaningVisitor);

    delete feedingVisitor;
    delete cleaningVisitor;
    // Properly delete the animals and the zoo to prevent memory leaks
    for (Animal* animal : zoo->animals) {
        delete animal;
    }
    delete zoo;

    return 0;
}

代码解释

  • AnimalVisitor 接口: 定义了 visit() 方法,用于访问不同类型的动物。
  • Animal 接口: 定义了 accept() 方法,用于接受访问者。
  • LionTigerMonkey 类: 实现了 Animal 接口,并实现了 accept() 方法,将自身传递给访问者。
  • FeedingVisitorCleaningVisitor 类: 实现了 AnimalVisitor 接口,定义了对不同类型动物的喂食和清洁操作。
  • Zoo 类: 维护了一个动物列表,并提供 accept() 方法,用于遍历动物列表,并让每个动物接受访问者的访问。 这里使用了互斥锁std::mutex mtx; 来保证线程安全。

并发操作的实现

在上面的代码中,Zoo::accept() 方法使用了 std::thread 来为每个动物创建一个线程,从而实现并发操作。每个线程都会调用动物的 accept() 方法,并将访问者对象传递给它。这样,就可以同时对多个动物进行操作,提高程序的效率。

线程安全

由于多个线程可能会同时访问动物列表,因此需要保证线程安全。在上面的代码中,我使用了 std::mutex 来保护动物列表的访问。在添加、删除动物以及访问动物时,都需要先获取锁,才能进行操作。这样可以避免多个线程同时修改动物列表,导致数据竞争和程序崩溃。 尤其是在lambda 表达式中使用std::lock_guard<std::mutex> lock(mtx);, 保证了在访问animal之前先获得锁。

访问者模式的优点

  • 符合单一职责原则: 将数据结构与数据操作分离,使得每个类只负责自己的职责。
  • 符合开闭原则: 可以在不修改对象结构的情况下,定义新的操作。
  • 易于扩展: 可以方便地添加新的访问者和新的元素。
  • 可以对对象结构进行并发操作: 通过多线程可以提高程序的效率。

访问者模式的缺点

  • 当元素类型发生变化时,需要修改访问者接口及其所有实现类。 这可能会导致代码的维护成本增加。
  • 可能会破坏封装性: 访问者需要访问元素的内部状态才能进行操作,这可能会破坏元素的封装性。 不过可以通过友元解决。

什么时候使用访问者模式?

  • 当需要对一个对象结构中的对象进行很多不同的并且不相关的操作时。
  • 当需要动态地添加新的操作,而不想修改对象结构时。
  • 当对象结构中的对象类型很少改变,但需要经常添加新的操作时。

访问者模式的应用场景

  • 编译器: 编译器需要对抽象语法树进行各种操作,如类型检查、代码优化、代码生成等。
  • 图形编辑器: 图形编辑器需要对图形对象进行各种操作,如绘制、旋转、缩放、填充等。
  • 数据库查询: 数据库查询需要对查询树进行各种操作,如查询优化、索引选择、数据提取等。
  • 报表生成: 报表生成需要对数据对象进行各种操作,如数据汇总、数据格式化、数据展示等。

并发访问的注意事项

在使用访问者模式进行并发操作时,需要注意以下几点:

  • 线程安全: 确保对共享数据的访问是线程安全的,可以使用互斥锁、读写锁等机制来保护共享数据。
  • 避免死锁: 当多个线程需要同时获取多个锁时,可能会发生死锁。需要 carefully 设计锁的获取顺序,避免死锁的发生。
  • 性能优化: 并发操作可以提高程序的效率,但也可能会带来额外的开销,如线程创建、线程切换、锁竞争等。需要根据实际情况进行性能优化,选择合适的并发策略。

总结

访问者模式是一种强大的设计模式,它可以让你在不修改对象结构的情况下,对它们进行各种操作。通过并发操作,还可以提高程序的效率。当然,使用访问者模式也需要注意一些问题,如线程安全、避免死锁、性能优化等。

希望今天的讲解对大家有所帮助!如果大家有什么问题,可以随时提问。谢谢大家!

表格:访问者模式的优缺点对比

优点 缺点
符合单一职责原则,将数据结构与数据操作分离 当元素类型发生变化时,需要修改访问者接口及其所有实现类,维护成本增加
符合开闭原则,可以在不修改对象结构的情况下,定义新的操作 可能会破坏封装性,访问者需要访问元素的内部状态才能进行操作
易于扩展,可以方便地添加新的访问者和新的元素 如果对象结构变化频繁, 那么每次都要修改Visitor接口以及相关的实现类,成本很高。
可以对对象结构进行并发操作,通过多线程可以提高程序的效率 如果对象结构比较简单, 使用Visitor模式可能会显得过于复杂

补充说明:如何解决封装性问题

访问者模式可能会破坏封装性,因为访问者需要访问元素的内部状态才能进行操作。为了解决这个问题,可以使用友元函数或友元类。

  • 友元函数: 在元素类中声明访问者类为友元类,这样访问者类就可以访问元素类的私有成员。
  • 友元类: 在元素类中声明访问者类为友元类,这样访问者类就可以访问元素类的私有成员。
// 元素类
class Element {
private:
    int internalState;
    friend class ConcreteVisitor; // 声明 ConcreteVisitor 为友元类
public:
    Element(int state) : internalState(state) {}
    void accept(Visitor* visitor);
};

// 访问者类
class ConcreteVisitor {
public:
    void visit(Element* element) {
        // 可以访问 element 的私有成员 internalState
        std::cout << "Internal state: " << element->internalState << std::endl;
    }
};

总结的总结

希望通过这个动物园的例子,大家能够更深入地理解访问者模式。记住,设计模式不是银弹,要根据实际情况选择合适的模式。 祝大家编程愉快!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注