Start from the simple, complicate things when you must.
Very simply put, avoid any level of complication/abstraction unless it actually solves more problems than it creates.
In order to cut down on fluff, but also ensure that you don't cut corners either, it helps to err on the side of simplicity, and elaborate as you see fit.
For example, first develop a Player
class that handles all of the player logic: movement, rendering, stats, ... As you go, re-evaluate that decision.
- Maybe you now have an enemy class and want to reuse the movement logic
- Maybe you now have a level which also needs to be rendered
- Maybe all the player logic is too much to keep organised in a single class
Whatever the reason, identify the missing abstraction, then implement it. Respectively:
- Create a generalized
MovementHandler
which can service any Player
or Enemy
.
- Create a more generalized rendering logic, which is then implemented by all the things that need to be rendered.
- Start subdividing your
Player
class into (composition!) subclasses such as PlayerMovement
, PlayerStats
, ...
This way, you don't overshoot from your first version, and you only build the things that you need.
This is a repeating pattern in development:
- Don't prematurely optimize. Just make something that works, and deal with performance issues if and when (!) they occur in the future.
- Don't abstract things that do not need abstraction yet - unless you are adamant that the abstract will be needed down the line (e.g. because the requirements are already known, just not implemented yet)
- Don't build things that are not part of the requirements.
- Caveat: unless that thing helps you get to your requirements quicker (i.e. something that assists the developer without necessarily making it into the final product). A great example here is debug logging or automated testing.
- By waiting for multiple implementations to warrant a shared abstraction, you ensure that you have good examples of precisely what these multiple implementations should and shouldn't (!) share. If you build your abstraction based on a single example, you are liable to make the wrong abstraction because it's unclear which parts will be reused and which will not.
Don't try to get it right the first time.
You won't. Instead, focus on clean coding practices, because what they do is enable you to minimize the effort/impact of having to make a change further down the line.
Having to make a change is practically inevitable, and actually a good thing (you didn't overbuild or violate YAGNI prematurely), and clean coding makes sure that your life doesn't become a living hell when you need to reshuffle the implementation in an established codebase.