return a Type, or how to preserve a type of an object pointer?

前端 未结 3 1821
不知归路
不知归路 2021-01-20 13:34

I have a very complicated code structure, but the important bits are:

typical setup: I have a base class and two classes that derive from this base class and each ha

相关标签:
3条回答
  • 2021-01-20 14:22

    The funny thing about polymorphism is that it points out to you when you are not using it.

    Inheriting a base class in the way you are serves 1 purpose: to expose a uniform interface for objects with different behaviors. Basically, you want the child classes to look the same. If I have classes B and C that inherit from A, I want to say "do foo" to the class, and it'll do foob or fooc.

    Essentially, you're flipping it around: I have a B and C of type A, and if it is B i want to do foob and if it is C I want to do fooc. While this may seem scary, usually the best way to solve the problem is to rephrase the question.

    So to your example, you are currently saying "OK, so I have an XML file, and I will read data from it one way if I'm making an A, or another way if I'm making a B." But the polymorphic way would be "I have an XML file. It tells me to make an A or a B, and then I tell the instance to parse the XML file".

    So one of the ways to solve this to change your solver interface:

    class BaseSolver
    {
    public:
      virtual void ReadXMLFile(string xml) = 0;
    ...
    };
    

    While this does rephrase the problem in a way that uses polymorphism, and removes the need for you to see what you've created, you probably don't like that for the same reason I don't: you'd have to supply a default constructor, which leaves the class in an unknown state.

    So rather than enforce it at the interface level, you could enforce it at the constructor level, and make both SolverA and SolverB have to take in the XML string as part of the constructor.

    But what if the XML string is bad? Then you'd get an error state in the constructor, which is also a no-no. So I'd deal with this using the factory pattern:

    class SolverFactory;
    
    class BaseSolver
    {
    public:
      virtual void solve() = 0;
    protected:
      virtual int ReadXML(std::string xml) = 0;
      friend class SolverFactory;
    };
    
    class A : public BaseSolver
    {
    public:
      virtual void solve() {std::cout << "A" << std::endl;}
    protected:
      A(){}
      virtual int ReadXML(std::string xml) {return 0;}
      friend class SolverFactory;
    };
    
    class B : public BaseSolver
    {
    public:
      virtual void solve() {std::cout << "B" << std::endl;}
    protected:
      B(){}
      virtual int ReadXML(std::string xml) {return 0;}
      friend class SolverFactory;
    };
    
    class SolverFactory
    {
    public:
      static BaseSolver* MakeSolver(std::string xml)
      {
        BaseSolver* ret = NULL;
        if (xml=="A")
        {
          ret = new A();
        }
        else if (xml=="B")
        {
          ret = new B();
        }
        else
        {
          return ret;
        }
        int err = ret->ReadXML(xml);
        if (err)
        {
          delete ret;
          ret = NULL;
        }
        return ret;
      }
    };
    

    I didn't put any actual XML processing in here because I am lazy, but you could have the factory get the type from the main tag and then pass the rest of the node in. This method ensures great encapsulation, can catch errors in the xml file, and safely separates the behaviors you are trying to get. It also only exposes the dangerous functions (the default constructor and ReadXMLFile) to the SolverFactory, where you (supposedly) know what you are doing.

    Edit: in response to the question

    The problem you've stated is "I have a B and C of type A, and if is B i want to set "b" settings and if it is C i want to set "c" settings".

    Taking advantage of polymorphism, you say "I have a B and C of type A. I tell them to get their settings."

    There a couple of ways to do this. If you don't mind mangling your IO with the class, you can simply expose the method:

    class BaseSolver
    {
    public:
      virtual void GetSettingsFromCommandLine() = 0;  
    };
    

    And then create the individual methods for each class.

    If you do want to create them separate, then what you want is polymorphism in the io. So expose it that way:

    class PolymorphicIO
    {
    public:
      virtual const BaseSolver& get_base_solver() const = 0;
      virtual void DoSettingIO() = 0;
    };
    

    an example implmentation

    class BaseSolverBIO : PolymorphicIO
    {
    public:
      virtual const BaseSolver& get_base_solver() const {return b;}
      virtual void DoSettingIO() { char setting = get_char(); b.set_b(setting);}
    private:
      BaseSolverB b;
    };
    

    At first glance this seems like a lot of code (we've doubled the number of classes, and probably need to supply a factory class for both BaseSolver and the IO interface). Why do it?

    It is the issue of scaleability/maintainability. Lets say you have figured out a new solver you want to add (D). If you are using dynamic cast, you have to find all the places in your top level and add a new case statement. If there is only 1 place, then this is pretty easy, but if it is 10 places, you could easily forget one and it would be hard to track down. Instead, with this method you have a separate class that has all the specific IO functionality for the solver.

    Lets also think of what happens to those dynamic_cast checks as the number of solvers grows. You've been maintaining this software for years now with a large team, and lets say you've come up with solvers up to the letter Z. Each of those if-else statements are hundreds-a tousand of lines long now: if you have an error in O you have to scroll through A-M just to find the bug. Also, the overhead for using the polymorphism is constant, while reflection just grows and grows and grows.

    The final benefit for doing it this way is if you have a class BB : public B. You probably have all the old settings from B, and want to keep them, just make it a little bigger. Using this model, you can extend the IO class as well for the io for BB and reuse that code.

    0 讨论(0)
  • 2021-01-20 14:23

    Use dynamic_cast to try to cast a pointer-to-base-class to pointer-to-derived-class. It will return NULL if the pointed-to object of the base class does not exist (NULL value of the base pointer), or is not actually a derived class object. If the result, instead, is not NULL, you have a valid pointer-to-derived-class.

    int main(){
        IOService ioService;
        BaseSolver* mySolver = ioService.getSolver();
        SolverB* bSolver = dynamic_cast<SolverB*>(mySolver);
        if (bSolver != NULL)
        {
            int finallyIGotB = bSolver->b;
            cout << finallyIGotB;
        }
    }
    

    Note that there may be some better design solutions than using dynamic_cast. But at least this is one possibility.

    0 讨论(0)
  • 2021-01-20 14:34

    One way to achieve this is to add an interface method into the base class:

    class BaseSolver{
    virtual void SolverMethodToCallFromMain() = 0;
    ...
    };
    
    class SolverA : BaseSolver{
    public:
        std::string a;
        SolverA(TypeA objectA);
        virtual void SolverMethodToCallFromMain() {/*SolverA stuff here*/};
    };
    
    class SolverB : BaseSolver{
    public:
        int b;
        SolverB(TypeB objectB);
        virtual void SolverMethodToCallFromMain() {/*SolverB stuff here*/};
    };
    

    And in main:

    int main(){
        IOService ioService;
        BaseSolver* mySolver = ioService.getSolver();
        mySolver->SolverMethodToCallFromMain();
    }
    
    0 讨论(0)
提交回复
热议问题