Heroes of DDD: BEHAVING perspective. What do I do?
Mateusz Nowak
Posted on June 18, 2024
Listen to a few statements from Heroes III players:
- I won't capture that mine because it's guarded by a horde of creatures.
- I need to build that dwelling, because it allows to upgrade my level 7 creature.
- For now, I won't attack with this creature, I'll just wait.
- The recruitment cost of this creature is too high; I don't have enough resources in my kingdom.
- Oh no! Plague week, and I haven't recruited the entire creatures population.
- I can't move because this creature has been blinded.
When you model behaviors rather than data structures, you can ask many more questions that bring you closer to understanding the essential elements of the analyzed process:
- Does capturing a mine always involve a battle?
- What else needs to happen besides building the dwelling for me to upgrade a creature? Can I only upgrade in dwellings I have built?
- Can you attack and wait? What other options are there?
- Where do resources in the kingdom come from? How is the creature cost calculated?
- Plague week? What is that? How often does it occur? Are there any other "special weeks"?
- Is "blinding" the only condition that prevents a creature from moving?
There is no bulletproof method or a process that always works 100% for gaining domain knowledge. Experience and intuition are needed here. That's why we should practice modeling wherever possible, even while playing games. Because this is the essence of our work; coding is just a formality. However, you can help yourself by using patterns that others have already invented. Let's employ strategic DDD techniques, such as the Big Picture phase of EventStorming.
🚁 EventStorming Big Picture, a helicopter view
Now, take a look below at the events we identified during such an EventStorming session in the Heroes III domain. We will now focus on the section where domain experts mentioned creatures.
The presented sticky notes symbolize events at different levels of abstraction and refer to various contexts.
- Some experts focused on the details of combat, even noting specific spell effects, like blinding (Creature Blinded).
- Others described recruitment (Available Creatures Changed / Creature Recruited) or possible operations in towns (Creature: Growth Changed / Upgraded / Deposited in Garrison).
Let's recall our entire "model" of a creature based on nouns from the previous article.
{
"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
}
]
}
Do we need all the shown attributes of a creature in every case to know if some operation is allowed? Does a possible upgrade depend, for example, on the creature's size during a battle? No. Does the creature's cost depend on hit points? Of course not.
In that case, we shouldn't mix these things in one model. This sounds obviously, but that's typically how legacy systems were built, which someone has to maintain till now (hopefully not you)...
💸 Money running away
An analogous example was given by Udi Dahan during the "Learn Advanced Distributed Systems Design" training:
Do I need to know the room's name to know if I can book it in a hotel? Can you imagine an invariant: "if the room's name starts with the letter A, bookings are only possible on Fridays"?
Sounds like an absurd, right? So let's not combine the bookings list into one object with the room's name, even through ORM and database relations. Group only the data that changes together into objects, and whose immediate consistency is necessary to meet business rules — meaning, it determines whether an operation is possible and whether the resulting event can occur. This way, you get more, but smaller and specialized models with lower complexity. Thanks to that you will avoid situations caused by optimistic locking where, for example, changing the room name is conflicting with updating a new booking... thereby causing your business to lose money!
🦄 Bounded Context appeared!
Assuming a model combines data and rules, do we need the same creature model in all contexts (such as Recruitment, Combat, Upgrading)? By now, you know the answer is NO. Each of these contexts will have different rules and sets of data needed to verify them. Only the ID will remain the same.
This means, at the solution level, we will have different Creature classes on which different operations can be performed. To decide if a given operation can be executed, we don't need to know every property for a given noun. Just as a domain expert doesn't need to know everything about the entire business but only needs to know everything about their department and a bit about those they need to collaborate with directly. Don't define classes based on names or nouns. Look for consistency rules — what changes together and what doesn't.
This will also give you low coupling and high cohesion (as Uncle Bob wrote in Clean Code). Remember, of course, that these are not goals in themselves. We structure code to make it easy to understand. When we understand it, we can quickly make changes and avoid bugs. At the end of the day, this is what a programmer's job boils down to. If you're not sure what the word cohesion means, now is a good time to refresh your memory. But if you remember, that's great for you and your coworkers!
Above, you see a possible division of the creature model, divided by context. An analogous diagram (for ecommerce product) in the book Patterns, Principles, and Practices of Domain-Driven Design changed my programming life forever.
What we'd like to emphasize in creature's model for different contexts?
- Combat: active spells, luck, morale, hit points.
- Recruitment: cost, availability, growth rate (depends on town buildings and, for example, the week symbol).
- Similarly, when encountering an enemy creature on the map, is it important which player it belongs to or whether it can be upgraded? Of course not.
If we use the creature model from the map (maybe a generic "shared" model) for every other context, it will be too simple and won't meet the requirements. On the other hand, using one, huge and complex model with all properties will result in longer code, filled with unnecessary if-statements, making it harder to understand and modify.
But how do we know which attributes should be grouped together? Where did this division come from? How do we manage to split so many fields at once? The answer is: don't try to split it all at once. Instead, it's better to go the other way around and let the necessary attributes emerge from the desired behaviors of the system. How to do that? In the next chapters :)
🤖 (Data ∪ Behaviors) ⊂ Model
Looking at the system only from the BEING (data) perspective while ignoring BEHAVING will make our solution flawed. This is especially true when dealing with complex business logic, rather than just a database browser like CRUD. A dominant BEING perspective results in a tangled mess of if-statements in huge god objects, that's hard to untangle.
Unfortunately, even software engineers with many years of experience, when asked to "design the architecture," often draw rectangles, connect them with arrows, and consider the planning done. However, only when we overlay behaviors and business processes (events and commands — more on this soon!) on such a diagram does the implicit tangle of interconnections and dependencies become explicit.
If you don't want to miss the upcoming parts, let's sign up for my mailing list HERE.
Posted on June 18, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.