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 Effects and can have Actions 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 Effects they are under or a list of Actions 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
Attackis of typeFireAND it penetrates the defender'sshieldthen aBurndebuff 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
Attackor polluting the defender'sbeAttackedmethod with additional arguments and logic or having anapplyBurnmethod on the defender.
- This necessites either querying the defender stats after the damage by the
- If the defender is blocking (is under a
Blockeffect) 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
Blockbuff, 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
Dodgebuff, he has a chance of avoiding the damage completely, depending on both monsters'speedratio.- Same problems as above...
- Attacks that have the
AreaOfEffectfield set toTruecannot be dodged at all... - The battle arena itself can be under effects - for example,
HurricaneorConflagration. But- there is a combo - if both effects are in play, thy are both removed and a third effect,Firenadois placed, which is more powerful than bothHurricaneandConflagrationat the same time.- When
Conflagrationis being placed it has to ask the battlefield if there is aHurricanealready and remove it if necessary...
- When
Forceattacks ignore some portion of shield completely,Pierceones 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 Effects, 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?