2

Let's say I have a parent class, Arbitrary, and two child classes, Foo and Bar. I'm trying to implement a function to insert any Arbitrary object into a database, however, since the child classes contain data specific to those classes, I need to perform slightly different operations depending on the type.

Coming into C++ from Java/C#, my first instinct was to have a function that takes the parent as the parameter use something like instanceof and some if statements to handle child-class-specific behavior.

Pseudocode:

void someClass(Arbitrary obj){
    obj.doSomething(); //a member function from the parent class
    //more operations based on parent class
    if(obj instanceof Foo){
        //do Foo specific stuff
    }
    if(obj instanceof Bar){
        //do Bar specific stuff
    }
}

However, after looking into how to implement this in C++, the general consensus seemed to be that this is poor design.

If you have to use instanceof, there is, in most cases, something wrong with your design. – mslot

I considered the possibility of overloading the function with each type, but that would seemingly lead to code duplication. And, I would still end up needing to handle the child-specific behavior in the parent class, so that wouldn't solve the problem anyway.

So, my question is, what's the better way of performing operations that where all parent and child classes should be accepted as input, but in which behavior is dictated by the object type?

Community
  • 1
  • 1
404 Not Found
  • 3,635
  • 2
  • 28
  • 34

5 Answers5

4

First, you want to take your Arbitrary by pointer or reference, otherwise you will slice off the derived class. Next, sounds like a case of a virtual method.

void someClass(Arbitrary* obj) {
    obj->insertIntoDB();
}

where:

class Arbitrary {
public:
    virtual ~Arbitrary();
    virtual void insertIntoDB() = 0;
};

So that the subclasses can provide specific overrides:

class Foo : public Arbitrary {
public:
    void insertIntoDB() override
    //                  ^^^ if C++11
    {
        // do Foo-specific insertion here
    }
};

Now there might be some common functionality in this insertion between Foo and Bar... so you should put that as a protected method in Arbitrary. protected so that both Foo and Bar have access to it but someClass() doesn't.

Barry
  • 286,269
  • 29
  • 621
  • 977
  • Aww, this makes a lot of sense, but it's really going to slaughter my dreams of keeping database methods within the database class. :( This does sound like the best way so far, so I guess I'll have to deal with it. – 404 Not Found Dec 24 '14 at 04:56
  • 1
    @404NotFound You can pass a DB object in... `obj->insertIntoDB(connection);` and then have `Foo` and `Bar` just write different things to it. – Barry Dec 24 '14 at 04:58
  • @404NotFound you don't have to lose that dream but it's hard to be more specific given the abstractness of the code in the question – M.M Dec 24 '14 at 05:13
  • That's what I figured I would end up doing, but having some SQL existing outside of the database class is just going to irk me. I just like to keep DB stuff encapsulated, as though it were a blackbox system. It's just a personal problem, but I guess I'll have to live with it. – 404 Not Found Dec 24 '14 at 05:13
  • @404NotFound You can still keep the db separate, how different are Foo and Bar in terms of data stored? Are they storing in the same way but use different ways of computing the data or are they represented in a different manner in a database? – Weltschmerz Dec 24 '14 at 07:37
1

The issue here is that this will arguably violate SOLID design principles, given that any extension in the number of mapped classes would require new branches in the if statement, otherwise the existing dispatch method will fail (it won't work with any subclass, just those it knows about).

What you are describing looks well suited to inheritance polymorphicism - each of Arbitrary (base), Foo and Bar can take on the concerns of its own fields.

There is likely to be some common database plumbing which can be DRY'd up the base method.

class Arbitrary { // Your base class
  protected:
   virtual void mapFields(DbCommand& dbCommand) {
     // Map the base fields here
   }

  public:
   void saveToDatabase() { // External caller invokes this on any subclass
      openConnection();
      DbCommand& command = createDbCommand();
      mapFields(command); // Polymorphic call
      executeDbTransaction(command);
   }
}

class Foo : public Arbitrary {
   protected: // Hide implementation external parties
     virtual void mapFields(DbCommand& dbCommand) {
        Arbitrary::mapFields();
        // Map Foo specific fields here
     }
}

class Bar : public Arbitrary {
   protected:
     virtual void mapFields(DbCommand& dbCommand) {
        Arbitrary::mapFields();
        // Map Bar specific fields here
     }
}

If the base class, Arbitrary itself cannot exist in isolation, it should also be marked as abstract.

StuartLC
  • 104,537
  • 17
  • 209
  • 285
  • Why did you say it violated the Liskov substitution principle? I would rather say it violates the open/closed principle. – Yongwei Wu Dec 24 '14 at 05:09
  • Agreed that it is no doubt also not `Open for Extension`, but LSP states `if S is a subtype of T, then objects of type T in a program may be replaced with objects of type S without altering any of the desirable properties of that program` - OP needs to alter his mega switch statement as he reasons over type. The `someClass` method advertises that it works with `Arbitrary`, which isn't true, it only works for specific subtypes of `Arbitrary` – StuartLC Dec 24 '14 at 05:12
  • Arguably, the violation is potential, but not shown in the given code. There is no sure sign that when the Arbitrary is replaced with a Foo, Bar, or Foobar, the program will break. – Yongwei Wu Dec 24 '14 at 05:51
  • Other than `FooBars` not being catered for, and hence not saved to database? I would regard that as broken IMO. – StuartLC Dec 24 '14 at 05:53
  • Saving to database is not a method of Arbitrary. It is actually not Arbitrary's behaviour. LSP barely applies here. I would say the generic method is that Arbitrary should implement a serialization method, and saving to database only needs to take advantage of that--but that is off-topic (about the applicability of LSP). – Yongwei Wu Dec 24 '14 at 06:07
  • @YongweiWu OP's example of conditionally checking on type meets Uncle Bob's criteria of a "Glaring Violation" of the LSP - the first example [cited here](http://www.objectmentor.com/resources/articles/lsp.pdf). Admittedly, Uncle Bob also mentions that these generally violate the Open and Closed principle as well, as new concrete subclasses need to be accomodated. – StuartLC Apr 06 '15 at 07:18
1

In my opinion, if at any place you need to write

if( is_instance_of(Derived1) )
    //do something
else if ( is_instance_of(Derived2) )
    //do somthing else
...

then it's as sign of bad design. First and most straight forward issue is that of "Maintainence". You have to take care in case further derivation happens. However, sometimes it's necessary. for e.g if your all classes are part of some library. In other cases you should avoid this coding as far as possible.

Most often you can remove the need to check for specific instance by introducing some new classes in the hierarchy. For e.g :-

class BankAccount {};
class SavingAccount : public BankAccount { void creditInterest(); };
class CheckingAccount : public BankAccount { void creditInterest(): };

In this case, there seems to be a need for if/else statement to check for actual object as there is no corresponsing creditInterest() in BanAccount class. However, indroducing a new class could obviate the need for that checking.

class BankAccount {};
class InterestBearingAccount : public BankAccount { void creditInterest(): } {};
class SavingAccount : public InterestBearingAccount { void creditInterest(): };
class CheckingAccount : public InterestBearingAccount { void creditInterest(): };
ravi
  • 10,994
  • 1
  • 18
  • 36
1

As StuartLC pointed out, the current design violates the SOLID principles. However, both his answer and Barry's answer has strong coupling with the database, which I do not like (should Arbitrary really need to know about the database?). I would suggest that you make some additional abstraction, and make the database operations independent of the the data types.

One possible implementation may be like:

class Arbitrary {
public:
    virtual std::string serialize();
    static Arbitrary* deserialize();
};

Your database-related would be like (please notice that the parameter form Arbitrary obj is wrong and can truncate the object):

void someMethod(const Arbitrary& obj)
{
    // ...
    db.insert(obj.serialize());
}

You can retrieve the string from the database later and deserialize into a suitable object.

Yongwei Wu
  • 5,292
  • 37
  • 49
0

So, my question is, what's the better way of performing operations that where all parent and child classes should be accepted as input, but in which behavior is dictated by the object type?

You can use Visitor pattern.

#include <iostream>

using namespace std;

class Arbitrary;
class Foo;
class Bar;

class ArbitraryVisitor
{
public:

    virtual void visitParent(Arbitrary& m)  {};
    virtual void visitFoo(Foo& vm) {};
    virtual void visitBar(Bar& vm) {};
};

class Arbitrary
{
public:
    virtual void DoSomething()
    {
        cout<<"do Parent specific stuff"<<endl;
    }

    virtual void accept(ArbitraryVisitor& v)
    {
        v.visitParent(*this);
    }
};

class Foo: public Arbitrary
{
public:

    virtual void DoSomething()
    {
        cout<<"do Foo specific stuff"<<endl;
    }

    virtual void accept(ArbitraryVisitor& v)
    {
        v.visitFoo(*this);
    }
};

class Bar: public Arbitrary
{
public:

    virtual void DoSomething()
    {
        cout<<"do Bar specific stuff"<<endl;
    }

    virtual void accept(ArbitraryVisitor& v)
    {
        v.visitBar(*this);
    }
};

class SetArbitaryVisitor : public ArbitraryVisitor
{
    void visitParent(Arbitrary& vm)
    {
        vm.DoSomething();
    }

    void visitFoo(Foo& vm)
    {
        vm.DoSomething();
    }

    void visitBar(Bar& vm)
    {
        vm.DoSomething();
    }
};

int main()
{
    Arbitrary *arb = new Foo();
    SetArbitaryVisitor scv;
    arb->accept(scv);
}
user1
  • 4,031
  • 8
  • 37
  • 66