0

Would there be any downsides of creating sub-interfaces for virtual member functions that have different parameter types? A drawing of this is shown in the image attached.

Apple and Orange do not share similarities and they themselves are interfaces. Making a higher level Fruit interface would run into the same issue of an interface of interface issue.

Is this an anti-pattern? And if so what are some ways to avoid this?

enter image description here

Note: the members included are for simplicity to avoid verbosity. The apple and orange classes are very different classes; hence, they are "apples to oranges" and would share an empty interface.

Clarification: To put the design in a more concrete example, I added the image below.

enter image description here

The point of having the sub interfaces is to be able to encapsulate the data loaded into classes that use different private member variable types.

Apple* apple = new Fuji();
FoodEater* person = new Jerry(); //people named jerry have their own way of eating apples
person->Load(apple);
person->Eat("person is eating an apple.");

This person will implement Eat() differently than an instance of a Mike.

I'm mostly interested if this is considered poor design practice to have interfaces with inheritance.

Update:

On second glance, it may be that the language I am trying to use (C++) doesn't allow this behavior since I am unsure if I could call FoodEater->Load(). I may have to use a template type for a virtual load in the top level superclass.

  • 4
    It's pretty much impossible to render judgement on this hierarchy when you only provide placeholder names for things. What are these classes actually supposed to mean? Specifically, why does Superclass exist, why do both A and B have to inherit from it, and why do they both have Load functions that take different types? – Nicol Bolas Sep 22 '18 at 1:12
  • I added a concrete example, but I may have already answered my question since it looks like the code example wouldn't compile. Is it recommended to remove this post since I found error in the design or leave it up? – Christian Gabor Sep 22 '18 at 2:33
1

Some observations:

Building an inheritance tree of interfaces may quickly defeat the purpose of using interfaces in the first place. One major benefit of interfaces is that they allow you to apply type safety independent of any class hierarchy. You may want classes in different class trees to implement the same features. Interfaces are great for this because of their independence on anything. If you start stringing them up together in a rigid tree, you will make them depend on each other and you will have recreated the problem situation you wanted to escape from.

Making one interface descend from another violates the interface segregation principle.

To the reader of your code it would be harder to get an idea of what your class does from the class declaration line. If he sees only the one god-interface that may have inherited other interfaces it will likely not be obvious. Compare

SomeClass : SuperRichFeatureSet

to

SomeClass : ISerializeable, IComparable, IEnumerable

The latter tells you a lot, the former makes you guess at best.

0

For what I believe you are trying to do, you can't utilize dynamic polymorphism in a context that requires knowledge of the specific types (including subtypes of an interface). You'd have to at least be aware that Jerry is an AppleLover to make a translation of the code you wrote compile:

Apple* apple = new Fuji();
AppleLover* apple_lover = new Jerry();
apple_lover->Load(apple); // this call requires that we know that Jerry
                          // is an AppleLover. The compiler can't know
                          // the existence of this method given only a
                          // pointer to FoodEater.
apple_lover->Eat("person is eating an apple.");
...

One practical way to avoid this particular scenario is to generalize Food and create its own interface which operates on generalized FoodEaters like:

class Food
{
public:
    virtual ~Food() {}
    virtual void eat(FoodEater& eater) = 0;
};

In that case you can define different subtypes of food which cause different effects to the food eaters through the generalized interface available for all food eaters. Some foods might heal the eater, others might poison them etc. As long as you can cause all these side effects through a generalized FoodEater interface, then the eat function of Food can cause all these things to happen without any specific knowledge of the specific type involved for a particular FoodEater (without knowing about a Jerry or AppleLover), so to speak. In that case you can do like:

Food* apple = new Fuji();
FoodEater* eater = new Jerry();
apple->Eat(eater);
...

Or vice versa. If everything that can possibly be done with Food can be known through its general interface, then we can do:

class FoodEater
{
public:
    virtual ~FoodEater() {}
    virtual void eat(Food& food) = 0;
};

Food* apple = new Fuji();
FoodEater* eater = new Jerry();
eater->Eat(apple);
...

Which might be more intuitive in terms of the client code but requires the Food interface to sort of be able to completely describe what sort of effects that consuming any possible type of food can have on the eater, e.g. Or you can just make a convenience function while making the eater pass itself via pointer/ref to the food being consumed. Put very crudely you have to make a concrete food eater capable of working without specific knowledge of the type of food he/she is eating, or you have to make a concrete type of food capable of working on an eater without specific knowledge of the type of food eater he/she is.

Whatever you do, if you want to utilize polymorphism and its benefits (including static polymorphism), then you have to write the code in a way that allows you to generally work with an object without knowing its concrete data type. You have to sort of write the code in the most generalized way possible using the common denominator interface that everything has in common, and that common demoninator interface must be sufficiently complete to do everything you want to do to avoid any need to downcast. You can't necessarily make exceptions to the rule for AppleLovers and so forth without losing some of the generality involved in the code and having to explicitly write it to be aware of this AppleLover type.

Where creating derived interfaces can be more practically useful is when you, say, only upcast from concrete types to the derived interface types before working with a more generalized interface. An example is if you have a particular section of code which is working specifically with AppleLovers and their subtypes like Jerry instances and others with no need to more generally work with FoodEaters (maybe that's a need that comes outside the context). In that case, if the concrete type is known upfront and then we only need the polymorphism on AppleLovers, then such a derived interface might be useful in that particular context when you only need to generalize to that level. There's no downcasting involved, so to speak.

Why?

In case this question is raised about why the language is designed this way, ask yourself what Jerry is supposed to do if you try to feed him an orange through the FoodEater interface. There's no code to handle that case, because he's not an OrangeLover and FoodEaters in general haven't been programmed to know what to do with oranges given the way you designed it. I worded that in somewhat of a crude way but hopefully one that sort of clicks and gets some eurekas from someone at student level because it's not a strange design of the language or necessarily something to work around with static polymorphism or any alternative. An interface can only do what it's been programmed to be aware of doing.

0

My answer tries to cover grounds which have not yet been covered in existing answers. I recommend reading the existing answers first.


What do we already know?

  • To call AppleLover.Load(Apple), the caller must have an AppleLover and an Apple.
  • To call OrangeLover.Load(Orange), the caller must have an OrangeLover and an Orange.

The entire discussion on OOP is beneficial if only:

  • The caller might have an AppleLover or OrangeLover without knowing it.
    • For example, it might only know that it has a FoodEater.
  • The caller might have an Apple or Orange without knowing it.
    • For example, it might only know that it has a Fruit.
  • A caller (function) who might not know the full specifics might want to delegate to another caller (function), in which case it is unable to propagate the specific type information.

What is my gut feeling?

  • I feel that judicious use of dynamic_cast, side-casting, and down-casting should be justified, if all other alternatives require code that is too ugly.
  • It might actually be okay (acceptable) to have sub-interfaces inherit from a common interface.

Why would we need FoodEater? Or: what benefit do we get by having FoodEater?

  • We would like to store instances of objects in containers (such as vector) or handles (such as shared_ptr) knowing that each instance supports a FoodEater interface, without more specific knowledge.
  • We would like to use such instances via the FoodEater interface, without more specific knowledge.
  • (Some other use cases which I cannot think of right now)

Devil's advocate - what harm would we do if we get rid of FoodEater?

Suppose we modify AppleLover and OrangeLover by simply copying and pasting the methods from FoodEater into AppleLover and OrangeLover, and then remove the inheritance relationships between these interfaces.

  • FoodEater
    • Eat(string), Foo(string)
  • AppleLover
    • Eat(string), Foo(string), Load(Apple)
  • OrangeLover
    • Eat(string), Foo(string), Load(Orange)

What is the harm if we modify the interfaces as such?

  • We can't store instances of AppleLover or OrangeLover in a container or handle of type FoodEater.
    • Note, however, we can still store a shared_ptr of AppleLover or OrangeLover in a shared_ptr<void>. This is a secret super-power of shared_ptr implementation. However, it would require additional safeguards and casting to achieve safety.
  • We cannot write a single piece of code that can perform Eat(string) or Foo(string) on either an instance AppleLover or OrangeLover.
    • However, we can still write function templates, and then instantiate for the type AppleLover and OrangeLover.
    • Or, we can use adapters, which is explored below.

Interfaces vs Adapters

Benefits of adapter pattern

  • Not subject to the limitations of interfaces and inheritance hierarchy.

Drawbacks of adapter pattern

  • The adapter can only be created by someone with knowledge of the type (whether AppleLover or OrangeLover) of the implementation.
  • Only the creator of that adapter knows about the existence of it. Once the adapter is created, it will have to be propagated to other places of code that use the adapter.
  • It is hard to enforce one-to-one relationship between an object and the instances of its adapters. Code may become fragile if two adapter instances of the same type are created on the same concrete object.
  • Navigating from the object to its adapter is not possible. In other words, the object (Jerry, Mike) knows nothing about the existence of adapters. Given a pointer or reference to the object (Jerry, Mike), one cannot ask for access to the adapter. It is possible to work around this limitation; see example below.

Situations where there are no drawbacks

  • If the adapter does nothing other than adapting between function calls, then the adapter can be created and destroyed at will. However, to instantiate an adapter, the code must know the type (AppleLover or OrangeLover) of the object.

The drawback can be eliminated (by having the concrete implementation be the creator and owner of the adapter), but which requires some boilerplate code. It reduces ambiguity surrounding the use of the adapter, but it does not completely prevent all misuse.

class Jerry
{
private:
    const std::unique_ptr<Adapter> aa;
    // any other stuff

public:
    Jerry(...)
    {
        // any other stuff
        aa = std::make_unique<Adapter>(*this);
    }

    Adapter& getAdapter() const
    {
        return *aa;
    }
};

What if we implement an adapter between FoodEater and (Fruit) Lover?

In other words, what if we implement an adapter that covers the methods: Eat(string), Foo(string), and nothing else?

Sample code

class AppleLoverToFoodEaterAdapter : public FoodEater
{
private:
    const std::shared_ptr<AppleLover> mp;

public:
    AppleLoverToFoodEaterAdapter(
        const std::shared_ptr<AppleLover>& p) 
        : mp(p) 
    { 
        if (!mp) throw bad(); 
    }

    // This method helps satisfy FoodEater interface.
    void Eat(const std::string& s)
    {
        // This method knows that AppleLover has a method Eat(string)
        mp->Eat(s);
    }

    void Foo(const std::string& s) 
    {...}
};

What if we implement an adapter that abstracts over the Load method?

This idea is a twist on the factory method pattern.

Even though this approach looks verbose and pointless in its current form, the code can be rolled into the factory method for Jerry (a concrete implementation of AppleLover) and Mike (a concrete implementation of OrangeLover), which removes the verbosity.

Whether this is pointless depend on whether there is a benefit from abstracting over the Load method.

class GenericLoader
{
public:
    virtual ~GenericLoader() = 0;
    virtual void Load() = 0;
};

class AppleLoader : public GenericLoader
{
private:
    std::shared_ptr<AppleLover> mp;
    std::shared_ptr<Apple> ma;

public:
    // To create this loader, the caller must have an AppleLover 
    // and an Apple.
    AppleLoader(
        const std::shared_ptr<AppleLover>& p, 
        const std::shared_ptr<Apple>& a) 
        : mp(p), ma(a) 
    {
        if (!mp || !ma) throw bad(); 
    }

    // This method helps satisfy GenericLoader interface.
    void Load()
    {
        // This method knows that AppleLover has a method Load(Apple)
        mp->Load(*ma);
    }
};

Can we follow the Interface Segregation Principle in C++ as we do in C#?

A concrete C++ class can inherit (implement) multiple abstract classes (interfaces), and more.

But how do we write a function that will accept an argument which must implement two or more interfaces?

Suppose we start with these C++ interfaces, by removing inheritances between interfaces and also removing all overlapping methods, according to the Interface Segregation Principle:

  • FoodEater
    • Eat(string), Foo(string)
  • AppleLoader (renamed from AppleLover)
    • Load(Apple)
  • OrangeLoader (renamed from OrangeLover)
    • Load(Orange)

And the following concrete implementation:

  • Jerry : public FoodEater, AppleLoader
  • Mike : public FoodEater, OrangeLoader

We want to write a function that will call AppleLoader.Load(Apple) as well as FoodEater.Eat(string). The argument must therefore implement both AppleLoader as well as FoodEater.

void LoadAndEat(T& t, Apple& apple, const string& stringToEat)
{ ... }

What would "T" be?

In C#, we can constrain T as follows:

void LoadAndEat(T t, ...) 
    where T: FoodEater, AppleLoader
{ ... }

In C++, this is trickier.

Given that AppleLoader doesn't inherit from FoodEater, it is possible for one to create a concrete class that implements AppleLoader but not FoodEater. While this is the gist of Interface Segregation Principle, this freedom may be undesirable within this particular scenario.

To prevent mistakes, a comment should be added to the AppleLoader declaration that says "concrete implementations of AppleLoader are expected to also implement FoodEater."

Going back to the LoadAndEat(T t, ...) method. There are several approaches. In approaches 1 and 2, the try-catch block can be simplified by dynamic_cast to a pointer type, in which a cast failure results in a nullptr instead of an exception (std::bad_cast) being thrown.

Approach 1 - pass in an AppleLoader, side-cast to FoodEater

void LoadAndEat(AppleLoader& appleLoader, Apple& apple, const string& stringToEat)
{
    appleLoader.Load(apple);
    try
    {
        FoodEater& foodEater = dynamic_cast<FoodEater&>(appleLoader);
        foodEater.Eat(stringToEat);
    }
    catch (std::bad_cast&)
    {
        // Decide what to do. This happens only if a programmer ignores
        // the warning we put in the comments that AppleLoader are
        // expected to also implement FoodEater.
    }
}

Approach 2 - pass in a FoodEater, side-cast to AppleLoader

void LoadAndEat(FoodEater& foodEater, Apple& apple, const string& stringToEat)
{
    try
    {
        AppleLoader& appleLoader = dynamic_cast<AppleLoader&>(foodEater);
        appleLoader.Load(apple);
    }
    catch (std::bad_cast&)
    {
        // Decide what to do. This is far more likely to happen, since
        // we can't prevent the caller from passing in an instance of 
        // OrangeLover (Mike) 
    }
    foodEater.Eat(stringToEat);
}

Approach 3 - write a function template with a complicated SFINAE "type filter"

template <class T>
auto LoadAndEat(T& t, Apple& apple, const std::string& stringToEat)
    -> std::enable_if_t<
        std::is_base_of_v<FoodEater, T> && 
        std::is_base_of_v<AppleLoader, T> >
{
    dynamic_cast<AppleLoader&>(t).Load(apple);
    dynamic_cast<FoodEater&>(t).Eat(stringToEat);
}

Other directions to be explored:

  • std::variant (C++17), or boost:variant

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.