如何优雅地在C++库中实现实例化设计(一)

简介: 如何优雅地在C++库中实现实例化设计

前言

在设计库时,如何最好地为用户提供类的实例化方式。这通常取决于你的库设计和特定类的功能。这里有一些常见的方法:

  1. 直接实例化:对于一些简单的类,用户可以直接创建实例。例如:
MyClass obj;
  1. 如果类需要构造参数,用户需要提供它们:
MyClass obj(arg1, arg2);
  1. 工厂函数/方法:如果类的创建过程比较复杂,或者你想隐藏某些实现细节,可以提供工厂函数或方法。工厂函数是一个返回类实例的函数,工厂方法则是返回类实例的类方法。例如:
std::shared_ptr<MyClass> obj = MyClass::create(arg1, arg2);
  1. 在这个例子中,MyClass::create是一个静态工厂方法,它返回一个MyClassshared_ptr
  2. 单例模式:如果你的类只应该有一个实例,可以使用单例模式。在这种情况下,你通常会提供一个方法来获取类的唯一实例:
MyClass& obj = MyClass::getInstance();
  1. 这里的MyClass::getInstance是一个静态方法,它返回MyClass的引用。
  2. 建造者模式:如果类有许多可选的或者需要以特定顺序设置的参数,你可以使用建造者模式。在这种情况下,你会提供一个“建造者”类,用户可以使用它来构建你的类的实例:
MyClass obj = MyClass::Builder().setArg1(arg1).setArg2(arg2).build();
  1. 这里,MyClass::Builder是一个内部类,它提供了一系列方法来设置MyClass的参数,最后调用build方法来创建MyClass的实例。

每种方法都有其适用的场景,你应该根据你的类的特性和用户的需求来选择最合适的方法。

工厂函数/方法

工厂模式是一种常见的设计模式,它提供了一个接口用于创建对象,而无需指定对象的具体类。这种模式通常用于以下场景:

  1. 当创建对象的过程比较复杂或者消耗资源时,例如需要读取配置文件或者初始化大量数据。
  2. 当你想在创建对象时提供一些额外的逻辑,例如缓存已经创建的对象或者返回一些特定的子类实例。
  3. 当你想隐藏类的具体实现时,例如只提供一个接口或者抽象基类,并返回其具体子类的实例。

在C++中,工厂模式可以通过函数或者方法来实现。这里有一些例子:

工厂函数

工厂函数是一个全局函数,它返回一个类的实例或者指针。例如:

std::unique_ptr<MyClass> createMyClass(int arg1, int arg2) {
    return std::make_unique<MyClass>(arg1, arg2);
}

在这个例子中,createMyClass是一个工厂函数,它接受两个参数并返回一个MyClassunique_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是一个静态工厂方法,它接受两个参数并返回一个MyClassshared_ptr。注意MyClass的构造函数是私有的,这意味着用户只能通过create方法来创建MyClass的实例。

工厂模式的主要优点是它可以提供更多的灵活性和控制权。然而,它也有一些缺点,例如它可能使代码变得更复杂,而且如果类有许多可选参数,使用工厂模式可能会使代码变得难以阅读和理解。在选择是否使用工厂模式时,你应该根据你的具体需求来决定。

每个派生类创建不同的库

如果你打算为每个派生类创建不同的库,并且希望用户通过同一接口(基类)来使用它们,那么你可以使用“接口+工厂函数/方法”的方式。

假设我们有一个Animal基类和多个派生类(Dog, Cat, 等等),每个派生类都是单例。你可以在每个派生类中实现单例模式,并提供一个静态方法来获取该类的唯一实例。

然后,你可以在每个库中提供一个工厂函数,该函数返回基类的指针或引用。例如,对于Dog库,你可以提供以下工厂函数:

extern "C" Animal& createAnimal() {
    return Dog::getInstance();
}

在这个例子中,createAnimal函数返回Dog单例的引用。请注意,我们使用了extern "C"来确保函数的C链接,这样在动态加载库时就可以通过函数名来找到这个函数。

然后,用户可以动态加载你的库,并通过createAnimal函数来获取Animal的实例。例如,使用dlopendlsym函数(在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)是一种对象创建软件设计模式,它旨在找到一种解决方案来处理具有多个参数的对象,其中有些是必需的,有些是可选的。这种模式可以使得构造过程可以灵活地添加新的参数而不破坏已有的代码,使得代码更加易读易写。

一个典型的建造者模式实现包括以下几个部分:

  1. Builder类:Builder类包含了创建复杂对象所需的所有步骤。Builder类通常是主对象的内部类,但也可以是独立的类。Builder类通常有一系列设置参数的方法(通常返回Builder自身以便于链式调用)和一个创建主对象的方法。
  2. 主类:这是你希望用户创建的对象。通常,主类的构造函数是私有的或受保护的,只有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是一个内部类,它有两个设置参数的方法(setParam1setParam2)和一个创建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,还有多个派生类DogCat等。你可以这样设计:

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_ptrstd::shared_ptr),你可以确保动态分配的对象能够正确地进行内存管理。

多态与单例模式的设计

以下是我从七个方面对这三种选择(基类为单例,派生类为单例,或两者都为单例)的分析:

  1. 设计复杂性
  • 基类为单例:设计相对简单,但可能限制了派生类的某些功能。
  • 派生类为单例:设计更复杂,需要在每个派生类中实现单例模式。
  • 都为单例:设计最复杂,需要在基类和所有派生类中都实现单例模式。
  1. 代码重复
  • 基类为单例:无需在每个派生类中都实现单例模式,可以减少代码重复。
  • 派生类为单例:可能需要在每个派生类中都实现单例模式,可能导致代码重复。
  • 都为单例:需要在基类和所有派生类中都实现单例模式,可能导致大量的代码重复。
  1. 灵活性
  • 基类为单例:灵活性较低,所有派生类都必须是单例。
  • 派生类为单例:灵活性较高,可以选择哪些派生类应该是单例。
  • 都为单例:灵活性较低,所有类都必须是单例。
  1. 可扩展性
  • 基类为单例:如果以后需要添加非单例的派生类,可能需要修改基类的设计。
  • 派生类为单例:可以很容易地添加新的单例或非单例派生类。
  • 都为单例:如果以后需要添加非单例的类,可能需要修改基类和所有派生类的设计。
  1. 可维护性
  • 基类为单例:只需要维护基类的单例实现。
  • 派生类为单例:需要维护所有派生类的单例实现。
  • 都为单例:需要维护基类和所有派生类的单例实现。
  1. 内存使用
  • 基类为单例:只需要存储一个基类的实例。
  • 派生类为单例:需要存储每个派生类的一个实例。
  • 都为单例:需要存储基类和所有派生类的实例。

如何优雅地在C++库中实现实例化设计(二)https://developer.aliyun.com/article/1464311

目录
相关文章
|
2月前
|
存储 安全 C++
如何优雅地在C++库中实现实例化设计(二)
如何优雅地在C++库中实现实例化设计
20 0
|
8月前
|
存储 编译器 程序员
【C++】类与对象(一)类的定义 访问限定符 类的实例化 this指针
【C++】类与对象(一)类的定义 访问限定符 类的实例化 this指针
|
2月前
|
存储 编译器 C++
【C++成长记】C++入门 | 类和对象(上) |类的作用域、类的实例化、类的对象大小的计算、类成员函数的this指针
【C++成长记】C++入门 | 类和对象(上) |类的作用域、类的实例化、类的对象大小的计算、类成员函数的this指针
|
2月前
|
存储 编译器 程序员
【C++】类和对象①(什么是面向对象 | 类的定义 | 类的访问限定符及封装 | 类的作用域和实例化 | 类对象的存储方式 | this指针)
【C++】类和对象①(什么是面向对象 | 类的定义 | 类的访问限定符及封装 | 类的作用域和实例化 | 类对象的存储方式 | this指针)
|
2月前
|
存储 编译器 C语言
【C++练级之路】【Lv.2】类和对象(上)(类的定义,访问限定符,类的作用域,类的实例化,类的对象大小,this指针)
【C++练级之路】【Lv.2】类和对象(上)(类的定义,访问限定符,类的作用域,类的实例化,类的对象大小,this指针)
|
2月前
|
存储 C语言 C++
【c++】类和对象 - 类的访问限定符及封装/作用域和实例化
【c++】类和对象 - 类的访问限定符及封装/作用域和实例化
【c++】类和对象 - 类的访问限定符及封装/作用域和实例化
|
10月前
|
存储 安全 编译器
【C++基础】类与对象(上):访问限定符、类作用域、类实例化、类对象模型、this指针
【C++基础】类与对象(上):访问限定符、类作用域、类实例化、类对象模型、this指针
89 0
|
存储 编译器 C++
【C++要笑着学】泛型编程 | 函数模板 | 函数模板实例化 | 类模板(二)
本章将正式开始介绍C++中的模板,为了能让大家更好地体会到用模板多是件美事!我们将会举例说明,大家可以试着把自己带入到文章中,跟着思路去阅读和思考,真的会很有意思!如果你对网络流行梗有了解,读起来将会更有意思!
118 1
【C++要笑着学】泛型编程 | 函数模板 | 函数模板实例化 | 类模板(二)
|
编译器 C语言 C++
【C++要笑着学】泛型编程 | 函数模板 | 函数模板实例化 | 类模板(一)
本章将正式开始介绍C++中的模板,为了能让大家更好地体会到用模板多是件美事!我们将会举例说明,大家可以试着把自己带入到文章中,跟着思路去阅读和思考,真的会很有意思!如果你对网络流行梗有了解,读起来将会更有意思!
105 0
【C++要笑着学】泛型编程 | 函数模板 | 函数模板实例化 | 类模板(一)
|
存储 编译器 程序员
【C++要笑着学】类和对象 | 初识封装 | 访问限定符 | 类的作用域和实例化 | 类对象模型 | this指针(二)
本章将正式开始学习C++中的面向对象,本篇博客涵盖讲解 访问限定符、封装的基础知识、类的作用域和实例化、探究类对象的存储和对于this指针由浅入深地讲解。
142 0
【C++要笑着学】类和对象 | 初识封装 | 访问限定符 | 类的作用域和实例化 | 类对象模型 | this指针(二)