简体   繁体   中英

c++ Dependency Injection Polymorphism

I have a question about best practices for dependency injection with polymorphic classes. I'm new to C++, so please forgive me if this is an obvious question. Say I have a class Runner, which needs to take in two objects, a Logger and a Worker. Logger is an abstract class with two children, say FileLogger and SocketLogger. Similarly, Worker is an abstract class with two children, say ApproximateWorker and CompleteWorker.

The Runner class will be created from main() and will create the Logger and Worker based on a config file or something similar. I've done a lot of reading on SO and other places, and the general sentiment seems to be to prefer stack allocated objects and pass them in by reference. I'm not quite sure how to manage dynamically creating the objects like this, however. If using heap-allocated objects, I could do something like:

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();

Because I'm just storing the pointers on the stack, I can handle the different cases for Logger and Worker independently. If using stack-allocated objects, the only thing I can think would be do to something like:

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);
}

Obviously, with more than two objects to pass in, or more than two subclasses per object, this quickly becomes ridiculous. My understanding is that object slicing will prevent you from doing something similar to the first snippet.

Am I missing something obvious here? Or is this a case for using dynamic memory (with smart pointers of course)?

If Runner will use these objects in a polymorphic way (access derived objects via base class interfaces), you should pass pointers or references to it. There are pros and cons of variables on stack and on heap. There is no universal rule that one is preferred over the other.

One thing more, abstract factory pattern may suit your case. It separates WHAT(exact types of objects are used) from HOW(these objects are used). It's all about encapsulating the change.

// 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);

Edit:

At least two benefits I can see:

  1. When you add a new case or change the type of concrete Logger/Worker, Client.cpp won't be affected. That said, you limit the change inside Factory.cpp so that the client logic(which actually uses the created objects) is unchanged.

  2. Runner is programmed to only the factory interface. Clients depending on the Runner interface won't be affected by the change of Logger , Worker , etc.

Personally, it's totally OK not to use this pattern for a small code base. In a large project where there are lots of dependencies among classes/files, it will make a difference, both to the compilation time and scalability.

Shared or unique pointers can help, but you can still take references to the object as dependency injected variables.

You do need to make sure that you don't destroy the objects (logger, worker) before the runner. Dependency injection asks for factories. In this case I use a unique_ptr not for passing around ownership, but as a RAII safe handle to the abstract type.

#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;
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM