Heroes of DDD: BEING perspective. What am I?

mateusznowak

Mateusz Nowak

Posted on June 16, 2024

Heroes of DDD: BEING perspective. What am I?
Cover sources: Heroes of Might and Magic III (Ubisoft) and Heroes III Board Game (Archon Studio).

🫣 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.

Heroes3_DatabaseModel

One huge database model for the entire Heroes III created by ChatGPT. But what's the point? In college, I would probably get an A grade for this, but in practice, it deserves no more than a D.


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>,
)
Enter fullscreen mode Exit fullscreen mode
{
  "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
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

🔴 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
)
Enter fullscreen mode Exit fullscreen mode

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
)
Enter fullscreen mode Exit fullscreen mode

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.

EyesGif

How do such changes hit your project?


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.

Heroes3 Hero Bounded Context

Actually, splitting the model increases the number of classes, but ultimately creates more models with less complexity.

🐉 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.

DragonUtopiaHeroes 3

Dragon Utopia — In Heroes III, if you want to acquire rare artifacts, you must defeat the dragons guarding them.


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.

💖 💪 🙅 🚩
mateusznowak
Mateusz Nowak

Posted on June 16, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related