数据结构模式
常常有一些组件在内部具有特定的数据结构。如何让客户程序依赖这些特定的数据结构,将极大的破坏组件的复用。那么这个时候将这些特定数据结构封装在内部。在外部提供统一的接口来实现与特定数据结构无关的访问。是一种行之有效的解决方案。
典型模式
组合模式允许你将对象组合成树形结构来表示“部分-整体”的层次结构。组合模式使得客户端对单个对象和组合对象的使用具有一致性。
这张图是一个UML(统一建模语言)类图,用于展示软件系统中类之间的结构和关系。通过图形化的方式描述了类的属性、操作(方法)以及类之间的继承、关联等关系。
Client(客户端):
Component(组件):
Add(Component)
: 添加一个子组件。Remove(Component)
: 移除一个子组件。GetChild(int)
: 根据索引获取子组件。children
,用于存储子组件的集合,尽管这个属性在图中没有明确标出,但根据UML的惯例和类的操作可以推断出来。Leaf(叶子节点):
forall g in children
的操作,这实际上是一个伪代码或注释,因为叶子节点没有子节点(children
为空或不存在),所以这个操作在叶子节点上下文中不适用。这里的展示可能只是为了强调Leaf类继承自Component类,并保留了Component的接口结构。Composite(复合节点):
g
的操作(g.Operation():
),这里g
代表了一个子组件的实例,这个注释或伪代码表明Composite类可以对其子节点执行某种操作,但没有具体说明是什么操作,这取决于实际的应用场景。这张UML类图展示了一个典型的组合模式(Composite Pattern)的结构,其中Component
是一个抽象类,代表了一个具有共同接口的对象,这个接口允许在组件的单个对象和组合对象之间进行一致的操作。Client
类展示了如何使用这个结构,而Leaf
和Composite
类则分别代表了结构中的叶子节点和复合节点。通过这种方式,系统可以以统一的方式处理单个对象和组合对象,简化了客户端代码并提高了系统的可扩展性。
以下是一个使用组合模式的C++代码示例,模拟一个文件系统,其中目录可以包含文件或其他子目录。
抽象组件类:
#include
#include
#include
// 抽象组件类,表示文件系统的节点
class FileSystemComponent {
public:
virtual void showDetails(int indent = 0) const = 0;
virtual void add(FileSystemComponent* component) {
throw std::runtime_error("Cannot add to a leaf component");
}
virtual void remove(FileSystemComponent* component) {
throw std::runtime_error("Cannot remove from a leaf component");
}
virtual ~FileSystemComponent() = default;
};
叶子节点类(文件):
class File : public FileSystemComponent {
private:
std::string name;
public:
File(const std::string& name) : name(name) {}
void showDetails(int indent = 0) const override {
std::cout << std::string(indent, ' ') << name << std::endl;
}
};
组合节点类(目录):
class Directory : public FileSystemComponent {
private:
std::string name;
std::vector<FileSystemComponent*> components;
public:
Directory(const std::string& name) : name(name) {}
void add(FileSystemComponent* component) override {
components.push_back(component);
}
void remove(FileSystemComponent* component) override {
components.erase(std::remove(components.begin(), components.end(), component), components.end());
}
void showDetails(int indent = 0) const override {
std::cout << std::string(indent, ' ') << name << "/" << std::endl;
for (const auto& component : components) {
component->showDetails(indent + 2);
}
}
~Directory() {
for (auto component : components) {
delete component;
}
}
};
客户端代码:
int main() {
FileSystemComponent* rootDir = new Directory("root");
FileSystemComponent* homeDir = new Directory("home");
FileSystemComponent* userDir = new Directory("user");
FileSystemComponent* file1 = new File("file1.txt");
FileSystemComponent* file2 = new File("file2.txt");
FileSystemComponent* file3 = new File("file3.txt");
rootDir->add(homeDir);
homeDir->add(userDir);
userDir->add(file1);
userDir->add(file2);
homeDir->add(file3);
rootDir->showDetails();
delete rootDir;
return 0;
}
运行结果:
root/
home/
user/
file1.txt
file2.txt
file3.txt
优点:
缺点:
组合模式在以下场景中应用较多:
在组合模式中,叶子节点通常不需要实现(即重载)Add(Component)
, Remove(Component)
, GetChild(int)
这三个方法,因为叶子节点不包含子节点。这些方法主要用于组合节点(Composite)以便管理子节点。但有时,为了简化代码或提高灵活性,叶子节点也可能会实现这些方法。以下是叶子节点重载与不重载这三个方法的优缺点对比。
实现方式:
在叶子节点中,这些方法通常被声明但不实现(在C++中通常可以抛出异常或者是空实现)。叶子节点不需要管理子组件。
class Leaf : public Component {
public:
void Add(Component* component) override {
throw std::runtime_error("Leaf nodes do not support Add operation");
}
void Remove(Component* component) override {
throw std::runtime_error("Leaf nodes do not support Remove operation");
}
Component* GetChild(int index) override {
throw std::runtime_error("Leaf nodes do not support GetChild operation");
}
void Operation() override {
// 具体叶子节点的操作实现
}
};
优点:
缺点:
实现方式:
叶子节点实现(重载)这些方法,但不执行任何操作或返回特定值,如nullptr
。
class Leaf : public Component {
public:
void Add(Component* component) override {
// 叶子节点不支持添加操作,但实现了这个方法
}
void Remove(Component* component) override {
// 叶子节点不支持移除操作,但实现了这个方法
}
Component* GetChild(int index) override {
return nullptr; // 叶子节点没有子节点,返回空指针
}
void Operation() override {
// 具体叶子节点的操作实现
}
};
优点:
Add
, Remove
, GetChild
,简化了代码逻辑。缺点:
Add
和Remove
),可能导致误用而不易发现。不重载方法的情况:适用于严格遵循职责分离原则的场景。通过不重载方法,明确区分了叶子节点和组合节点的职责,使得代码更清晰,类型安全性更高。这种方式适合对系统稳定性和安全性要求较高的场合,或在需要明确捕获误用场景的应用中使用。
重载方法的情况:适用于追求客户端代码简单性和一致性的场景。通过重载这些方法,客户端不需要区分叶子节点和组合节点,统一处理所有组件,减少了代码的复杂性。这种方式适合在系统中灵活性要求较高、且不易出错的场合。
综上,选择是否重载这些方法取决于具体应用的需求、开发团队的编码习惯和系统的复杂性。如果系统需要严格的职责区分和类型安全性,建议不重载这些方法;如果系统追求统一性和简洁性,可以考虑重载这些方法。