0

I'm coming mostly from functional languages background and I have no idea how to do this sort of pattern well in object oriented languages.

Let's say I have some Animal classes and subclasses and an AnimalManager. I want an "add" method on the AnimalManager which has different behavior depending on whether I'm adding a Cat or a Dog. Something like this (in no particular language):

class Animal
class Cat : Animal
class Dog : Animal

class AnimalManager {
  void add(Animal a) { 
    a match {
       case Cat => // do something with cats
       case Dog => // do different something with dogs
    } 
  }
}

Now what if I'm using a language like C++ which does not support matching on type in this manner. Two solutions I could think of are:

  1. Use dynamic_cast. However, I'm told dynamic cast should be avoided because it can lead to run-time errors, so this may be bad practice.
  2. Use virtual functions. I could do something like the following (again no particular language, but you should get the gist):
class Animal {
  virtual void beAdded(AnimalManager am)
}
class Cat : Animal {
  void beAdded(AnimalManager am) {
    am.doThingsWithCats(this)
  }  
}
class Dog : Animal {
  void beAdded(AnimalManager am) {
    am.doThingsWithDogs(this)
  }  
}

class AnimalManager {
  void doThingsWithDogs(Dog d) {
    // do something with dogs
  }
  void doThingWithCats(Cat d) {
    // do something with cats
  }
  void add(Animal a) {
    a.beAdded(this)
  } 
}

This is obviously extremely awkward and has other problems (now Animal needs to know about AnimalManager?).

What is the best way to do this sort of pattern in object-oriented languages? Is there some "design pattern" here I should know about?

KidKush420
  • 11
  • 1
  • Enable RTTI and you may check the type on runtime. Or create an attribute for storing the type. – Louis Go Sep 17 '21 at 01:29
  • 4
    What's awkward from an OO standpoint is doing something different with cats and dogs when adding them instead of having virtual functions for the things you want to do with them once they're already added. – chris Sep 17 '21 at 01:38
  • 1
    A little tangential but... [what is `AnimalManager`](https://blog.codinghorror.com/i-shall-call-it-somethingmanager/)? And the way you wrote your attempted solution, seems you are kinda reinventing a visitor (a common design pattern). – StoryTeller - Unslander Monica Sep 17 '21 at 01:38
  • 2
    You don't. You use virtual methods, that do different things in each subtype as required. Any time you need to know the actual subtype you are probably doing something wrong. – user207421 Sep 17 '21 at 01:53
  • @chris I suppose "process" may be a better method name here. It doesn't necessarily have to own the object itself, just do something with it. – KidKush420 Sep 17 '21 at 02:06
  • @Unslander Monica It's just a thing that does something non-specific with Animals. Maybe the visitor pattern is what I'm looking for. – KidKush420 Sep 17 '21 at 02:09
  • @user207421 I used virtual methods in my attempted solution and it's extremely awkward. Can you suggest a better way to handle it? – KidKush420 Sep 17 '21 at 02:09
  • Please provide your real use case.. or Animal is your real use case? – Louis Go Sep 17 '21 at 02:10
  • 2
    @KidKush420: If `AnimalManager` needs to know what kind of animal it is managing, then `Animal` is *failing* in its job to be a base class, or `AnimalManager` is failing in its job of managing `Animal`s. Anything which takes an `Animal` should not care which kind it is, and if it does, you've done something wrong or you shouldn't be using OOP. – Nicol Bolas Sep 17 '21 at 02:10
  • 1
    @KidKush420: "*It's just a thing that does something non-specific with Animals*" But it does something *very* specific with `Animal`s. So specific that it needs to know what kind of `Animal` that it is in order to do its job. The question is why it needs this. – Nicol Bolas Sep 17 '21 at 02:11
  • @Nicol Bolas Let's say it prints "bark" if it's a dog or "meow" if it's a cat to the console. It's just an example of the type of pattern I want, not real-world code. My real world code is for my job and heaps more complex. – KidKush420 Sep 17 '21 at 02:24
  • It'd be better to *know* what you want to do. Most of cases the Manager should just call an instance's function stead of "knows" what to do. However read my answer about slicing, it's an important issue if you didn't notice it. – Louis Go Sep 17 '21 at 02:33
  • @Louis Go Yes, thanks for you post. I was aware of that issue, but was kind of writing C++/java psuedo code. My bad I really should have used real compiling example. Anyway, it seems the visitor pattern may be what I'm looking for, it's just frustrating that something so simple in one paradigm ends up being much more complex in another, but I guess that's why there are different paradigms. – KidKush420 Sep 17 '21 at 02:42
  • @KidKush420: "*Let's say it prints "bark" if it's a dog or "meow" if it's a cat to the console.*" That's a `virtual` function; each `Animal` can "utter" some sound. `AnimalManager` has absolutely no need to be involved in that. – Nicol Bolas Sep 17 '21 at 02:51
  • @Nicol Bolas I'm not sure why it's so important what exactly it does, point it is does some side effect that can't be done from within each `Animal` subclass. Let's say it writes to a stream that the `Animal` type shouldn't know about – KidKush420 Sep 17 '21 at 04:25
  • 1
    @KidKush420: "*I'm not sure why it's so important what exactly it does*" The point here is that what you want is not a thing you *should* want. The problem you have encountered suggests that you have taken a wrong turn in your design somewhere. Either OOP is not appropriate to your problem domain, or you have poorly designed your interactions between objects. For example, if `Animal` shouldn't know about some stream, then clearly there needs to be some third type, produced by the `Animal`, that *does* know about the stream or can interact with it (like a string). – Nicol Bolas Sep 17 '21 at 06:05

1 Answers1

0

I'll suggest you provide the real use case instead of metaphor because you might have a XY problem. You may use RTTI here and there is another issue I want to elaborate.

Don't use void add(Animal a) which slices your object into just Animal and you will always get Animal instead of derived class Cat or Dog. Use void add(Aniaml& a) to keep the object or use a (smart) pointer.

#include <iostream>

class Animal{
public:
// Read Item 7 in Scott Meyers' Effective C++ if you don't know why you need it
#include <iostream>

class Animal{
public:
    virtual ~Animal(){};
};
class Cat : public Animal{

};
class Dog : public Animal{

};

class AnimalManager {
public:
#define REF < comment it to see what your code do
#ifdef REF
  void add(Animal& a) { 
#else
  void add(Animal a) { 
#endif
    using std::cout;
    using std::endl;
    if(typeid(a) == typeid(Cat)){
        cout << "Meow" << endl;
    }else if(typeid(a) == typeid(Dog)){
        cout << "Bark" << endl;
    }else{
        cout << "I'm just animal" << endl;
    }
  }
};
int main()
{
    Dog d;
    Cat a;
    AnimalManager m;
    m.add(d);
    m.add(a);
}

Live demo

With reference

Bark
Meow

Pass by value

"I'm just animal
"I'm just animal
Louis Go
  • 2,213
  • 2
  • 16
  • 29