2

For example, for base and child classes, if all child class need a class member, eg: baseHp, which represents the base hp to calculate the actual hp of monsters in a game:

public class Monster{
    private int baseHp;
    private int lv;
    public int getHp(){
        return this.lv*this.baseHp;
    }
}

public class WeakMonster extends Monster{
}

public class StrongMonster extends Monster{
    public int buffMultiplier;
    public int getHp(){
        return this.buffMultiplier*this.lv*this.baseHp;
    }
}

which all Monsters needs "baseHp", so it is reasonable to put "baseHp" in base class. And "buffMultiplier" is used by StrongMonster only, so it is reasonable to put it in child class only.

However, what if I need to add another Monster which doesn't use "baseHp"? eg:

public class SpecialMonster extends Monster{
    public int getHp(){
        return 100;
    }
}

? The hp of SpecialMonster is always 100, and class member "baseHp" becomes useless in this class. It inspire me a question : What is the threshold of "usage" of moving a class member in base class to child class? Is it ok to have some child classes that don't use a parent class member at all? If not, when should I move a parent class member to child class?

4
  • 1
    I don't see your problem. Simply make getHp virtual. That is what non abstract virtual methods are for, customizing base class behaviour. It's like toString.
    – Ccm
    Oct 18, 2023 at 6:39
  • 1
    "Is it ok to have some child classes that don't use a parent class member at all?" - sure, sometimes it is.
    – Doc Brown
    Oct 18, 2023 at 6:51
  • @Ccm: I think the OP is already talking about virtual methods (they have probably Java in mind, where all methods are virtual by default).
    – Doc Brown
    Oct 18, 2023 at 6:52
  • "what if I need to add another Monster which doesn't use baseHp?" That is generally going considered to be an LSP violation. Subclasses must follow the contract outlined by their base class. If the Monster contract entails the ability to take damage (as reflected by observing the HP), then deriving Monster and refusing to implement this behavior is a violation of LSP.
    – Flater
    Oct 19, 2023 at 3:59

2 Answers 2

5

There's no simple recipe that you could apply mechanically. Thinking about the correct solution if there are several options is part of the developer's job, and t needs to be done for the specific case.

You need to consider readability, consistency, etc. For example, does creating an artificial abstract class for monsters which do have a baseHp member make your code better readable and maintainable or would you just do it to satisfy an uneasy feeling about not using that member in specific subclasses?

0

In short

Private is private. The real question is how much could break by short-circuiting its internals.

But also, overthinking it will not help to advance. Start with a simple design, yet sufficiently clear for not having to rely on the internals. If it gets more complex, refactor and prefer composition over inheritance.

More details

Class represent a whole with some invariants, and here some parts are missing: in the real code you would probably have some means to initialize baseHp probably in the constructor. And you would have ways to change baseHp, for example incrementing or decrementing it with some methods such as: takeHit().

Your shortcut could then lead to an inconsistent behavior, where some part may rely on the private implementation and some on the override of the public method. Maybe even at some time the monster will think being dead while the getHp() still insolently returns 100.

The basic advise here, is not to interfere with the base class internal business. This is by the way called "history constraint" in LSP.

Four solutions in your case:

  • expose updater of hp in the base class, ensure that no other method changes the private hp value. Then make sure special monster get its hp initialized as needed and override its hp change methods.
  • outsource hp management using the strategy pattern. Classes would have a default strategy and special monster a special strategy. This would by the way allow for fine tuning the gameplay with the strong monster, e.g. multiplying the increases of hp, but making it still more difficult to decrease it. When this approach is used also for other behaviors, it naturally leads to the entity component system pattern, a popular design in gaming industry.
  • change your base class to allow some phantasy for the derived classes about the hp. The template method design pattern will be your friend. But this adds a lot of complexity and seems overengineered if just to handle a few exceptions.
  • Deepen your class hierarchy with an abstract rawBaseMonster without internal implementation for hp, and let baseMonster inherit from it. special monster would inherit from raw. But this will lead to complex hierarchies, and quickly you'd realize that there are all sorts of other exceptions, and redundant branches. Clearly a sympto that inheritance was overused. Sooner or later you'd come to the entity component system.

The key to your problem is to think carefully about how the state of your base object is to be used by its children. Do not to overengineer it from start, or you'll risk to end up stuck in thinking. Start with your base class, clean it to expose the hp management better, and refactor only once you really need it.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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