1

I want to create new instances of programs depending on a file extension. Unfortunately I cannot change the interface, because filename is a command-line argument. Also, I don't like that if I want to add new program, I have to deal with if-else statements. I thought about maps for storing (string, type_index), but apparently this approach doesn't work with templates. Furthermore, I tried to deal with std::enable_if by passing a constexpr string comparison function, but that obviously doesn't work either. I suppose I can somehow extend the latter approach by creating a bunch of functions and pick one depending on a string comparison. Basically, what I want is to achieve extensibility and be compliant with SOLID principles.

std::shared_ptr<Program> Program::fromFile(const Path& file){
    auto extension = file.extension();

    if(extension == "cpp"){
        return std::make_shared<CppProgram>(file);
    } else if(extension == "py"){
        return std::make_shared<PythonProgram>(file);
    } else if(extension == "java"){
        return std::make_shared<JavaProgram>(file);
    } else if(extension == "go"){
        return std::make_shared<GoProgram>(file);
    } else {
        throw std::runtime_error("unknown file extension " + extension);
    }
}
Joseph Kirtman
  • 349
  • 2
  • 10
  • You've already answered your question. That's exactly how you do it. Any other variations are going to be basically the same thing, but more obfuscated. – Sam Varshavchik Nov 06 '19 at 12:02
  • Use registration (of a creator) with a factory. To to extend just register a new creator with the factory. The factory just asks each creator _"can you create from this file"_ if it can return the result, if it can't try the next one. – Richard Critten Nov 06 '19 at 12:02

1 Answers1

2

How about this?

struct Maker {
  virtual shared_ptr<Program> make(const Path& file) = 0;
};

map<string, unique_ptr<Maker>> g_makers;

std::shared_ptr<Program> Program::fromFile(const Path& file) {
  auto extension = file.extension();
  auto it = g_makers.find(extension);
  if(it == g_makers.end())
    throw std::runtime_error("unknown file extension " + extension);
  return it->second->make(file);        
}

// probably in a separate file:

struct CppMaker : Maker {
  shared_ptr<Program> make(const Path& file) override {
    return make_shared<CppProgram>(file);
  }
};

__attribute__((constructor))
static void initCppMaker() {
  g_makers["cpp"].reset(new CppMaker());
}

Now all you need to do is decide when and how to register the various Maker implementations into g_makers. You might do that using compiler-specific syntax like GCC's __attribute__((constructor)) or other initialization techniques. You can define each Maker derived class in a separate translation unit and do the initialization there.

John Zwinck
  • 239,568
  • 38
  • 324
  • 436