前言
在设计库时,如何最好地为用户提供类的实例化方式。这通常取决于你的库设计和特定类的功能。这里有一些常见的方法:
- 直接实例化:对于一些简单的类,用户可以直接创建实例。例如:
MyClass obj;
- 如果类需要构造参数,用户需要提供它们:
MyClass obj(arg1, arg2);
- 工厂函数/方法:如果类的创建过程比较复杂,或者你想隐藏某些实现细节,可以提供工厂函数或方法。工厂函数是一个返回类实例的函数,工厂方法则是返回类实例的类方法。例如:
std::shared_ptr<MyClass> obj = MyClass::create(arg1, arg2);
- 在这个例子中,
MyClass::create
是一个静态工厂方法,它返回一个MyClass
的shared_ptr
。 - 单例模式:如果你的类只应该有一个实例,可以使用单例模式。在这种情况下,你通常会提供一个方法来获取类的唯一实例:
MyClass& obj = MyClass::getInstance();
- 这里的
MyClass::getInstance
是一个静态方法,它返回MyClass
的引用。 - 建造者模式:如果类有许多可选的或者需要以特定顺序设置的参数,你可以使用建造者模式。在这种情况下,你会提供一个“建造者”类,用户可以使用它来构建你的类的实例:
MyClass obj = MyClass::Builder().setArg1(arg1).setArg2(arg2).build();
- 这里,
MyClass::Builder
是一个内部类,它提供了一系列方法来设置MyClass
的参数,最后调用build
方法来创建MyClass
的实例。
每种方法都有其适用的场景,你应该根据你的类的特性和用户的需求来选择最合适的方法。
工厂函数/方法
工厂模式是一种常见的设计模式,它提供了一个接口用于创建对象,而无需指定对象的具体类。这种模式通常用于以下场景:
- 当创建对象的过程比较复杂或者消耗资源时,例如需要读取配置文件或者初始化大量数据。
- 当你想在创建对象时提供一些额外的逻辑,例如缓存已经创建的对象或者返回一些特定的子类实例。
- 当你想隐藏类的具体实现时,例如只提供一个接口或者抽象基类,并返回其具体子类的实例。
在C++中,工厂模式可以通过函数或者方法来实现。这里有一些例子:
工厂函数:
工厂函数是一个全局函数,它返回一个类的实例或者指针。例如:
std::unique_ptr<MyClass> createMyClass(int arg1, int arg2) { return std::make_unique<MyClass>(arg1, arg2); }
在这个例子中,createMyClass
是一个工厂函数,它接受两个参数并返回一个MyClass
的unique_ptr
。
工厂方法:
工厂方法是一个类的静态成员函数,它返回一个类的实例或者指针。例如:
class MyClass { public: static std::shared_ptr<MyClass> create(int arg1, int arg2) { return std::shared_ptr<MyClass>(new MyClass(arg1, arg2)); } private: MyClass(int arg1, int arg2) { // Initialization logic } };
在这个例子中,MyClass::create
是一个静态工厂方法,它接受两个参数并返回一个MyClass
的shared_ptr
。注意MyClass
的构造函数是私有的,这意味着用户只能通过create
方法来创建MyClass
的实例。
工厂模式的主要优点是它可以提供更多的灵活性和控制权。然而,它也有一些缺点,例如它可能使代码变得更复杂,而且如果类有许多可选参数,使用工厂模式可能会使代码变得难以阅读和理解。在选择是否使用工厂模式时,你应该根据你的具体需求来决定。
每个派生类创建不同的库
如果你打算为每个派生类创建不同的库,并且希望用户通过同一接口(基类)来使用它们,那么你可以使用“接口+工厂函数/方法”的方式。
假设我们有一个Animal
基类和多个派生类(Dog
, Cat
, 等等),每个派生类都是单例。你可以在每个派生类中实现单例模式,并提供一个静态方法来获取该类的唯一实例。
然后,你可以在每个库中提供一个工厂函数,该函数返回基类的指针或引用。例如,对于Dog
库,你可以提供以下工厂函数:
extern "C" Animal& createAnimal() { return Dog::getInstance(); }
在这个例子中,createAnimal
函数返回Dog
单例的引用。请注意,我们使用了extern "C"
来确保函数的C链接,这样在动态加载库时就可以通过函数名来找到这个函数。
然后,用户可以动态加载你的库,并通过createAnimal
函数来获取Animal
的实例。例如,使用dlopen
和dlsym
函数(在dlfcn.h
头文件中):
#include <dlfcn.h> // Load the library void* handle = dlopen("/path/to/libDog.so", RTLD_LAZY); if (!handle) { // Handle error } // Get the createAnimal function typedef Animal& (*CreateAnimalFunc)(); CreateAnimalFunc createAnimal = (CreateAnimalFunc) dlsym(handle, "createAnimal"); if (!createAnimal) { // Handle error } // Use the Animal Animal& myAnimal = createAnimal(); myAnimal.speak(); // Outputs "Woof!"
在这个例子中,用户动态加载了Dog
库,并通过createAnimal
函数获取了Dog
的单例。尽管用户直接与Dog
类交互,但他们只需要知道Animal
接口,因此你的库仍然保持了多态性。
这种设计方式的优点是具有很好的灵活性和扩展性。你可以为每个动物类型提供一个库,用户可以选择加载哪个库,而不需要更改他们的代码。然而,它的缺点是需要用户动态加载库和查找函数,这可能会增加用户的使用复杂性。同时,你需要确保你的库在所有目标平台上都可用,因为动态加载库的方式在不同的平台上可能会有所不同。
建造者模式
建造者模式(Builder Pattern)是一种对象创建软件设计模式,它旨在找到一种解决方案来处理具有多个参数的对象,其中有些是必需的,有些是可选的。这种模式可以使得构造过程可以灵活地添加新的参数而不破坏已有的代码,使得代码更加易读易写。
一个典型的建造者模式实现包括以下几个部分:
- Builder类:Builder类包含了创建复杂对象所需的所有步骤。Builder类通常是主对象的内部类,但也可以是独立的类。Builder类通常有一系列设置参数的方法(通常返回Builder自身以便于链式调用)和一个创建主对象的方法。
- 主类:这是你希望用户创建的对象。通常,主类的构造函数是私有的或受保护的,只有Builder类可以访问。主类通常有一个静态方法来创建Builder类的实例。
下面是一个C++的建造者模式的示例:
class MyClass { public: class Builder; private: int param1; int param2; MyClass(int param1, int param2) : param1(param1), param2(param2) {} public: class Builder { private: int param1; int param2; public: Builder& setParam1(int param1) { this->param1 = param1; return *this; } Builder& setParam2(int param2) { this->param2 = param2; return *this; } MyClass build() { return MyClass(param1, param2); } }; };
然后,用户可以使用如下方式来创建MyClass
的实例:
MyClass obj = MyClass::Builder().setParam1(1).setParam2(2).build();
在这个例子中,MyClass::Builder
是一个内部类,它有两个设置参数的方法(setParam1
和setParam2
)和一个创建MyClass
实例的方法(build
)。主类MyClass
的构造函数是私有的,只有Builder
类可以访问。
建造者模式的优点是可以使代码更加清晰,减少错误的可能性,特别是当对象有许多参数,或者构造过程复杂的时候。然而,它也会使代码更加复杂,增加了维护的难度。因此,你应该在需要时才使用建造者模式。
单例模式和多态
单例模式
可以直接为基类实现单例模式,用户可以通过获取基类的单例来使用你的库。例如:
class Animal { public: static Animal& getInstance() { static Animal instance; return instance; } virtual void speak() const { std::cout << "Animal sound!\n"; } private: Animal() = default; // private constructor Animal(const Animal&) = delete; // no copy Animal& operator=(const Animal&) = delete; // no copy assignment };
然后,用户可以通过以下方式获取Animal
的单例并使用它:
Animal& myAnimal = Animal::getInstance(); myAnimal.speak(); // Outputs "Animal sound!"
这种设计方式的优点是简单明了,用户只需要与一个类进行交互。然而,它的缺点是缺乏灵活性和扩展性。如果你在未来需要添加更多的动物类型,你可能需要重构你的代码以支持多态。
另外,值得注意的是,使用单例模式时需要特别小心。单例模式可能会导致代码之间的高度耦合,使得单元测试变得困难,并可能引发多线程问题。在选择使用单例模式时,你应该权衡其优缺点,并确保它适合你的需求。
多态的实现
如果你的设计需要通过基类来公开一个多态接口,那么你可以将基类设计为接口,即只包含纯虚函数,并不能直接实例化。然后提供一个或多个派生类的实现。用户只需要知道接口(基类),而具体实现(派生类)可以隐藏在库内部。这种方式被称为 “面向接口编程”。
例如,假设你有一个基类Animal
,还有多个派生类Dog
、Cat
等。你可以这样设计:
class Animal { public: virtual ~Animal() = default; virtual void speak() const = 0; }; class Dog : public Animal { public: void speak() const override { std::cout << "Woof!\n"; } }; class Cat : public Animal { public: void speak() const override { std::cout << "Meow!\n"; } };
然后,你可以提供工厂函数或者工厂方法来创建特定的动物类型:
std::unique_ptr<Animal> createAnimal(AnimalType type) { switch (type) { case AnimalType::Dog: return std::make_unique<Dog>(); case AnimalType::Cat: return std::make_unique<Cat>(); // ... } }
这样,用户就可以通过Animal
接口和createAnimal
工厂函数来使用你的库,而不需要知道具体的派生类:
std::unique_ptr<Animal> myAnimal = createAnimal(AnimalType::Dog); myAnimal->speak(); // Outputs "Woof!"
这种设计方式的优点是具有很好的灵活性和扩展性。你可以在不改变Animal
接口的情况下添加更多的动物类型,用户的代码也不需要做任何修改。同时,通过使用智能指针(例如std::unique_ptr
或std::shared_ptr
),你可以确保动态分配的对象能够正确地进行内存管理。
多态与单例模式的设计
以下是我从七个方面对这三种选择(基类为单例,派生类为单例,或两者都为单例)的分析:
- 设计复杂性:
- 基类为单例:设计相对简单,但可能限制了派生类的某些功能。
- 派生类为单例:设计更复杂,需要在每个派生类中实现单例模式。
- 都为单例:设计最复杂,需要在基类和所有派生类中都实现单例模式。
- 代码重复:
- 基类为单例:无需在每个派生类中都实现单例模式,可以减少代码重复。
- 派生类为单例:可能需要在每个派生类中都实现单例模式,可能导致代码重复。
- 都为单例:需要在基类和所有派生类中都实现单例模式,可能导致大量的代码重复。
- 灵活性:
- 基类为单例:灵活性较低,所有派生类都必须是单例。
- 派生类为单例:灵活性较高,可以选择哪些派生类应该是单例。
- 都为单例:灵活性较低,所有类都必须是单例。
- 可扩展性:
- 基类为单例:如果以后需要添加非单例的派生类,可能需要修改基类的设计。
- 派生类为单例:可以很容易地添加新的单例或非单例派生类。
- 都为单例:如果以后需要添加非单例的类,可能需要修改基类和所有派生类的设计。
- 可维护性:
- 基类为单例:只需要维护基类的单例实现。
- 派生类为单例:需要维护所有派生类的单例实现。
- 都为单例:需要维护基类和所有派生类的单例实现。
- 内存使用:
- 基类为单例:只需要存储一个基类的实例。
- 派生类为单例:需要存储每个派生类的一个实例。
- 都为单例:需要存储基类和所有派生类的实例。
如何优雅地在C++库中实现实例化设计(二)https://developer.aliyun.com/article/1464311