-1

Consider I have a Plant class that has derived Fruit and Vegetable classes, and Fruit class has some more derived classes, like Orange and Apple, while Vegetable has derived Potato and Tomato. Assume, Plant has Plant::onConsume()=0; method:

class Plant
{
public:
    virtual void onConsume(void)=0;
};

class Fruit:public Plant
{
};

class Orange:public Fruit
{
    void onConsume(void)
    {
        // Do something specific here
    }
};

class Apple:public Fruit
{
    void onConsume(void)
    {
        // Do something specific here
    }
};

class Vegetable:public Plant
{
};

class Potato:public Vegetable
{
    void onConsume(void)
    {
        // Do something specific here
    }
};
class Tomato:public Vegetable
{
    void onConsume(void)
    {
        // Do something specific here
    }
};

class Consumer
{
public:
    void consume(Plant &p)
    {
        p.onConsume();
        // Specific actions depending on actual p type here
        // like send REST command to the remote host for Orange
        // or draw a red square on the screen for Tomato
    }
};

Suppose, I have a Consumer class with Consumer::consume(Plant) method. This "consume" method should perform different actions for different "Plants" instances/types, among calling Plant::onConsume() for any of "Plants". These action ain't directly related to the Plant class, require a lot of different additional actions and parameters, could literally be completely arbitrary, so cannot be implemented inside onConsume method.

What is the preferred method to implement this? As I understand, it is possible to implement some "Plant::getPlantType()=0" method, that would return plant type, but in this case I'm not sure what should it return. In case the returned value would be an enum, I'd need to change this enum each time I add a new derived class. And in any case, there's no control that multiple derived classes could return the same value.

Also, I'm aware there's a dynamic_cast conversion that returns nullptr if conversion could not be made, and typeid() operator that returns std::typeinfo (even with typeinfo::name()), which could be used in the switch() (it's just great for my case). But I'm afraid it could significally slow down the execution and make code heavier.

So, my question is, what is the preferred way in C++ to do that? maybe I just forgot about some simpler way to implement that?

A little update. Thank you for your explanations about inheritance, encapsulation etc! I supposed it's clear from my question, but it is not, I am sorry about that. So, please think about it, like I don't have an access to the whole Plant sources hierarchy, just need to implement this Consumer::onConsume(Plant). So I cannot add new specific methods in it. Or, also, it could be considered as a Plants library, that I have to write once, and make it usable for other devs. So, I could divide use cases/functionality into two parts: one that implemented "per class" in the Plant::onConsume() method, and second that is unknown yet and will differ depending on usage.

BUKTOP
  • 867
  • 10
  • 22
  • 4
    The preferred way in C++ is to extend the interface of your Plant class such that it covers any actions that might be fruit specific. Not sure what your actions are all about, maybe something like `peel()`, `cut()`, `cook()`, ... That way you do not have to care for the type, polymorphism will do so for you. – Aconcagua May 07 '21 at 17:01
  • ...And if you cannot find such common interface, your current hyerarchy is pointless – MatG May 07 '21 at 17:03
  • 1
    You should give us some examples of what the `Consumer::consume(Plant)` method might do in specific cases. Otherwise the general recommendation would be simply to forward to the `Plant` class: `Consumer::consume(Plant& p){ p.consume(); }` – quamrana May 07 '21 at 17:04
  • 1
    I think it's a mistake to try to move functionality or knowledge out of the `Plant` hierarchy and into the `Consumer` code. What are you interested in? Is it tasty? Is it nutritious? These are attributes of the plant, not the consumer. So, define a virtual interface in Plant that answers these questions, perhaps with safe defaults in parent classes, and then make the derived classes as distinct as you like. Then it's up to the consumer to use the information as they see fit – Tim Randall May 07 '21 at 17:13
  • I edited the question, hope now it's clearer than it was at the beginning – BUKTOP May 07 '21 at 17:53
  • Wow. Just missed some comments. Ever heard about loose coupling, guys? ))))) – BUKTOP May 07 '21 at 20:42

2 Answers2

1

One option would be the visitor pattern, but this requires one function per type in some class. Basically you create a base class PlantVisitor with one Visit function per object type and pass add a virtual method to Plant that receives a PlantVisitor object and calls the corresponding function of the visitor passing itself as parameter:

class PlantVisitor
{
public:
    virtual void Visit(Orange& orange) = 0;
    virtual void Visit(Tomato& tomato) = 0;
    ...
};

class Plant
{
public:
    virtual void Accept(PlantVisitor& visitor) = 0;
};

class Orange : public Plant
{
public:
    void Accept(PlantVisitor& visitor) override
    {
        visitor.Visit(*this);
    }
};


class Tomato : public Plant
{
public:
    void Accept(PlantVisitor& visitor) override
    {
        visitor.Visit(*this);
    }
};

This would allow you to do something like this:

class TypePrintVisitor : public PlantVisitor
{
public:
    void Visit(Orange& orange) override
    {
        std::cout << "Orange\n";
    }
    void Visit(Tomato& tomato) override
    {
        std::cout << "Tomato\n";
    }
};

std::vector<std::unique_ptr<Plant>> plants;
plants.emplace_back(std::make_unique<Orange>());
plants.emplace_back(std::make_unique<Tomato>());

TypePrintVisitor visitor;

for (size_t i = 0; i != plants.size(); ++i)
{
    std::cout << "plant " << (i+1) << " is a ";
    plants[i]->Accept(visitor);
}

Not sure the need for this does not indicate a design inefficiency though.

Btw: If you've got multiple visitors and do not necessarily want to implement logic for every single type in all of them, you could add default implementations in PlantVisitor that call the function for the supertype instead of specifying pure virtual functions.

fabian
  • 80,457
  • 12
  • 86
  • 114
  • Thank you! Probably, visitor pattern is really the best solution, although it could make code harder to understand in my case. – BUKTOP May 07 '21 at 17:52
0

Polymorphism is all about not having to know about a specific type. Usually your design is flawed if you discover having to detect a specific type explicitly.

At very first:

void Consumer::consume(Plant p)

does not work as intended! The Plant object is accepted by value, i. e. its bytes are copied one by one; however, only those of the Plant type, any others (those of derived types) are ignored and get lost within consume function – this is called object slicing.

Polymorphism only works with references or pointers.

Now assume you want to do something like the following (incomplete code!):

void Consumer::consume(Plant& p) // must be reference or pointer!
{
    p.onConsume();
    generalCode1();
    if(/* p is apple */)
    {
        appleSpecific();
    }
    else if(/* p is orange */)
    {
        orangeSpecific();
    }
    generalCode2();
}

You don't want to decide yourself upon type, you let the Plant class do the stuff for you, which means you extend its interface appropriately:

class Plant
{
public:
    virtual void onConsume() = 0;
    virtual void specific() = 0;
};

The code of the consume function will now be changed to:

void Consumer::consume(Plant const& p) // must be reference or pointer!
{
    p.onConsume();
    generalCode1();
    p.specific();
    generalCode2();
}

You'll do so at any place you need specific behaviour (and specific is just a demo name, chose one that describes nicely what the function actually is intended to do).

    p.onConsume();
    generalCode1();
    p.specific1();
    generalCode2();
    p.specific2();
    generalCode3();
    p.specific3();
    generalCode4();
    // ...

Of course you need now to provide appropriate implementations in your derived classes:

class Orange:public Fruit
{
    void onConsume() override
    { }
    void specific() override
    {
        orangeSpecific();
    }
};

class Apple:public Fruit
{
    void onConsume() override
    { }

    void specific() override
    {
        appleSpecific();
    }
};

Note the addition of override keyword, which protects you from accidentally creating overloaded functions instead actually overwriting in case of signature mismatch. It helps you, too, to locate all places of necessary changes if you discover having to change the function signature in the base class.

Aconcagua
  • 24,880
  • 4
  • 34
  • 59
  • Thank you, I was working with java for last 25 years, and it pass arguments by reference. I have edited my question. – BUKTOP May 07 '21 at 17:44
  • @BbIKTOP Then I recommend having a look at const-correctness, too, a matter not available in Java either (at least was not when I worked last with). In short: member functions that do not modify their objects should be declared `const` (and you can have const and non-const overloads, as constness is part of the signature), reference or pointer arguments that won't be modified should be declared `const` as well, e. g. `class Demo { void example(Plant const& p) const; };`. – Aconcagua May 07 '21 at 17:53