0

can base class contain protected methods that are not to be used by all descendant classes?

I have this situation:

  • EntityA: has some methods that handle activities
  • EntityB: has some methods that handle products
  • EntityC: has methods that handle both activities and products

I use C#, so multiple inheritance is not possible.

Is it OK to have a base class with protected methods that deal with both activities and products, and have all 3 classes inherit from this base class?

Edit: I never understood what do we gain by using composition instead, how do you implement such approach in a more complex scenario. Let me give you a bit more expanded situation:

  • EntityA: needs to use internally FilterActivitiesByType() method, and has public methodsA1(), A2(), A3()
  • EntityB: needs to use internally FilterProductsByType() method, and has public methodsB1(), B2(), B3()
  • EntityC: needs to use internally both FilterActivitiesByType() and FilterProductsByType(), and has public methodsC1(), C2(), C3()

So, both FilterActivitiesByType and FilterProductsByType are not to be exposed through interface. These methods can in many cases be even static. I can now put those two methods in IFilterActivityHelper and IFilterProductHelper classes, respectively, and use DI in constructor to make it available to EntityA, EntityB and EntityC classes. This is, I believe what you all advise by composition.

Now, Imagine that I have several activity helper methods, and several product helper methods. I could put them in each own specific helper class (1st), or I could put all activity helper methods in a single class ActivityHelper (2nd). (1st) Means that I will have lots of instances to inject in constructor, for each separate helper method. (2nd) Gives us the the same situtation as in my original question with inheritance, since I now have IActivityHelper inserted in ActivityA, and ActivityA needs to use only 2 methods out of 5 that it exposes.

So, can someone elaborate how to do the composition properly in such cases?

  • 7
    Why do you want to use inheritance? It is a tool, not a goal. – Jacob Raihle Jul 31 '18 at 11:11
  • You're violating the "is a" relationship in this way. A derived class B which doesn't implement all methods of A is a situation where B is not an A. If you want to be able to have a common relationship between EntityA, EntityB, and EntityC, have them derive from an interface common in all three. – Neil Jul 31 '18 at 12:03
  • @Neil I have edited the question. Can you please elaborate how would properly implement composition in given situation? – Goran Jul 31 '18 at 15:15
5

Is it OK to have a base class with protected methods that deal with both activities and products, and have all 3 classes inherit from this base class?

No that would be bad(tm).

Use interfaces and composition instead

public class EntityC : IActivities, IProducts
{
    private IActivities EntityA;
    private IProducts EntityB;

    public Activity GetActivity()
    {
        return this.EntityA.GetActivity();
    }

    public Product GetProduct()
    {
        return this.EntityB.GetProduct();
    }
}

If you make a new base class with all the functionality it will just keep getting bigger and bigger until it does everything.

Post Edit:

(1st) Means that I will have lots of instances to inject in constructor, for each separate helper method.

(2nd) Gives us the the same situtation as in my original question with inheritance, since I now have IActivityHelper inserted in ActivityA, and ActivityA needs to use only 2 methods out of 5 that it exposes.

Well first off don't call them 'Helpers' It may seem like a trivial naming nitpick but actually its the root of the problem.

If you call a class Helper or Manager or something, then you have no idea what methods should go in it.

Have a Repository or a Filterer or a ShoppingBag, then you know which class a new method should go in. Thinking of the name forces you to consider that class's single responsibility and will give you sensibly sizes classes with sensible groups of methods

  • Additionally (extra info for OP - not a correction to the answer), it's mainly the Liskov Substitution Principle that is being violated here. The base class suggests that it can both handle activities and products, but its subtype apparently can not handle one of them, thus breaking the contract of its base type. – Flater Jul 31 '18 at 12:25
  • 1
    @Flater hmm the hypothetical base class's methods are protected – Ewan Jul 31 '18 at 12:27
  • Fair point. I'm not 100% sure if this means LSP doesn't apply or whether it's just an atypical case of LSP. – Flater Jul 31 '18 at 12:42
  • @flater yeah, its an odd one. I have a feeling the LSP is not violated, because there literally no functionality! But protected isnt private. there could be an interesting edge case – Ewan Jul 31 '18 at 12:46
  • @Ewan The point is that EntityC does not need all methods from EntityA and EntityB. I have edited the question, can you please elaborate? – Goran Jul 31 '18 at 15:13
0

I think what you are describing is FINE. There is no reason not to do it, but there are some issues about HOW to do it.

You use interfaces to express what you want to be seen externally - your design. But you may use inheritance to express code sharing - especially if you use private inheritance.

The key is to define INTERFACES for the essence of A-Activities, and for the essence of B-Activities.

Then if you have an implementation of A, and an implementation of B, that implement those interfaces.

And you can then create C, supporting the two interfaces. And if you choose to use private inheritance or aggregation, or some other means, is completely your call - whatever is most convenient.

interaceA {
  .... some methods;
};
interaceB {
  .... some methods;
};

ONE WAY:

class A1 : implement interaceA {
};
class B1 : implement interaceB {
};
class C1 : implements interface A, implements interfaceB {
  A a;
  B b;
 ....
};

ANOTHER REASONABLE WAY:

namespace private { class myhelper_ : implements A, implements B { .... }; }

class A2 : implement interaceA , private::myhelper{
};
class B2 : implement interaceB, private::myhelper {
};
class C2 : implements interface A, implements interfaceB, private::myhelper {
 ....
};

EITHER way, you have captured your two interfaces, and how you implement them is a hidden implementation detail.

0

Interfaces and base classes should provide a guarantee for how instances of your class can be interacted with. Essentially, they are a mechanism for polymorphism (where the base class can also provide functionality and apply restrictions to its descendants). This is not what you are trying to achieve here, so there is no reason to prefer inheritance, so you should prefer composition.

Composition has at least two advantages pertinent to this case:

  • You can do it more than once at a time
  • You can choose which parts of the composed objects you actually use

This means that you can choose to have a helper for activities, and a helper for products (giving one to A, another to B, and both to C). Or you can choose to have a helper for both activities and products (giving it to A, B, C), without anyone outside your classes noticing a difference. While your base class would have no public interface, it would still affect hypothetical descendants of A, B, and C, and getting access to those protected methods means you have to give up on any other base class that would be useful.

Composition also enables you to use dependency injection, which is not an option when you are depending on your base class. However, you can avoid this if it is not useful to you, and create instances of the helpers internally. It probably will be useful for testing if nothing else. If you find yourself injecting too much, A, B, and C are probably doing too much. If you want to keep the client code simple, you can provide default constructors using default helper implementations.

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.