In a DDD architecture design, the rationality of domain layer design will directly affect the code structure of the whole architecture as well as the design of application layer and infrastructure layer. However, domain layer design is a challenging task, especially in an application with relatively complex business logic, it is worth thinking carefully whether each business rule should be placed in Entity, ValueObject or DomainService, to avoid poor scalability in the future, and to ensure that excessive design will not lead to complexity. Today I’ll use a case study in a relatively straightforward area, but similar logic can be applied to real-world business applications, whether transactions, marketing, or interactions.

On the world structure of dragon and magic

Background and Rules

I read a lot of serious business code in weekdays, today I find a relaxed topic, how to use code to achieve a dragon and magic game world (minimalist) rules?

The basic configuration is as follows:

  • Player can be a Fighter, Mage, or Dragoon.

  • Monsters can be Orc, Elf or Dragon. Monsters have health

  • Weapon can be Sword or Staff. The Weapon is offensive

  • The player can be equipped with a weapon, which can be physical (0), fire (1), ice (2), etc. The weapon type determines the damage type

The attack rules are as follows:

  • Orcs take half the damage from physical attacks

  • Elves take half the damage of magic attacks

  • Dragons are immune to physical and magic attacks, unless the player is a dragoon, damage is doubled

OOP realization

For those familiar with object-oriented Programming, a relatively simple implementation is through class inheritance (omit some non-core code here) :

public abstract class Player {
      Weapon weapon
}
public class Fighter extends Player {}
public class Mage extends Player {}
public class Dragoon extends Player {}

public abstract class Monster {
    Long health;
}
public Orc extends Monster {}
public Elf extends Monster {}
public Dragoon extends Monster {}

public abstract class Weapon {
    int damage;
    int damageType; // 0 - physical, 1 - fire, 2 - ice etc.
}
public Sword extends Weapon {}
public Staff extends Weapon {}
Copy the code

The implementation rule code is as follows:

public class Player { public void attack(Monster monster) { monster.receiveDamageBy(weapon, this); } } public class Monster { public void receiveDamageBy(Weapon weapon, Player player) { this.health -= weapon.getDamage(); }} public class extends Monster {@override public void receiveDamageBy(Weapon Weapon, Player player) { if (weapon.getDamageType() == 0) { this.setHealth(this.getHealth() - weapon.getDamage() / 2); ReceiveDamageBy (weapon, player); // Orc physical defense rules} else {super.receiveDamageBy(weapon, player); } } } public class Dragon extends Monster { @Override public void receiveDamageBy(Weapon weapon, Player player) { if (player instanceof Dragoon) { this.setHealth(this.getHealth() - weapon.getDamage() * 2); // No damage, else no damage}}Copy the code

Then run a few single tests:

public class BattleTest {

    @Test
    @DisplayName("Dragon is immune to attacks")
    public void testDragonImmunity() {
        // Given
        Fighter fighter = new Fighter("Hero");
        Sword sword = new Sword("Excalibur", 10);
        fighter.setWeapon(sword);
        Dragon dragon = new Dragon("Dragon", 100L);

        // When
        fighter.attack(dragon);

        // Then
        assertThat(dragon.getHealth()).isEqualTo(100);
    }

    @Test
    @DisplayName("Dragoon attack dragon doubles damage")
    public void testDragoonSpecial() {
        // Given
        Dragoon dragoon = new Dragoon("Dragoon");
        Sword sword = new Sword("Excalibur", 10);
        dragoon.setWeapon(sword);
        Dragon dragon = new Dragon("Dragon", 100L);

        // When
        dragoon.attack(dragon);

        // Then
        assertThat(dragon.getHealth()).isEqualTo(100 - 10 * 2);
    }

    @Test
    @DisplayName("Orc should receive half damage from physical weapons")
    public void testFighterOrc() {
        // Given
        Fighter fighter = new Fighter("Hero");
        Sword sword = new Sword("Excalibur", 10);
        fighter.setWeapon(sword);
        Orc orc = new Orc("Orc", 100L);

        // When
        fighter.attack(orc);

        // Then
        assertThat(orc.getHealth()).isEqualTo(100 - 10 / 2);
    }

    @Test
    @DisplayName("Orc receive full damage from magic attacks")
    public void testMageOrc() {
        // Given
        Mage mage = new Mage("Mage");
        Staff staff = new Staff("Fire Staff", 10);
        mage.setWeapon(staff);
        Orc orc = new Orc("Orc", 100L);

        // When
        mage.attack(orc);

        // Then
        assertThat(orc.getHealth()).isEqualTo(100 - 10);
    }
}
Copy the code

The above code and single test are relatively simple, do not do redundant explanation.

Analyze design flaws in OOP code

The strong typing of programming languages cannot host business rules

The OOP code above works until we add a constraint:

  • A warrior can only be armed with a sword

  • A mage can only equip a staff

This rule is not implemented in the Java language through strong typing. Although Java has Variable Hiding (or C#’s new class Variable), it is simply adding a new Variable to a subclass, causing the following problems:

@Data public class Fighter extends Player { private Sword weapon; } @Test public void testEquip() { Fighter fighter = new Fighter("Hero"); Sword sword = new Sword("Sword", 10); fighter.setWeapon(sword); Staff staff = new Staff("Staff", 10); fighter.setWeapon(staff); assertThat(fighter.getWeapon()).isInstanceOf(Staff.class); // Error}Copy the code

In the end, although the code feels like setWeapon(Staff), it actually only modiates the parent class’s variables, not the child class’s variables, so it doesn’t actually take effect and doesn’t throw exceptions, but the result is wrong.

Of course, you could make the setter protected on the parent class, but that would limit the parent API, greatly reducing flexibility and violating the Principle of Liskov Substitution, which states that a parent class must be cast to use it:

@Data public abstract class Player { @Setter(AccessLevel.PROTECTED) private Weapon weapon; } @Test public void testCastEquip() { Fighter fighter = new Fighter("Hero"); Sword sword = new Sword("Sword", 10); fighter.setWeapon(sword); Player player = fighter; Staff staff = new Staff("Staff", 10); player.setWeapon(staff); // Compile however, but from API level should be open to use}Copy the code

Finally, if a rule is added:

  • Every warrior and mage can equip a dagger.

BOOM, all the strongly typed code I wrote before is dead and needs refactoring.

Object inheritance causes code to rely heavily on parent logic, which violates the Open-closed Principle (OCP)

The open closed principle (OCP) states that “objects should be open for extension and closed for modification.” Inheritance can extend new behavior through subclasses, but because subclasses may directly depend on the implementation of their parent class, a change may affect all objects. In this example, adding any type of player, monster, or weapon, or adding a rule, might require modifying all methods from parent to subclass.

For example, if you want to add a weapon type: sniper rifle, which can ignore all defenses and kill with one hit, the code you need to change includes:

  • Weapon

  • Player and all subclasses (whether or not a weapon can be equipped)

  • Monster and all subclasses (damage calculation logic)

public class Monster { public void receiveDamageBy(Weapon weapon, Player player) { this.health -= weapon.getDamage(); // Old base rule if (Weapon instanceof Gun) {// new logic this.sethealth (0); } } } public class Dragon extends Monster { public void receiveDamageBy(Weapon weapon, Player Player) {if (Weapon instanceof Gun) {// New logic super.receiveDamageBy(Weapon, Player); } // old logic ellipses}}Copy the code

Why is it advisable to “try” not to violate OCP in a complex piece of software? The core reason is that a change to an existing logic may affect some of the existing code, resulting in some unforeseen effects. This risk can only be protected by full unit test coverage, but it is difficult to guarantee single-test coverage in real development. OCP’s principles avoid this risk as much as possible, since the old code behaves the same way when the new behavior can only be implemented through new fields/methods.

Although inheritance can be Open for extension, it is difficult to close for modification. So the main solution to OCP today is through compose-over-inheritance, which means extensibility through Composition rather than inheritance.

Player. Attack (Monster) or Monster.receiveDamage(Weapon, Player)?

In this case, there is some disagreement about where the logic of the business rule should be written: when we look at the interaction between one object and another, does the Player attack Monster or does Monster get attacked by the Player? The current code mainly writes logic in Monster class. The main consideration is that Monster will hurt and reduce Health, but what if Player will hurt itself with a double-edged sword? Do you find problems with writing in the Monster class? What are the rules for where to write code?

Multiple objects behave similarly, resulting in code duplication

OOP inevitably leads to code duplication when we have different objects that have the same or similar behavior. In this example, if we were to add a “moveable” behavior, we would add similar logic to both the Player and Monster classes:

public abstract class Player {
    int x;
    int y;
    void move(int targetX, int targetY) {
        // logic
    }
}

public abstract class Monster {
    int x;
    int y;
    void move(int targetX, int targetY) {
        // logic
    }
}
Copy the code

One possible solution is to have a generic parent class:

public abstract class Movable {
    int x;
    int y;
    void move(int targetX, int targetY) {
        // logic
    }
}

public abstract class Player extends Movable;
public abstract class Monster extends Movable;
Copy the code

But what about adding another jumping ability, Jumpable? A Runnable running ability? If Player can Move and Jump, Monster can Move and Run, how to handle inheritance? Be aware that Java (and most languages) do not support multiparent inheritance, so you can only do it by repeating code.

The problem summary

In this case, while the OOP logic is intuitively simple, if your business is complex and there will be a lot of business rule changes in the future, the simple OOP code will turn into a complex paste at a later stage, with the logic scattered all over the place and the lack of a global perspective, and the overlapping of rules will trigger bugs. Does it feel like deja vu? Yes, the preferential and trading links in the e-commerce system often encounter similar pits. And the core essence of these questions is:

  • Is a business rule ascribed to an object’s “behavior” or a separate “rule object”?

  • How are relationships between business rules handled?

  • How should common “behaviors” be reused and maintained?

Before we talk about DDD solutions, let’s take a look at one of the most popular architectural designs in games recently, entity-component-System (ECS) implementation.

Introduction to entity-component-System (ECS) architecture

ECS is introduced

The ECS architecture model is actually a very old game architecture design, probably dating back to dungeon Siege’s component-based design, but has recently become popular with the addition of Unity (overwatch, for example, uses ECS). To quickly understand the value of the ECS architecture, we need to understand a core problem of game code:

  • Performance: the game must achieve a high render rate (60FPS), which means that the entire game world needs to be fully updated (physics engine, game state, rendering, AI, etc.) within 1/60s (about 16ms). In a game, there are usually a lot of (thousands, thousands) game object need to update the status, in addition to rendering can rely on the GPU, other logic needs to be done by the CPU, even for the most part can only be performed by a single thread, causing most of the time under complex scene CPU (mainly the bandwidth of the memory into the CPU) will become a bottleneck. In the era when CPU single-core speed almost no longer increases, how to improve the efficiency of CPU processing is the core of improving game performance.

  • Code organization: As we saw in chapter 1, when developing games using traditional OOP models, it’s easy to get caught up in code organization issues that make code difficult to read, maintain, and optimize.

  • Extensibility: This is similar to the previous one, but it’s more about the nature of the game that it needs to be updated quickly to add new elements. A game’s architecture needs to be able to add game elements with low code, or even zero code, in order to retain users through rapid updates. If every change required new code to be developed, tested, and then re-downloaded, it would be hard to imagine a game that could survive in today’s competitive environment.

ECS architecture can well solve the above problems. ECS architecture is mainly divided into:

  • Entity: Used to represent any game object, but in ECS the only important thing about an Entity is its EntityID. An Entity contains multiple components

  • Component: Is real data. The ECS architecture separates Entity objects into more detailed Components, such as location, material, state, etc. An Entity is actually a Bag of Components.

  • System (or ComponentSystem) : Is the real behavior, a game can have many different component systems, each responsible for only one thing, can handle a large number of the same components in turn, without the need to understand the specific Entity. So a ComponentSystem can theoretically have more efficient component processing efficiency, and even realize parallel processing, so as to improve CPU utilization.

Some of the core performance optimizations of ECS include placing components of the same type in the same Array and leaving Entity only to pointer of each component to better utilize CPU cache, reduce data loading costs, and optimize SIMD.

The pseudocode for an ECS case is as follows:

public class Entity { public Vector position; Public class MovementSystem {list <Vector> list; public class MovementSystem {list <Vector> list; Public void update(float delta) {for(Vector pos: Pos. X = pos. X + delta; pos.y = pos.y + delta; } } } @Test public void test() { MovementSystem system = new MovementSystem(); system.list = new List<>() { new Vector(0, 0) }; Entity entity = new Entity(list.get(0)); System. The update (0.1); AssertTrue (entity) position) x = = 0.1). }Copy the code

Since this article is not about ECS architecture, interested students can search for entity-component-system or check out Unity’s ECS documentation.

ECS architecture analysis

Going back to ECS, its roots are still very old concepts:

componentization

In software systems, we often reduce complexity by splitting large, complex systems into separate components. For example, front-end componentization reduces repetitive development costs in web pages, and micro-service architecture reduces service complexity and system impact surface by splitting services and databases. But the ECS architecture takes this to the extreme, where each object is componentized internally. By splitting the data and behavior of a game object into multiple components and component systems, high reusability of components can be achieved and repetitive development costs can be reduced.

Act out of

This has an obvious advantage in game systems. With OOP, a gameobject might include movement code, combat code, render code, AI code, etc., which would be long and difficult to maintain in a single class. By separating the generic logic into separate System classes, you can significantly improve the readability of your code. Another benefit is to remove dependencies that have nothing to do with the object code, such as the delta above, which needs to be injected as an input if it is placed in the Entity update method, but can be managed uniformly in the System. Attack (Monster) or Monster.receiveDamage(Weapon, Player). In ECS, this problem is much easier to solve than in CombatSystem.

Data driven

That is, the behavior of an object is not written but determined by its parameters. Through dynamic modification of parameters, the specific behavior of an object can be quickly changed. In the game architecture of ECS, by registering the corresponding Component for an Entity and changing the combination of specific parameters of the Component, an object’s behavior and gameplay can be changed. For example, creating a kettle + explosion attribute turns it into an “explosion kettle”, adding wind magic to a bicycle turns it into a flying car, etc. In some rougelikes, there may be more than 10,000 items of different types and functions. If these items with different functions were coded separately, it would take forever to write them, but with the data-driven + componentized architecture, the configuration of all items is a table, and modification is extremely easy. This is also an embodiment of the principle of composition over inheritance.

The defect of ECS

While ECS is beginning to make its mark in the gaming world, I don’t see the ECS architecture being used in any large commercial applications yet. There are many reasons for this, including the fact that ECS is relatively new and unknown, the lack of commercially mature and usable frameworks, and the fact that programmers are not ready to adapt to the shift in thinking from writing logical scripts to writing components, but I think the biggest one is the Behaivor separation of data/State that ECS emphasizes to improve performance. And in order to reduce GC costs, manipulating data directly goes to an extreme. In business applications, where data correctness, consistency, and robustness should be the highest priority and performance is just icing on the cake, ECS is hardly particularly beneficial in business scenarios. But that doesn’t mean we can’t take advantage of some of the groundbreaking ideas of ECS, including componentization, decoupling of cross-object behavior, and data-driven patterns, which work well in DDD.

A solution based on DDD architecture

Domain object

Going back to our original problem domain, let’s break down the various objects from the domain layer:

Entity class

In DDD, the entity classes contain ids and internal states, in this case Player, Monster, and Weapon. Weapon was designed for the entity class because two weapons of the same name can exist at the same time, so there must be an ID to distinguish them. Weapon can also be expected to contain some states in the future, such as upgrades, temporary buffs, durability, etc.

public class Player implements Movable {
    private PlayerId id;
    private String name;
    private PlayerClass playerClass; // enum
    private WeaponId weaponId; // (Note 1)
    private Transform position = Transform.ORIGIN;
    private Vector velocity = Vector.ZERO;
}

public class Monster implements Movable {
    private MonsterId id;
    private MonsterClass monsterClass; // enum
    private Health health;
    private Transform position = Transform.ORIGIN;
    private Vector velocity = Vector.ZERO;
}

public class Weapon {
    private WeaponId id;
    private String name;
    private WeaponType weaponType; // enum
    private int damage;
    private int damageType; // 0 - physical, 1 - fire, 2 - ice
}
Copy the code

In this simple case, we can use enum PlayerClass and MonsterClass instead of inheritance, and then we can use the Type Object design pattern to be data-driven.

Weapon can exist independently. Player is not an aggregation root. Therefore, Player can only store WeaponId, not Weapon id.

Componentization of a value object

In the previous ECS architecture, the concept of MovementSystem is reusable. Although you should not operate on components directly or inherit from common parent classes, you can componentialize domain objects through interfaces:

Public interface Movable {// equivalent to component Transform getPosition(); Vector getVelocity(); // behavior void moveTo(long x, long y); void startMove(long velX, long velY); void stopMove(); boolean isMoving(); } public class Player implements Movable {public void moveTo(long x, long y) { this.position = new Transform(x, y); } public void startMove(long velocityX, long velocityY) { this.velocity = new Vector(velocityX, velocityY); } public void stopMove() { this.velocity = Vector.ZERO; } @Override public boolean isMoving() { return this.velocity.getX() ! = 0 || this.velocity.getY() ! = 0; } } @Value public class Transform { public static final Transform ORIGIN = new Transform(0, 0); long x; long y; } @Value public class Vector { public static final Vector ZERO = new Vector(0, 0); long x; long y; }Copy the code

Two points to note:

  • Moveable interface has no Setter. The rule of an Entity is that it cannot change its properties directly, but must change the internal state through the Entity method. This ensures data consistency.

  • The advantage of abstract Movable is that, like ECS, some particularly generic behaviors (such as moving in a large map) can be handled with a unified System code, eliminating duplication of effort.

Equipment behavior

Since we are no longer using Player subclasses to determine what Weapon can be equipped, this logic should be split into a separate class. This class is called a Domain Service in DDD.

public interface EquipmentService {
    boolean canEquip(Player player, Weapon weapon);
}
Copy the code

In DDD, an Entity should not refer directly to another Entity or service, which means the following code is wrong:

public class Player { @Autowired EquipmentService equipmentService; // BAD: cannot directly rely on public void equip(Weapon Weapon) {//... }}Copy the code

The problem here is that the Entity can only retain its own state (or non-aggregated root objects). Any other object, whether or not it has been brought in by dependency injection, destroys Entity Invariance and is difficult to measure separately.

The correct way to refer to it is through method argument import (Double Dispatch) :

public class Player { public void equip(Weapon weapon, EquipmentService equipmentService) { if (equipmentService.canEquip(this, weapon)) { this.weaponId = weapon.getId(); } else { throw new IllegalArgumentException("Cannot Equip: " + weapon); }}}Copy the code

Here, either Weapon or EquipmentService is passed in through method parameters to ensure that it does not contaminate the Player’s own state.

Double Dispatch is a method commonly used with Domain services, similar to call inversion.

The relevant logical judgments are then implemented in EquipmentService, where we use another commonly used Strategy (or Policy) design pattern:

public class EquipmentServiceImpl implements EquipmentService { private EquipmentManager equipmentManager; @Override public boolean canEquip(Player player, Weapon weapon) { return equipmentManager.canEquip(player, weapon); }} // Public Class EquipmentManager {private static Final List<EquipmentPolicy> POLICIES = new ArrayList<>(); static { POLICIES.add(new FighterEquipmentPolicy()); POLICIES.add(new MageEquipmentPolicy()); POLICIES.add(new DragoonEquipmentPolicy()); POLICIES.add(new DefaultEquipmentPolicy()); } public boolean canEquip(Player player, Weapon weapon) { for (EquipmentPolicy policy : POLICIES) { if (! policy.canApply(player, weapon)) { continue; } return policy.canEquip(player, weapon); } return false; Public Class FighterEquipmentPolicy implements EquipmentPolicy {@override public Boolean canApply(Player) player, Weapon weapon) { return player.getPlayerClass() == PlayerClass.Fighter; } /** * Override public Boolean canEquip(Player, tag) {/** * Override public Boolean canEquip(Player, tag) { Weapon weapon) { return weapon.getWeaponType() == WeaponType.Sword || weapon.getWeaponType() == WeaponType.Dagger; }} // Other policies are omitted, see source codeCopy the code

The biggest benefit of this design is that future rule additions only require the addition of new Policy classes, rather than the need to change existing classes.

Aggressive behavior

Attack (Monster) or Monster.receiveDamage(Weapon, Player)? In DDD, this behavior is cross-entity business logic because it may affect Player, Monster, and Weapon. In this case, a third party Domain Service is required.

public interface CombatService { void performAttack(Player player, Monster monster); } public class CombatServiceImpl implements CombatService { private WeaponRepository weaponRepository; private DamageManager damageManager; @Override public void performAttack(Player player, Monster monster) { Weapon weapon = weaponRepository.find(player.getWeaponId()); int damage = damageManager.calculateDamage(player, weapon, monster); if (damage > 0) { monster.takeDamage(damage); // (Note 1) Change Monster in domain services} // omit Player and Weapon effects}}Copy the code

Similarly, in this case, damage calculation can be solved through the Strategy design mode:

Public class DamageManager {private static final List<DamagePolicy> POLICIES = new ArrayList<>(); public class DamageManager {private static final List<DamagePolicy> POLICIES = new ArrayList<>(); static { POLICIES.add(new DragoonPolicy()); POLICIES.add(new DragonImmunityPolicy()); POLICIES.add(new OrcResistancePolicy()); POLICIES.add(new ElfResistancePolicy()); POLICIES.add(new PhysicalDamagePolicy()); POLICIES.add(new DefaultDamagePolicy()); } public int calculateDamage(Player player, Weapon weapon, Monster monster) { for (DamagePolicy policy : POLICIES) { if (! policy.canApply(player, weapon, monster)) { continue; } return policy.calculateDamage(player, weapon, monster); } return 0; Public class DragoonPolicy implements DamagePolicy {public int calculateDamage(Player, calculateDamage) Weapon weapon, Monster monster) { return weapon.getDamage() * 2; } @Override public boolean canApply(Player player, Weapon weapon, Monster monster) { return player.getPlayerClass() == PlayerClass.Dragoon && monster.getMonsterClass() == MonsterClass.Dragon; }}Copy the code

Special attention should be paid to CombatService Field service and EquipmentService 3.2 Field Service, which, although both are field services, are substantially different. The EquipmentService above is more of a read-only strategy that affects only a single object, so it can be injected through parameters on the Player.equip method. But CombatService can affect multiple objects, so it can’t be called directly through parameter injection.

Unit testing

@Test
@DisplayName("Dragoon attack dragon doubles damage")
public void testDragoonSpecial() {
    // Given
    Player dragoon = playerFactory.createPlayer(PlayerClass.Dragoon, "Dart");
    Weapon sword = weaponFactory.createWeaponFromPrototype(swordProto, "Soul Eater", 60);
    ((WeaponRepositoryMock)weaponRepository).cache(sword);
    dragoon.equip(sword, equipmentService);
    Monster dragon = monsterFactory.createMonster(MonsterClass.Dragon, 100);

    // When
    combatService.performAttack(dragoon, dragon);

    // Then
    assertThat(dragon.getHealth()).isEqualTo(Health.ZERO);
    assertThat(dragon.isAlive()).isFalse();
}

@Test
@DisplayName("Orc should receive half damage from physical weapons")
public void testFighterOrc() {
    // Given
    Player fighter = playerFactory.createPlayer(PlayerClass.Fighter, "MyFighter");
    Weapon sword = weaponFactory.createWeaponFromPrototype(swordProto, "My Sword");
    ((WeaponRepositoryMock)weaponRepository).cache(sword);
    fighter.equip(sword, equipmentService);
    Monster orc = monsterFactory.createMonster(MonsterClass.Orc, 100);

    // When
    combatService.performAttack(fighter, orc);

    // Then
    assertThat(orc.getHealth()).isEqualTo(Health.of(100 - 10 / 2));
}
Copy the code

The specific code is relatively simple to explain the omission

Mobile system

Finally, there is a Domain Service. By componentizing, we can actually implement ecS-like systems to reduce some of the repetitive code:

public class MovementSystem { private static final long X_FENCE_MIN = -100; private static final long X_FENCE_MAX = 100; private static final long Y_FENCE_MIN = -100; private static final long Y_FENCE_MAX = 100; private List<Movable> entities = new ArrayList<>(); public void register(Movable movable) { entities.add(movable); } public void update() { for (Movable entity : entities) { if (! entity.isMoving()) { continue; } Transform old = entity.getPosition(); Vector vel = entity.getVelocity(); long newX = Math.max(Math.min(old.getX() + vel.getX(), X_FENCE_MAX), X_FENCE_MIN); long newY = Math.max(Math.min(old.getY() + vel.getY(), Y_FENCE_MAX), Y_FENCE_MIN); entity.moveTo(newX, newY); }}}Copy the code

Single test:

@Test
@DisplayName("Moving player and monster at the same time")
public void testMovement() {
    // Given
    Player fighter = playerFactory.createPlayer(PlayerClass.Fighter, "MyFighter");
    fighter.moveTo(2, 5);
    fighter.startMove(1, 0);

    Monster orc = monsterFactory.createMonster(MonsterClass.Orc, 100);
    orc.moveTo(10, 5);
    orc.startMove(-1, 0);

    movementSystem.register(fighter);
    movementSystem.register(orc);

    // When
    movementSystem.update();

    // Then
    assertThat(fighter.getPosition().getX()).isEqualTo(2 + 1);
    assertThat(orc.getPosition().getX()).isEqualTo(10 - 1);
}
Copy the code

MovementSystem here is a relatively independent Domain Service that centralizes similar code and some common dependencies/configurations (X, Y boundaries, etc.) through the componentization of Movable.

Some design specifications for DDD domain layer

Above, I compared three implementations of OOP, ECS, and DDD for the same example, as follows:

  • Inheritance-based OOP code: OOP code is the easiest to write and understand. All rule code is written in objects, but as domain rules become more complex, their structure limits their development. New rules may result in an overall refactoring of the code.

  • Component-based ECS code: ECS code has the highest flexibility, reusability, and performance, but it greatly weakens the cohesion of entity classes. All business logic is written in the service, which will lead to the consistency of business cannot be guaranteed, and will have a great impact on the business system.

  • DDD architecture based on domain object + domain service: the rules of DDD are the most complex, considering the cohesion of entity classes and Invariants, the ownership of cross-object rule codes, and even the invocation of specific domain services. Therefore, the cost of understanding DDD is relatively high.

So below, I will try to reduce the design cost of DDD domain layer by adopting some design specifications. Please refer to my previous article for the design specification of Value Object (Domain Primitive) in the Domain layer.

Entity Class

At the heart of most DDD architectures are entity classes, which contain state in a domain and direct operations on that state. The most important design principle of Entity is Invariants, ensuring that no matter what the external world does to it, the properties of an Entity cannot be in conflict with each other or in inconsistent states. So the design principles are as follows:

Create is consistent

In the anaemic model, the code commonly seen is that after a model is manually new, the callers assign values one parameter by one parameter, which is easy to cause omissions, resulting in inconsistent entity states. So there are two methods of creating entities in DDD:

The constructor parameter should contain all necessary attributes or have reasonable defaults in constructor.

For example, account creation:

public class Account { private String accountNumber; private Long amount; } @Test public void test() { Account account = new Account(); account.setAmount(100L); TransferService.transfer(account); // Error because Account is missing the required AccountNumber}Copy the code

Without a strongly validated constructor, consistency of created entities cannot be guaranteed. So we need to add a strong check constructor:

public class Account { public Account(String accountNumber, Long amount) { assert StringUtils.isNotBlank(accountNumber);  assert amount >= 0; this.accountNumber = accountNumber; this.amount = amount; } } @Test public void test() { Account account = new Account("123", 100L); // Make sure the object is valid}Copy the code

Use the Factory pattern to reduce caller complexity

Another approach is to create objects through the Factory mode, reducing some of the repetitive input arguments. Such as:

public class WeaponFactory { public Weapon createWeaponFromPrototype(WeaponPrototype proto, String newName) { Weapon weapon = new Weapon(null, newName, proto.getWeaponType(), proto.getDamage(), proto.getDamageType()); return weapon; }}Copy the code

You can quickly create new entities by passing in an existing Prototype. There are other design patterns, such as Builder, that are not mentioned.

Try to avoid public setters

One of the most common causes of inconsistencies is when entities expose setter methods that are public, especially if a single parameter to a set causes inconsistent state. For example, an order may contain sub-entities such as order status (ordered, paid, shipped, received), payment order, logistics order, etc. If a caller can set order status at will, it may cause the order state and sub-entities to not match, resulting in business process failure. So in entities, behavior methods are needed to modify the internal state:

@data @setter (accesslevel.private) // Make sure not to generate public Setter public class Order {PRIVATE int status; // 0 - create, 1 - pay, 2 - ship, 3 - receive Private Shipping Shipping; Public void pay(Long userId, Long amount) {if (status! = 0) { throw new IllegalStateException(); } this.status = 1; this.payment = new Payment(userId, amount); } public void ship(String trackingNumber) { if (status ! = 1) { throw new IllegalStateException(); } this.status = 2; this.shipping = new Shipping(trackingNumber); }}Copy the code

[Suggestion] In some simple scenarios, it is sometimes possible to set a value arbitrarily without causing inconsistencies. It is also suggested that the method name be rewritten to a more “behavioral” name to enhance its semantics. For example, setPosition(x, y) can be called moveTo(x, y), setAddress can be called assignAddress, etc.

The consistency of the master child entity is guaranteed by the aggregate root

In slightly more complex domains, where the master entity usually contains child entities, the master entity needs to act as an aggregate root, i.e. :

  • Child entities cannot exist alone and can only be obtained by aggregating roots. No external object can directly retain a reference to a child entity

  • Child entities do not have a separate Repository, so they cannot be stored and retrieved separately and must be instantiated by aggregating root repositories

  • Subentities can modify their state individually, but state consistency between multiple subentities is guaranteed by aggregation roots

Common cases of e-commerce domain aggregation include master sub-order model, product /SKU model, cross-sub-order discount model, cross-store discount model, etc. Many of the design specifications for aggregation roots and Repository are explained in detail in my previous article on Repository for reference.

You cannot rely heavily on other aggregated root entities or domain services

The principle of an entity is high cohesion and low coupling, that is, an entity class cannot be directly internally dependent on an external entity or service. This principle is in serious conflict with most ORM frameworks, so it is one that needs special attention during development. The necessary reasons for this principle include: dependencies on external objects directly cause entities to be untestable; And an entity cannot guarantee that changes to external entities will not affect the consistency and correctness of the entity.

So, there are two proper ways to rely on the outside world:

Save only external entity ids: again, I strongly recommend using strongly typed ID objects rather than Long ids. Strongly typed ID objects can not only contain their own validation code to ensure that the ID values are correct, but also ensure that the various input parameters are not bugged by the changing order of parameters. See my Domain Primitive article for details.

For external dependencies with “no side effects”, pass in as method inputs. For example, the Equip service (Weapon, EquipmentService) method mentioned above.

If methods have side effects on external dependencies, they cannot be used as method inputs. Instead, they can be used as Domain services, as described below.

The actions of any entity can only directly affect the entity (and its sub-entities)

This principle is more a matter of ensuring readable and understandable code that the behavior of any entity should not have “direct” “side effects” that directly modify other entity classes. The advantage of this is that the code reads without surprises.

Another reason for compliance is to reduce the risk of unknown changes. All changes to an entity object in a system should be expected, increasing the risk of code bugs if an entity can be directly modified externally at will.

Domain Services

As mentioned above, there are many kinds of domain services. Here, three common ones are summarized based on the above:

Single-object policy type

This domain object is primarily for changes to a single entity object, but involves rules for multiple domain objects or external dependencies. In the preceding context, a EquipmentService is such:

  • The object of the change is the Player parameter

  • Read Player and Weapon data, and possibly read some data from outside

In this type, the entity should pass in the domain service as a method entry and then reverse the method calling the domain service via Double Dispatch, for example:

Player.equip(Weapon, EquipmentService) {
    EquipmentService.canEquip(this, Weapon);
}
Copy the code

Why not call the domain service first and then the methods of the entity object to reduce the entity’s input dependence on the domain service? For example, the following method is incorrect:

boolean canEquip = EquipmentService.canEquip(Player, Weapon); if (canEquip) { Player.equip(Weapon); // ❌, this method is not feasible because this method has the possibility of inconsistencies}Copy the code

The main reason for this error is that the lack of domain service participation can lead to inconsistencies in the approach.

Cross-object transactional

When a behavior directly modifies multiple entities, it can no longer be handled through a single entity approach, but must be handled directly using the domain service approach. Here, domain services act more as cross-object transactions, ensuring consistency between changes to multiple entities.

Above, the following code is not recommended, although it can run:

public class Player { void attack(Monster, CombatService) { CombatService.performAttack(this, Monster); // ❌, do not write this, it will cause side effects}}Copy the code

Instead, we call a method that should call CombatService directly:

public void test() {
    //...
    combatService.performAttack(mage, orc);
}
Copy the code

This rule also mirrors the 4.1.5 rule, which states that player. attack will directly affect Monster, but Monster will not perceive this call.

General purpose component type

This type of domain service is more like the System in ECS, providing componentized behavior without being tied directly to an entity class itself. For specific cases, you can refer to MovementSystem implementation above.

Domain Policy Objects

The Policy or Strategy design pattern is a common design pattern, but one that occurs frequently in DDD architectures. At its core, the Policy or Strategy design pattern encapsulates domain rules.

A Policy is a stateless singleton that typically requires at least two methods: canApply and a business method. The canApply method is used to determine whether a Policy is applicable in the current context, and if so, the caller fires the business method. In general, to reduce the testability and complexity of a Policy, the Policy should not operate on objects directly, but instead operate on objects in Domain Services by returning computed values.

In the example above, DamagePolicy is only responsible for calculating the damage that should be caused, not directly causing damage to Monster. In this way, it is not only testable, but also prepared for the future multi-policy stacking calculation.

In addition to statically injecting multiple policies and manually prioritizing them, it is common to see policies registered through Java’s SPI mechanism or SPI-like mechanism, and policies sorted by different Priority schemes. I won’t expand too much here.

Extra meals – Side effects handling – domain events

One type of domain rule THAT I have deliberately neglected in the previous article is “side effects.” A common side effect is the synchronous or asynchronous impact or behavior of another object after a change in the state of the core domain model. In this case, we can add a side effect rule:

  • When Monster’s health drops to 0, Player is rewarded with experience

There are several solutions to this problem, such as writing the side effects directly in the CombatService:

public class CombatService { public void performAttack(Player player, Monster monster) { // ... monster.takeDamage(damage); if (! monster.isAlive()) { player.receiveExp(10); // Received experience}}}Copy the code

The problem with that is that pretty soon the CombatService code becomes very complicated. For example, if we add another side effect:

  • When Player exp reaches 100, increases by one level

Our code would then become:

public class CombatService { public void performAttack(Player player, Monster monster) { // ... monster.takeDamage(damage); if (! monster.isAlive()) { player.receiveExp(10); If (player.canlevelUp ()) {player.levelup (); // Upgrade}}}}Copy the code

What if you add “reward XXX after leveling up”? How about “update XXX rankings”? And so on, the subsequent code becomes unmaintainable. So we need to introduce the last concept of the Domain layer: Domain events.

Domain Event Introduction

A domain event is a notification mechanism that you want other objects in the domain to be aware of after something has happened in the domain. In the above case, the underlying reason for the increasing complexity of the code is that the reactive code (such as the upgrade) is directly coupled to the event trigger condition (such as the experience received), and this coupling is implicit. The benefit of domain events is to “externalize” the hidden side effects. Through an explicit event, the event trigger and event location can be decoupled, and ultimately the code can be clearer and more extensible.

Therefore, domain events are the preferred cross-entity “side effect” propagation mechanism in DDD.

Domain event implementation

Unlike message queue middleware, domain events are usually executed immediately, within the same process, and may be synchronous or asynchronous. We can use an EventBus to implement in-process notification as follows:

// implementer: 2019/11/28 Public class EventBus {getPrivate Final EventRegistry invokerRegistry = new EventRegistry(this); / / event dispenser private final EventDispatcher dispatcher = new EventDispatcher (ExecutorFactory. GetDirectExecutor ()); / / asynchronous events dispenser private final EventDispatcher asyncDispatcher = new EventDispatcher (ExecutorFactory. GetThreadPoolExecutor ());  Public Boolean dispatch(Event Event) {return dispatch(Event, dispatcher); } public Boolean dispatchAsync(Event Event) {return dispatch(Event, asyncDispatcher); } private Boolean dispatch(Event Event, EventDispatcher dispatcher) {checkEvent(Event); / / 1. Array access events Set < Invoker > invokers = invokerRegistry. GetInvokers (event); Dispatcher. dispatch(event, invokers); return true; Public void Register (Object Listener) {if (Listener == null) {throw new IllegalArgumentException("listener  can not be null!" ); } invokerRegistry.register(listener); } private void checkEvent(Event event) { if (event == null) { throw new IllegalArgumentException("event"); } if (! (event instanceof Event)) { throw new IllegalArgumentException("Event type must by " + Event.class); }}}Copy the code

Call method:

public class LevelUpEvent implements Event {
    private Player player;
}

public class LevelUpHandler {
    public void handle(Player player);
}

public class Player {
    public void receiveExp(int value) {
        this.exp += value;
        if (this.exp >= 100) {
            LevelUpEvent event = new LevelUpEvent(this);
            EventBus.dispatch(event);
            this.exp = 0;
        }
    }
}
@Test
public void test() {
    EventBus.register(new LevelUpHandler());
    player.setLevel(1);
    player.receiveExp(100);
    assertThat(player.getLevel()).equals(2);
}
Copy the code

Defects and prospects for current domain events

As you can see from the code above, good implementation of domain events relies on framework level support such as EventBus, Dispatcher, and Invoker. Another problem is that Entity cannot rely directly on external objects, so EventBus can only be a global Singleton for now, and you should know that global Singleton objects are difficult to test individually. This can lead to Entity objects not being easily covered by a full single test.

Another solution is to hack into entities and add a List to each Entity:

public class Player { List<Event> events; public void receiveExp(int value) { this.exp += value; if (this.exp >= 100) { LevelUpEvent event = new LevelUpEvent(this); events.add(event); // add event to this.exp = 0; } } } @Test public void test() { EventBus.register(new LevelUpHandler()); player.setLevel(1); player.receiveExp(100); For (Event Event: player.getevents ()) {eventbus.dispatch (Event); } assertThat(player.getLevel()).equals(2); }Copy the code

But you can see that this solution not only intrudes on the entity itself, but also requires a verbose explicit dispatch event at the caller, and is not a good solution.

There may be a framework in the future that does not rely on global singletons and does not require explicit event handling, but most of the current solutions have more or less defects that you should be aware of when using them.

conclusion

In real business logic, our domain model is more or less “special” and it may be tiring to comply with DDD specifications 100% of the time, so the most important thing is to sort out the impact surface of an object’s behavior and make a design decision: whether to affect only a single object or multiple objects,

  • The future expansion and flexibility of the rules,

  • Performance requirements,

  • Management of side effects, etc

Of course, most of the time, a good design is a choice of many factors, you need to have a certain amount of accumulation, really understand the logic behind each architecture and advantages and disadvantages. A good architect does not have a single right answer, but rather chooses the most balanced solution among multiple alternatives.

Look for cases, look for resumes

Finally, I’m looking for some input from the readers here. I’d like to write a follow-up on how to get a programmer to stop writing CRUD code all the time. I hope to produce extensible and maintainable architectures through DDD architecture design from some obvious CRUD code cases. I still lack some real cases, I hope readers can send me some cases via email, including (desensitized) code and business description, etc. I promise to reply to every case as much as possible, and I will include some classic cases in the article. My email address: [email protected], or you can add my nail number: Luangm (Yin Hao)

At the same time, our team is constantly recruiting. I am in charge of the industry and shopping guide team of Taodepartment. Our team is responsible for the daily business needs and innovative business (3D/AR, matching, customization, sizing guide, etc.) of the four industries (clothing, FMCG, consumer electronics, and home decoration) of Tmall and Taobao, and the front office (iFashion, Global Shopping, Baby, Makeup Academy, Planet Coldplay, etc.). As well as the horizontal shopping guide field of Mobile shopping (good goods, good stores, big rewards, brand shopping guide, etc.), the total DAU (average daily users) is about 3000W. The core goal of our team this year is to reconstruct the shopping guide experience of the industry and bring different, more humanized, more interactive, and more able to reflect the differentiated characteristics and culture of each sub-industry to consumers. All interested students are welcome to join us.