π«£ BEING perspective: what am I?
Creatures in Heroes III have their own names and specific levels.
Each belongs to a different faction, but there are also neutral creatures. Some creatures can be upgraded. Each creature has a defined recruitment cost and a base growth rate (how many can be recruited each week). Creatures have specific stats (like attack, defense, hit points etc.), which are especially important during combats.
π Playing a board game? Start with the instructions!
When you focus on nouns, data structures and database tables for creatures, what clarifying questions can you ask based on this description to create a better "model"? And what answers are you likely to get?
- Question: What does a creatures have? Answer: A name...
- Question: How many characters can a creature's name have? Answer: It varies... probably up to 50 to keep the UI from breaking.
- Question: How do we define the level? Answer: A number from 1 to 7.
- Question: How many upgrades can a creature have? Answer: From 0 to 1.
Since we did so well with defining "what a creature has," we could continue and draw out the entire database, but see what King Julien has to say about that:
Only advantage of this approach is that I didn't waste much time on it. ChatGPT did the work quickly and beautifully laid out the tables. Such programmers will likely be replaced by AI soon. But you want to stay in the industry, right? So keep reading.
Starting a project with a database schema is like opening a board game (by the way, a Heroes III version in this form was recently released), looking at the contents: "OK, I have 50 cards, and I have a board"... and trying to play without reading the instructions. It's no surprise that with a similar approach to programming (without understanding the domain and its processes), we'll apply a solution suitable for CRUD everywhere β most board games also typically have cards and a board...
From the database tables diagram, do you already know what you need to do first? Or where you can parallelize the work of developers? Can separate teams work on it? Or what dependencies or user interface designs are still missing? And finally: does it bring you closer to understanding the business processes? Do you know why creatures have levels, or what is the point of upgrading them? First, let's focus on understanding the essence of the problem. We'll deal with the rest, like database details, later.
π Relax, relax... I do OOP and have classes, not tables!
So how do we model an "object-oriented" creature? Below, you can see the implementation in Kotlin and the object parsed into JSON (with example values).
data class Creature(
val id: String,
val level: Int,
val faction: Faction,
val growth: Int,
val upgrades: Set<Creature>,
val cost: Resources,
val attack: Int,
val defense: Int,
val damage: Range,
val health: Int,
val speed: Int,
val shots: Int = 0,
val size: Int = 1,
val spells: Set<Spell>,
val abilities: Set<SpecialAbility>,
)
{
"id": "Angel",
"level": 7,
"faction": "castle",
"growth": 1,
"upgrades": [
"Archangels"
],
"cost": {
"gold": 3000,
"crystal": 1,
"wood": 0,
"ore": 0,
"sulfur": 0,
"mercury": 0,
"gems": 0
},
"attack": 20,
"defense": 20,
"damage": {
"low": 30,
"high": 50
},
"health": 200,
"speed": 12,
"shots": 0,
"size": 1,
"spells": [],
"abilities": [
{
"type": "HATE",
"creatures": [
"Devil",
"ArchDevil"
]
},
{
"type": "ConstRaisesMorale",
"amount": 1
}
]
}
π΄ Accidental complexity and all tests red
Are the variables below well-named? Yes.
Do all these attributes belong to the creature or are they related to it? Yes.
So, is there anything wrong with this solution?
Before we answer that question, let's listen to two dialogues between a programmer and domain experts:
- Expert #1: "A hero ALWAYS belongs to a player."
- Programmer: "Are you sure? Could there ever be a case where a hero doesn't belong to a player?"
- Expert #1: "No, no... a hero on the map is always under some player's flag. That will NEVER change."
Satisfied, you create the perfect model that meets business requirements, in collaboration with experts, feeling that this is truly Clean Code:
data class Hero(
val id: HeroId,
val player: PlayerId
)
After some time, you talk to another expert:
- Expert #2: "In the Tavern, we buy a hero who doesn't belong to any player." π€―
- Programmer: "What? But Expert #1 said that a hero ALWAYS belongs to a player."
- Expert #2: "He must be wrong... I've been working here longer and I know better."
What happens to your code in such case?
You introduce a modification because the code must reflect the business logic.
data class Hero(
val id: HeroId,
val player: PlayerId?, // null only in tavern
val cost: Resources // cost of hiring a hero
)
So, it's finally a small change β just adding the possibility of null
in the player
field. Is that the end of the work? Not at all! Now, all the tests that created a Hero instance are failing and need to be updated (which means they no longer protect you from regression). Additionally, everywhere you reference hero.player
, you have to introduce an if
statement to check against null.
It's still manageable if you're using Kotlin (or another language with sophisticated Null Safety) and the compiler alerts you, rather than causing abug in production. Even if you manage to handle this, you now want to merge the changes and... bam (as my 1-year-old son says)! It turns out another developer has already overwritten your changes, and we have a marge conflict! Your morale and work efficiency drop like a hero's army in Heroes III when you mix different factions. Wouldn't it be simpler, instead of modifying, to apply the Open-Closed Principle at the architecture level and have two separate models? Models that even separate teams of developers can work on without any issues. Let's see an example below.
π Bad habits β here be dragons!
Different attributes describing a Hero are needed in the context of the tavern compared to when exploring the adventure map. Simply add another namespace/module, or whatever it's called in your environment, and create two separate classes. Nothing is limiting you here. Unless, of course, you're still thinking in terms of the Hero
table and adding a nullable column, or the relationships between tables. This is how we were taught from the beginning, and fighting bad habits is the hardest. Those habits are like dangerous dragons, but if you defeat them β you and your project will receive greater rewards than after beating a Dragon Utopia.
When you hear conflicting information from experts about the same noun, trying to reconcile them in a single model introduces what's known as accidental complexity. Now, every programmer, even one who knows almost the entire system (but not the tavern and hero hiring), will ask you: "Dude, why do I have to check for null here?" And that's the least dangerous form of this problem... both of you will just waste some time.
As a result, such problem exists only in the code and make the domain itself harder to understand than it really is. Is one of the experts lying to you or incompetent? Not at all! In the tavern, you can buy a hero, but you can't buy any of the heroes moving on the map because they belong to you or another player.
Such inconsistencies in "business" statements are a clear sign that we should now talk about behaviors rather than move forward with nouns. The noun "hero" is the same, but the behaviors - operations that can be performed on the object - are completely different, depends on the context.
If you want to actively participate in modeling the solution or just don't miss the upcoming parts, let's sign up for my mailing list HERE.
Top comments (0)