It used to be fairly common for people to call a getter, do some calculation on it, then call a setter with the result. This is a clear sign your calculation actually belongs to the class you called the getter on. "Tell, don't ask" was coined to remind people to be on the lookout for that anti-pattern, and it worked so well that now some people think that part is obvious
(source: Karl Bielefeldt's answer)
This is by far not the first time I hear a piece of such advice.
Yet, looking at the game I'm trying to make, I simply can't seem to see how to bring this code to conformance with this advice. Admittedly, this anti-pattern is rampant there.
This is the current design: Monsters can be under Effect
s and can have Action
s queued for execution. Monsters are instances of a class specific to their species, which inherits from the Species
abstract class - which in turn holds state such as their current HP, stamina, a list of Effect
s they are under or a list of Action
s that are queued for execution.
Let me put here a few examples why it is so hard for me to conform to this advice. For example, let's pay a closer look on the Attack
Action
. Seems straightforward enough: total attack power is computed as Attack.power * attacker.power
(attacker
is a monster) and this value is substracted from the defender's current HP. But now, where to put this logic? To conform with this piece of advice we would have to have a makeAttack
method on the attacker, which passes its current power
to the attack, which then multiplies this value by its own power and passes it to the defender's beAttacked
method, which then substracts this amount from its own HP. And, indeed, this is how I started this.
However, it turns out, this is only the simplest case. The Attack
on its own can be under Effects
. Soon, as the complexity of the game rules was growing, I was finding myself making more and more fields public and moving more and more logic to the effects and actions themselves and away from whatever is under these effects and whatever these actions are supposed to affect.
Consider these requirements:
- If an
Attack
is of typeFire
AND it penetrates the defender'sshield
then aBurn
debuff is applied to the defender whose strength is proportional to the amount of fire damage that has passed through the shield.- This necessites either querying the defender stats after the damage by the
Attack
or polluting the defender'sbeAttacked
method with additional arguments and logic or having anapplyBurn
method on the defender.
- This necessites either querying the defender stats after the damage by the
- If the defender is blocking (is under a
Block
effect) he mitigates a portion of the incoming damage at the cost of some of hisStamina
(he can't, therefore, mitigate more damage than he has stamina for). How much damage is exactly mitigated depends on the ratio of both monsters'power
.- Again: put this logic in the
Block
buff, which then has to query the state of both monsters AND the power of the attack? Or pollute other classes with unecessary methods?
- Again: put this logic in the
- If the defender is under a
Dodge
buff, he has a chance of avoiding the damage completely, depending on both monsters'speed
ratio.- Same problems as above...
- Attacks that have the
AreaOfEffect
field set toTrue
cannot be dodged at all... - The battle arena itself can be under effects - for example,
Hurricane
orConflagration
. But- there is a combo - if both effects are in play, thy are both removed and a third effect,Firenado
is placed, which is more powerful than bothHurricane
andConflagration
at the same time.- When
Conflagration
is being placed it has to ask the battlefield if there is aHurricane
already and remove it if necessary...
- When
Force
attacks ignore some portion of shield completely,Pierce
ones are instead more powerful to the extend they are blocked by shield- Make it work with the blocking semantics...
I could go on and on and on. The bottom line is: the only alternative to tell, don't ask seems to be... For each subclass of Effect
that can affect a monster, make a method on the monster. For each subclass of Effect
that can affect an Action
, make a method on that action. For each subclass of Action
that can target a monster, make a method on that monster.
I don't like this. Aside from the clutter such a solution would introduce, this kind of defeats the purpose of having separate Effect
and Action
subclasses. Logic that is semantically theirs would, in the end, be moved to methods of other classes! So, AFAIK, tell, don't ask is supposed to precisely avoid this what it would now introduce! If I have, say, 15 different types of Effect
s, then it makes sense to enable these Effects
to simply operate on instances of IMonster
whose requirements are kept simple (IMonster
is supposed to have HP, stamina, etc, but not necessarily provide a burnMe
method) rather than instances of IBurnableMonster
which kind of makes little sense because every monster is burnable.
This also seems more elegant to me insofar as modifying the game's rules (adding a new effect or something like that) is supposed to boil down to only adding a new Effect
subclass and/or making changes to one Effect
subclass rather than also considering all classes this Effect
would affect. (OK I'm lying here: the practice shows this is not the case, however, this is what I - ideally - wanted to achieve through this design).
So... currently I have a lot of getters and a lot of public fields. Which, I suppose, is a little tragedy in the eyes of any experienced engineer. As the number of special cases was growing, I even removed the makeAttack
and beAttacked
methods from the monster and instead put this logic in the Attack
action.
However, now that I described the "architecture", how should this be handled in a more right-handed way?