从其他聚合内部检索聚合

时间:2015-04-05 11:43:52

标签: python domain-driven-design aggregateroot

我一直在研究DDD已有一年多了,但我仍然对我的总体理解感到不满。我在python中编写了一个复杂的用例示例,其中出现了聚合的一些问题。

使用案例:玩家可以命令他的单位攻击其他单位,他选择将进行什么样的攻击。攻击后,被攻击单位的所有者会被告知该事实。

我的问题是当单元攻击域逻辑中的另一个单元时,我只能访问那些单元聚合,但是为了计算攻击造成的损坏,我需要访问这些单元上id引用的聚合。特别是对于武器和装甲聚合物(它们是AR,因为它们可以在没有单位的情况下存在,我想跟踪它们的历史)。

我有两个选择:

  1. 在域服务中加载这些聚合,并在函数调用中作为参数传递:
  2. unit.attack_other_unit_with_power(unit_being_attacked, power, weapon, armor)

    但看起来很糟糕。

    1. 从单位聚合物内部装载武器和装甲聚合物。
    2. 我准备了代码来介绍这种方法。

      申请服务。

      
      """
      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)
      

      问题是,如果可以从聚合体内部访问reposistory只是为了读取(不存储)? 如果我想拿出装甲的盔甲并对其进行一些修改然后存储,该怎么办?

      armor = unit.armor() # loaded using repository internally armor.repair() armor_repository.store(armor)

      是否违反了任何规定或导致问题?

      如果您对此代码有任何其他评论,我会很高兴听到它。

      更新:我发现了另一个问题。如果我想在每次攻击后降低武器质量怎么办? 我必须改变武器状态并存储它,但是从聚合的insisde存储是一个坏主意,因为我们无法控制它。

1 个答案:

答案 0 :(得分:4)

通常,在聚合中对存储库执行任何操作都是一个坏主意。特别是在不相关的聚合中对另一个聚合的存储库执行操作。聚合的目的是维护该实体和该实体的不变量。每当您对多个实体执行操作时,您可能希望将其放入域服务中。

我会说你最好的选择是2,是第一个。如果您确实需要许多对象来计算损坏,那么将它们打包在值对象中可能更清晰。您不一定需要此新值对象中每个实体的所有属性,只需要应用于损坏计算的属性。您可以将此新对象称为“DamageAttributes”,然后您的方法签名将如下所示:

unit.attack_other_unit_with_power(unit_being_attacked, damage_attributes)

作为最后一个注意事项,我试着在我的一个游戏中做一个类似的DDD游戏引擎。我遇到了很多摩擦,并最终取消了它,转而采用更多的事务脚本方法。我的生活变得更轻松,我没有后悔这一次。并非所有项目都是DDD的良好候选者,在我看来这可能是其中之一。当规则不断变化时,DDD最为闪耀,但是对于游戏引擎,通常不会像数据(生命值,生命值,护甲值)那样改变规则。你必须问问自己从DDD中获得了什么。就我而言,我无法想出一个令人信服的答案。