C++依赖注入多态性

4

我有一个关于多态类依赖注入最佳实践的问题,涉及到IT技术。我是C++的新手,请谅解如果我的问题很简单。假设我有一个Runner类,它需要接收两个对象:Logger和Worker。Logger是一个抽象类,有两个子类:FileLogger和SocketLogger。同样地,Worker也是一个抽象类,有两个子类:ApproximateWorker和CompleteWorker。

Runner类将在main()函数中创建,并根据配置文件或类似的东西创建Logger和Worker。我在SO和其他地方阅读了很多文章,通常的建议是优先使用栈分配的对象,并通过引用传递它们。然而,我不太确定如何动态地创建这些对象。如果使用堆分配的对象,我可以做以下操作:

Logger* log;
Worker* worker;
if (/*user wants a file logger*/ {
    log = new FileLogger();
} else {
    log = new SocketLogger();
}
if (/* user wants an approximate worker*/) {
    worker = new ApproximateWorker();
} else {
    worker = new CompleteWorker();
}
Runner runner = Runner(log, worker);
runner.run();

因为我只是在堆栈上存储指针,所以可以独立处理Logger和Worker的不同情况。如果使用堆栈分配的对象,则唯一能想到的方法是:

if (/*file logger and approx worker*/) {
    FileLogger log();
    ApproximateWorker worker();
    Runner runner = Runner(log, worker);
} else if (/*file logger and complete worker*/) {
    FileLogger log();
    CompleteWorker worker();
    Runner runner = Runner(log, worker);
} else if (/*socket logger and approx worker*/) {
    SocketLogger log();
    ApproximateWorker worker();
    Runner runner = Runner(log, worker);
} else {
    SocketLogger log();
    CompleteWorker worker();
    Runner runner = Runner(log, worker);
}

显然,如果要传入两个以上的对象,或者每个对象有两个以上的子类,这将很快变得荒谬。我的理解是,对象切片将防止您执行类似于第一个代码片段的操作。
我是否漏掉了什么明显的东西?还是应该使用动态内存(当然要使用智能指针)?

不要根据堆栈来设计程序。建议在没有动态分配的情况下使用堆栈。你不想在循环内使用 new / delete,这会很昂贵,尤其对于嵌入式系统,但我在你的例子中看不出问题。 - user5106417
感谢回复。是的,性能不是问题,只是遵循常规做法。我看到很多人说使用栈分配的对象通常更好,因为它使对象的生命周期非常明显,对于像这样的情况,所有对象的生命周期应该在此主函数退出时结束,这似乎是个好主意。 - Orion
2个回答

2
如果Runner将以多态方式使用这些对象(通过基类接口访问派生对象),则应传递指针或引用。堆栈和堆上的变量各有优缺点。没有普遍规则,哪个更好。
还有一件事,抽象工厂模式可能适合您的情况。它将WHAT(所使用的确切对象类型)与HOW(这些对象如何使用)分离开来。这一切都是关于封装变化的。
// Factory.h
class tAbstractFactory
{
public:
   virtual Logger* getLogger() = 0;
   virtual Worker* getWorker() = 0;
};

template<typename loggerClass, typename workerClass>
class tConcreteFactory: public tAbstractFactory
{
public:
   loggerClass* getLogger() { return new loggerClass; }
   workerClass* getWorker() { return new workerClass; }
};

// Runner.h
class Runner
{
public:
   Runner(tAbstractFactory &fa)
   {
      m_logger = fa.getLogger();
      m_worker = fa.getWorker();
   }
private:
   Logger *m_logger;
   Worker *m_worker;
};

// Factory.cpp
tAbstractFactory &getFactory(int sel)
{
   if (sel == 1)
   {
      static tConcreteFactory<FileLogger, ApproximateWorker> fa;
      return fa;
   }
   else if (sel == 2)
   {
      static tConcreteFactory<FileLogger, CompleteWorker> fa;
      return fa;
   }
   else if (sel == 3)
   {
      static tConcreteFactory<SocketLogger, ApproximateWorker> fa;
      return fa; 
   }
   else
   {
      static tConcreteFactory<SocketLogger, CompleteWorker> fa;
      return fa; 
   }
}

// Client.cpp
Runner runner(fac);

编辑:

我至少能看到两个好处:

  1. 当你添加一个新的案例或更改具体的Logger/Worker类型时,Client.cpp不会受到影响。也就是说,你将变化限制在Factory.cpp内部,以便客户端逻辑(实际使用创建的对象)不变。

  2. Runner仅编程为工厂接口。依赖于Runner接口的客户端不会受到LoggerWorker等的更改的影响。

个人而言,在小型代码库中不使用此模式完全没有问题。在大型项目中,类/文件之间存在许多依赖关系,这将产生巨大的影响,无论是编译时间还是可扩展性。


没有普适的规则表明哪种方法更好。如果你遵循Bjarn Stroustrup的RAII(资源获取即初始化)原则,那么在使用资源容器时,如std::vectorstd::shared_ptr,它们可能会使用堆来存储其内容,栈有一个明确的优先级,而不是使用newdelete(即堆)。当然,这个规则并不是普适的,多态性需要指针或引用(但不一定是(裸)堆上的对象)。 - Kasper van den Berg
首先,RAII 与堆栈无关。相反,RAII 对象的很大一部分都封装了在堆上分配的对象。 - Eric Z
感谢回复。如果不清楚的话,第二个片段是想通过引用传递,只是使用堆栈对象的引用。因此,我相信多态性应该可以工作,但前提是实际堆栈对象的类型是派生类。我猜我没有看到为什么工厂在这种情况下有用?在您的示例中,似乎我仍然需要为每个可能的派生类组合复制代码。最终我可能会改用唯一和共享指针来使用堆分配的对象。 - Orion
  1. 在将函数作用域变量的引用传递给另一个对象时,您应该非常小心。这会导致在传入的对象超出范围时保持无效引用的风险。这需要仔细编程。
  2. 即使您正在使用shared_ptr,您仍然在使用它来管理堆分配的对象。换句话说,你不应该担心堆本身,而是要关注如何管理它。
  3. 关于您最后的问题,答案已在上面更新。
- Eric Z

0

共享或独特指针可以帮助,但您仍然可以将对象的引用作为依赖注入变量。

您确实需要确保在运行程序之前不要销毁对象(记录器、工作者)。依赖注入要求使用工厂。在这种情况下,我使用unique_ptr而不是传递所有权,而是作为抽象类型的RAII安全句柄。

#include <iostream>
#include <memory>
#include <exception>

struct Logger{
    virtual void log() =0;
}; 
struct Logger1 : Logger {
    void log() override { std::cout << " l1 " << std::endl;} 
};
struct Logger2 : Logger {
    void log() override { std::cout << " l2 " << std::endl;} 
};
struct Logger3 : Logger {
    void log() override { std::cout << " l3 " << std::endl;} 
};

struct Worker{
    virtual void work() =0;
};
struct Worker1 : Worker{
    void work() override { std::cout << " w1 " << std::endl;} 
};
struct Worker2 : Worker{
    void work() override { std::cout << " w2 " << std::endl;} 
};
struct Worker3 : Worker{
    void work() override { std::cout << " w3 " << std::endl;} 
};

struct Runner{
   Runner(Worker& worker, Logger& logger): worker(worker),logger(logger) {};

   Worker& worker;
   Logger& logger;
   void run(){
      worker.work();
      logger.log();
   }
};


std::unique_ptr<Worker> mkUniqueWorker(int i){ 
    switch (i) {
        case 1: return std::make_unique<Worker1>() ;
        case 2: return std::make_unique<Worker2>() ;
        case 3: return std::make_unique<Worker3>() ;
        case 4: throw std::runtime_error("unknown worker");
   }
};
std::unique_ptr<Logger> mkUniqueLogger(int i){ 
    switch (i) {
        case 1: return std::make_unique<Logger1>() ;
        case 2: return std::make_unique<Logger2>() ;
        case 3: return std::make_unique<Logger3>() ;
        case 4: throw std::runtime_error("unknown logger");
   }
};

int main() {

    auto worker = mkUniqueWorker(2);
    auto logger = mkUniqueLogger(3);
    Runner runner = Runner(*worker, *logger);
    runner.run();

    return 0;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接