Получение агрегатов изнутри других агрегатов

Я изучаю DDD уже более года, но я все еще очень недоволен своим совокупным пониманием. Я подготовил сложный пример использования в python, где возникают некоторые проблемы с агрегатами.

Пример использования: Игрок может приказать своему юниту атаковать другого юнита, он сам выбирает, с какой силой будет производиться атака. После нападения владелец атакуемого объекта уведомляется об этом факте.

Моя проблема в том, что когда единица атакует другую единицу в доменной логике, у меня есть доступ только к агрегатам этих единиц, но для вычисления ущерба, нанесенного при атаке, мне нужен доступ к агрегатам, на которые ссылается идентификатор этих единиц. В частности, для агрегатов оружия и доспехов (которые являются AR, потому что они могут существовать без юнитов, и я хотел бы отслеживать их историю).

У меня есть два варианта:

  1. загрузите эти агрегаты в службу домена и передайте в качестве аргументов в вызове функции:

unit.attack_other_unit_with_power(unit_being_attacked, power, weapon, armor)

но выглядит очень плохо.

  1. Загружайте агрегаты оружия и брони изнутри агрегата юнита.

Я подготовил код для представления этого подхода.

Служба приложений.


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

Это что-то нарушает или это вызовет проблемы?

Если у вас есть какие-либо другие замечания по этому коду, я был бы рад их услышать.

ОБНОВЛЕНИЕ: я обнаружил еще одну проблему. Что, если я хочу снижать качество оружия после каждой атаки? Мне пришлось бы изменить состояние оружия и сохранить его, но хранение внутри агрегата — плохая идея, потому что мы не можем его контролировать.


person Rafał Łużyński    schedule 05.04.2015    source источник
comment
Возможно, вы могли бы использовать некоторые события предметной области для обработки деградации оружия. Создайте событие, такое как WeaponUsedInAttack, которое содержит некоторые значения, касающиеся оружия и атаки, тогда вы можете подписаться на это обработчиком и выполнить соответствующее действие.   -  person Tyler Day    schedule 06.04.2015
comment
Вы правы, но в коде должен быть только один способ проведения атаки оружием, поэтому использование события избыточно, за исключением того, что в обработчике событий я могу использовать репозиторий. И здесь возникает мой следующий вопрос: почему мы можем использовать репозиторий в доменном сервисе, но не в агрегированном методе? Технически это то же самое, я создам для этого отдельный поток SO.   -  person Rafał Łużyński    schedule 06.04.2015
comment
Я думаю, вы упустили из виду тот факт, что AR могут ссылаться на другие AR, если только один из них модифицируется за транзакцию. Если одному AR нужен другой для выполнения своих задач, то вам, вероятно, следует ссылаться на другой AR по ссылке, а не по id.   -  person plalx    schedule 06.04.2015
comment
Можно, но Вон Вернон отговаривает это делать. Конечно, это не должно быть правильным во всех случаях. Но таким образом построение агрегатного дерева было бы проблематичным, если бы была длинная цепочка ссылок. Кроме того, что, если я изменю этот ссылочный агрегат во время своей задачи, я не смогу его сохранить, а также прикладной уровень не будет знать, что он должен хранить его при изменении.   -  person Rafał Łużyński    schedule 06.04.2015


Ответы (1)


В общем, делать что-либо с репозиториями из ваших агрегатов — плохая идея. В частности, выполнение операций в репозитории другого агрегата из несвязанного агрегата. Цель агрегата состоит в том, чтобы поддерживать инварианты этого объекта и только этого объекта. Всякий раз, когда вы выполняете действия над несколькими объектами, вы, вероятно, захотите, чтобы это происходило в службе домена.

Я бы сказал, что ваш лучший вариант из двух - первый. Если вам действительно нужно много объектов для расчета ущерба, то может быть чище упаковать их в объект-значение. Вам не обязательно нужны все свойства каждой сущности в этом новом объекте-значении, только те, которые применяются для расчета ущерба. Вы можете назвать этот новый объект «DamageAttributes», и тогда сигнатура вашего метода будет выглядеть так:

unit.attack_other_unit_with_power(unit_being_attacked, damage_attributes)

И последнее примечание: некоторое время назад я пытался сделать аналогичный игровой движок DDD для одной из своих игр. Я столкнулся с большим количеством трений, как и вы, и в конце концов отказался от него в пользу подхода, основанного на сценариях транзакций. Моя жизнь стала намного проще, и я ни разу не пожалел о своем выборе. Не все проекты являются хорошими кандидатами на DDD, и, на мой взгляд, это может быть одним из них. DDD сияет больше всего, когда правила постоянно меняются, но с игровыми движками обычно меняются не столько правила, сколько данные (очки жизни, здоровье, броня). Вы должны спросить себя, что вы получаете от DDD. В моем случае я не мог придумать убедительный ответ.

person Tyler Day    schedule 05.04.2015
comment
Спасибо за ответ. Ваш подход с дополнительным ValueObject интересен, но я думаю, что таким образом много логики просачивается на прикладной уровень. Как и знание того, что агрегаты оружия и брони нужны для расчета урона. Я придумал другое решение — поместить атакующую логику в доменную службу, но таким образом мой Unit AR становится анемичным. Что касается Вашего вопроса о преимуществах использования DDD. Я думаю, что такие проблемы, как у меня здесь, возникают в большинстве доменов, поэтому, если DDD не будет работать с этим, то он не будет работать для большинства доменов. Я продолжу попытки с DDD или, по крайней мере, с DDD Lite. - person Rafał Łużyński; 05.04.2015
comment
Ничто не будет просачиваться на прикладной уровень. Объект значения существует на уровне домена и создается на уровне домена. Вы можете передать оружие, силу, урон в конструктор объекта значения и соответствующим образом установить его внутренние свойства. Я согласен, что в конечном итоге вы получите анемичную модель предметной области, но в данном случае это может быть не самое худшее. - person Tyler Day; 05.04.2015
comment
Я забыл добавить, что я не использую DDD для игрового движка. Игровой движок в основном касается инфраструктуры, а не игровой логики. Игровой движок IMO должен быть просто клиентом, использующим наши сервисы приложений. Я отмечу Ваш ответ как принятый, если больше не будет ответов, которые я могу принять во внимание. - person Rafał Łużyński; 05.04.2015
comment
Спасибо. Возможно, я использовал неправильный термин, когда говорил о игровом движке. Я думаю, мы имеем в виду одно и то же (бэкэнд, с которым все разговаривает) :) - person Tyler Day; 05.04.2015