从其他聚合内部检索聚合
Retrieving aggregates from inside of other aggregates
我研究 DDD 已经一年多了,但我对我的总体理解仍然很不满意。我在 python 中准备了一个复杂的用例示例,其中出现了一些聚合问题。
用例:玩家可以命令他的单位攻击其他单位,他选择用什么力量攻击。攻击后,被攻击单位的所有者会收到通知。
我的问题是,当单元攻击域逻辑中的另一个单元时,我只能访问那些单元聚合,但要计算攻击造成的损害,我需要访问这些单元上 id 引用的聚合。特别是武器和装甲聚合(它们是 AR,因为它们可以没有单位存在,我想跟踪它们的历史)。
我有两个选择:
- 在域服务中加载这些聚合并在函数调用中作为参数传递:
unit.attack_other_unit_with_power(unit_being_attacked, power, weapon, armor)
但看起来真的很糟糕。
- 从单位聚合内部加载武器和装甲聚合。
我已经准备好代码来展示该方法。
应用服务。
"""
Game application services.
"""
from game.domain.model.attackpower import AttackPower
from game.domain.exception import PlayerNotOwnerOfUnit, UnitCannotMeleeAttack
class GameService(object):
"""
Game application services.
"""
def __init__(self, player_repository, unit_repository):
"""
Init.
:param PlayerRepository player_reposistory: Player repository.
:param UnitRepository unit_reposistory: Unit repository.
"""
self._player_repository = player_repository
self._unit_repository = unit_repository
def player_order_unit_to_melee_attack_another_unit_using_power(
self, player_id, unit_id, unit_being_attacked_id, power
):
"""
Player order his unit to melee attack other unit, using given power.
:param int player_id: Player id.
:param int unit_id: Player unit id.
:param int unit_being_attacked_id: Id of unit that is being attacked.
:param float power: Power percentage value .
"""
player = self._player_repository.get_by_id(player_id)
unit = self._unit_repository.get_by_id(unit_id)
unit_being_attacked = self._unit_repository.get_by_id(unit_being_attacked_id)
attack_power = AttackPower(power)
if not self._is_player_owner_of_unit(player, unit):
raise PlayerNotOwnerOfUnit(player, unit)
if not unit.can_melee_attack():
raise UnitCannotMeleeAttack(unit)
unit.melee_attack_unit_using_power(unit_being_attacked, attack_power)
self._unit_repository.store(unit)
self._unit_repository.store(unit_being_attacked)
单位合计。
from game.domain.model.health import Health
from game.domain.model.event.unitwasattacked import UnitWasAttacked
from game.domain.service.damage import calculate_damage
class Unit(object):
"""
Unit aggregate.
"""
def __init__(self, id, owner_id, player_repository, weapon_repository, armor_repository, event_dispatcher):
"""
Init.
:param int id: Id of this unit.
:param int owner_id: Id of player that is owner of this unit.
:param PlayerRepository player_repository: Player repository implementation.
:param WeaponRepository weapon_repository: Weapon repository implementation.
:param ArmorRepository armor_repository: Armor repository implementation.
:param EventDispatcher event_dispatcher: Event dispatcher.
"""
self._id = id
self._owner_id = owner_id
self._health = Health(100.0)
self._weapon_id = None
self._armor_id = None
self._player_repository = player_repository
self._weapon_repository = weapon_repository
self._armor_repository = armor_repository
self._event_dispatcher = event_dispatcher
def id(self):
"""
Get unit id.
:return: int
"""
return self._id
def can_melee_attack(self):
"""
Check if unit can melee attack.
:return: bool
"""
if self._is_fighting_bare_hands():
return True
weapon = self._weapon_repository.get_by_id(self._weapon_id)
if weapon.is_melee():
return True
return False
def _is_fighting_bare_hands(self):
"""
Check if unit is fighting with bare hands (no weapon).
:return: bool
"""
return self.has_weapon()
def has_weapon(self):
"""
Check if unit has weapon equipped.
:return: bool
"""
if self._weapon_id is None:
return False
return True
def melee_attack_unit_using_power(self, attacked_unit, attack_power):
"""
Melee attack other unit using given attack power.
:param Unit attacked_unit: Unit being attacked.
:param AttackPower attack_power: Attack power.
"""
weapon = self.weapon()
armor = attacked_unit.armor()
damage = calculate_damage(weapon, armor, attack_power)
attacked_unit.deal_damage(damage)
self._notify_unit_owner_of_attack(attacked_unit)
def _notify_unit_owner_of_attack(self, unit):
"""
Notify owner of given unit that his unit was attacked.
:param Unit unit: Attacked unit.
"""
unit_owner = unit.owner()
unit_was_attacked = UnitWasAttacked(unit.id(), unit_owner.id())
self._event_dispatcher.dispatch(unit_was_attacked)
def owner(self):
"""
Get owner aggregate.
:return: Player
"""
return self._player_repository.get_by_id(self._owner_id)
def armor(self):
"""
Get armor object.
:return: Armor
"""
if self._armor_id is None:
return None
return self._armor_repository.get_by_id(self._armor_id)
def weapon(self):
"""
Get weapon object.
:return: Weapon
"""
if self._weapon_id is None:
return None
return self._weapon_repository.get_by_id(self._weapon_id)
def deal_damage(self, damage):
"""
Deal given damage to self.
:param Damage damage: Dealt damage.
"""
self._health.take_damage(damage)
问题是从聚合内部访问存储库是否可以仅用于读取(不存储)?
如果我想拿走单位的装甲并对其进行一些更改然后存储怎么办。
armor = unit.armor() # loaded using repository internally
armor.repair()
armor_repository.store(armor)
它是否违反任何内容或会导致问题?
如果您对此代码有任何其他评论,我将很高兴听到。
更新:我发现了另一个问题。如果我想在每次攻击后降低武器质量怎么办?
我将不得不更改武器状态并存储它,但是从聚合的内部存储是一个坏主意,因为我们无法控制它。
一般来说,对聚合中的存储库进行任何操作都是一个坏主意。特别是从不相关的聚合中对另一个聚合的存储库执行操作。聚合的目的是维护该实体的不变量,并且仅维护该实体。每当您对多个实体执行操作时,您可能希望将其放入域服务中。
我会说你最好的选择是第一个。如果你真的需要很多对象来计算伤害,那么将它们包装在一个值对象中可能会更干净。您不一定需要这个新值对象中每个实体的所有属性,只需要那些适用于伤害计算的属性。您可以调用这个新对象 "DamageAttributes" 然后您的方法签名将如下所示:
unit.attack_other_unit_with_power(unit_being_attacked, damage_attributes)
作为最后的旁注,我曾尝试为我的一款游戏制作类似的 DDD 游戏引擎。我 运行 和你一样陷入了很多摩擦,最终放弃了它,转而采用更 t运行saction 脚本方法。我的生活变得轻松多了,我从来没有后悔过这个选择。并非所有项目都适合 DDD,我认为这可能是其中之一。当规则不断变化时,DDD 最闪耀,但对于游戏引擎来说,通常变化最大的不是规则,而是数据(生命值、生命值、护甲)。你必须问问自己,你从 DDD 中得到了什么。就我而言,我无法提出令人信服的答案。
我研究 DDD 已经一年多了,但我对我的总体理解仍然很不满意。我在 python 中准备了一个复杂的用例示例,其中出现了一些聚合问题。
用例:玩家可以命令他的单位攻击其他单位,他选择用什么力量攻击。攻击后,被攻击单位的所有者会收到通知。
我的问题是,当单元攻击域逻辑中的另一个单元时,我只能访问那些单元聚合,但要计算攻击造成的损害,我需要访问这些单元上 id 引用的聚合。特别是武器和装甲聚合(它们是 AR,因为它们可以没有单位存在,我想跟踪它们的历史)。
我有两个选择:
- 在域服务中加载这些聚合并在函数调用中作为参数传递:
unit.attack_other_unit_with_power(unit_being_attacked, power, weapon, armor)
但看起来真的很糟糕。
- 从单位聚合内部加载武器和装甲聚合。
我已经准备好代码来展示该方法。
应用服务。
"""
Game application services.
"""
from game.domain.model.attackpower import AttackPower
from game.domain.exception import PlayerNotOwnerOfUnit, UnitCannotMeleeAttack
class GameService(object):
"""
Game application services.
"""
def __init__(self, player_repository, unit_repository):
"""
Init.
:param PlayerRepository player_reposistory: Player repository.
:param UnitRepository unit_reposistory: Unit repository.
"""
self._player_repository = player_repository
self._unit_repository = unit_repository
def player_order_unit_to_melee_attack_another_unit_using_power(
self, player_id, unit_id, unit_being_attacked_id, power
):
"""
Player order his unit to melee attack other unit, using given power.
:param int player_id: Player id.
:param int unit_id: Player unit id.
:param int unit_being_attacked_id: Id of unit that is being attacked.
:param float power: Power percentage value .
"""
player = self._player_repository.get_by_id(player_id)
unit = self._unit_repository.get_by_id(unit_id)
unit_being_attacked = self._unit_repository.get_by_id(unit_being_attacked_id)
attack_power = AttackPower(power)
if not self._is_player_owner_of_unit(player, unit):
raise PlayerNotOwnerOfUnit(player, unit)
if not unit.can_melee_attack():
raise UnitCannotMeleeAttack(unit)
unit.melee_attack_unit_using_power(unit_being_attacked, attack_power)
self._unit_repository.store(unit)
self._unit_repository.store(unit_being_attacked)
单位合计。
from game.domain.model.health import Health
from game.domain.model.event.unitwasattacked import UnitWasAttacked
from game.domain.service.damage import calculate_damage
class Unit(object):
"""
Unit aggregate.
"""
def __init__(self, id, owner_id, player_repository, weapon_repository, armor_repository, event_dispatcher):
"""
Init.
:param int id: Id of this unit.
:param int owner_id: Id of player that is owner of this unit.
:param PlayerRepository player_repository: Player repository implementation.
:param WeaponRepository weapon_repository: Weapon repository implementation.
:param ArmorRepository armor_repository: Armor repository implementation.
:param EventDispatcher event_dispatcher: Event dispatcher.
"""
self._id = id
self._owner_id = owner_id
self._health = Health(100.0)
self._weapon_id = None
self._armor_id = None
self._player_repository = player_repository
self._weapon_repository = weapon_repository
self._armor_repository = armor_repository
self._event_dispatcher = event_dispatcher
def id(self):
"""
Get unit id.
:return: int
"""
return self._id
def can_melee_attack(self):
"""
Check if unit can melee attack.
:return: bool
"""
if self._is_fighting_bare_hands():
return True
weapon = self._weapon_repository.get_by_id(self._weapon_id)
if weapon.is_melee():
return True
return False
def _is_fighting_bare_hands(self):
"""
Check if unit is fighting with bare hands (no weapon).
:return: bool
"""
return self.has_weapon()
def has_weapon(self):
"""
Check if unit has weapon equipped.
:return: bool
"""
if self._weapon_id is None:
return False
return True
def melee_attack_unit_using_power(self, attacked_unit, attack_power):
"""
Melee attack other unit using given attack power.
:param Unit attacked_unit: Unit being attacked.
:param AttackPower attack_power: Attack power.
"""
weapon = self.weapon()
armor = attacked_unit.armor()
damage = calculate_damage(weapon, armor, attack_power)
attacked_unit.deal_damage(damage)
self._notify_unit_owner_of_attack(attacked_unit)
def _notify_unit_owner_of_attack(self, unit):
"""
Notify owner of given unit that his unit was attacked.
:param Unit unit: Attacked unit.
"""
unit_owner = unit.owner()
unit_was_attacked = UnitWasAttacked(unit.id(), unit_owner.id())
self._event_dispatcher.dispatch(unit_was_attacked)
def owner(self):
"""
Get owner aggregate.
:return: Player
"""
return self._player_repository.get_by_id(self._owner_id)
def armor(self):
"""
Get armor object.
:return: Armor
"""
if self._armor_id is None:
return None
return self._armor_repository.get_by_id(self._armor_id)
def weapon(self):
"""
Get weapon object.
:return: Weapon
"""
if self._weapon_id is None:
return None
return self._weapon_repository.get_by_id(self._weapon_id)
def deal_damage(self, damage):
"""
Deal given damage to self.
:param Damage damage: Dealt damage.
"""
self._health.take_damage(damage)
问题是从聚合内部访问存储库是否可以仅用于读取(不存储)? 如果我想拿走单位的装甲并对其进行一些更改然后存储怎么办。
armor = unit.armor() # loaded using repository internally
armor.repair()
armor_repository.store(armor)
它是否违反任何内容或会导致问题?
如果您对此代码有任何其他评论,我将很高兴听到。
更新:我发现了另一个问题。如果我想在每次攻击后降低武器质量怎么办? 我将不得不更改武器状态并存储它,但是从聚合的内部存储是一个坏主意,因为我们无法控制它。
一般来说,对聚合中的存储库进行任何操作都是一个坏主意。特别是从不相关的聚合中对另一个聚合的存储库执行操作。聚合的目的是维护该实体的不变量,并且仅维护该实体。每当您对多个实体执行操作时,您可能希望将其放入域服务中。
我会说你最好的选择是第一个。如果你真的需要很多对象来计算伤害,那么将它们包装在一个值对象中可能会更干净。您不一定需要这个新值对象中每个实体的所有属性,只需要那些适用于伤害计算的属性。您可以调用这个新对象 "DamageAttributes" 然后您的方法签名将如下所示:
unit.attack_other_unit_with_power(unit_being_attacked, damage_attributes)
作为最后的旁注,我曾尝试为我的一款游戏制作类似的 DDD 游戏引擎。我 运行 和你一样陷入了很多摩擦,最终放弃了它,转而采用更 t运行saction 脚本方法。我的生活变得轻松多了,我从来没有后悔过这个选择。并非所有项目都适合 DDD,我认为这可能是其中之一。当规则不断变化时,DDD 最闪耀,但对于游戏引擎来说,通常变化最大的不是规则,而是数据(生命值、生命值、护甲)。你必须问问自己,你从 DDD 中得到了什么。就我而言,我无法提出令人信服的答案。