Дисклеймер: По-моему, статья об архитектуре ПО не должна и не может быть идеальной. Любое описанное решение может покрывать необходимый одному программисту уровень недостаточно, а другому программисту — слишком усложнит архитектуру без надобности. Но она должна давать решение тем задачам, которые поставила перед собой. И этот опыт, вместе со всем остальным багажом знаний программиста, который обучается, систематизирует информацию, оттачивает новыки, и критикует сам себя и окружающих — этот опыт превращается в отличные програмные продукты. Статья будет переключаться между художественой и технической частью. Это небольшой эксперимент и я надеюсь, что он будет интересным.
— Слушай, я тут придумал отличную идею игры! — гейм-дизайнер Вася был взъерошен, а глаза — красные. Я ещё попивал кофе и холиварил на Хабре, чтобы убить время перед стенд-апом. Он выжидательно посмотрел на меня, пока я закончу писать в комментариях человеку, в чем он не прав. Вася знал, что пока справедливость не восторжествует, а правда не будет защищена — смысла продолжать со мной разговор нету. Я дописал последнее предложение и перевел на него взгляд.

— В двух словах — маги с маной могут кастовать заклинания, а воины могут сражаться в близком бою и тратить выносливость. И маги и воины могут двигаться. Да, там ещё можно будет грабить корованы, но это уже следующей версии сделаем, короче. Покажешь прототип после стенд-апа, окей?

Он убежал по своим гейм-дизайнерским делам, а я — открыл IDE.

На самом деле тема «композиция против наследования», «banana-monkey problem», «проблема ромба (множественное наследование)» — частые вопросы на собеседовании в разных форматах и не зря. Неправильное использование наследования может усложнить архитектуру, а неопытные программисты не знаю, как с этим побороться и, в итоге, начинают критиковать ООП в целом и начинают писать процедурный код. Потому опытные программисты (или те, которые прочитали умные вещи в интернете) считают своим долгом спросить о таких вещах на собеседовании в самых разных формах. Универсальный ответ — «композиция лучше наследования, должна применяться и никаких оттенков серого». Тех, кто просто начитался всякого такой ответ устроит на все 100%.

Но, как я и говорил в начале статьи, каждая архитектура подойдет своему проекту и если для вашего проекта вполне достаточно наследования и все, что нужно, чтобы решить задачу — это создать ОбезьянуСБананом — создавайте. Наш программист оказался в похожей ситуации. Нет смысла отказываться от наследования только потому что ФПшники будут над вами смеяться.

class Character {
    x = 0;
    y = 0;

    moveTo (x, y) {
        this.x = x;
        this.y = y;
    }
}

class Mage extends Character {
    mana = 100;

    castSpell () {
        this.mana--;
    }
}

class Warrior extends Character {
    stamina = 100;

    meleeHit () {
        this.stamina--;
    }
}

Стенд-ап как всегда затянулся. Я покачивался на стуле и висел в телефоне пока джун Петя старался убедить тестера, что невозможность быстрого управления через правую кнопку мыши не баг, ведь нигде такой возможности описано не было, а значит нужно бросать задачу на отдел пре-продакшена. Тестер утверждал, что раз для пользователей управление через правую кнопку кажется обязательным, то это баг, а не фича. На самом деле, как единственный из нашей команды игрок в нашу игру на боевых серваках он хотел скорейшего добавления этой возможности, но знал, что если закинуть её в отдел пре-продакшена, то бюрократическая машина позволит выпустить её в релиз не раньше, чем через 4 месяца, а оформив её как баг — можно получить её уже в следующем билде. Менеджер проекта, как всегда, опаздывал, а ребята настолько яро ругались, что уже перешли на маты и, наверное, скоро дело дошло бы до мордобоя, если бы на ругань не прибежал директор студии и не увел бы обоих в свой кабинет. Наверное, снова на 300 баксов штрафанут.

Когда я вышел с митинг-рума, ко мне подбежал гейм-дизайнер и радосно сказал, что прототип всем понравился, его приняли в работу и теперь это наш новый проект на следующих полгода. Пока мы шли к моему столу он воодушевленно рассказывал, какие новые фичи будут в нашей игре. Сколько разных заклинаний он придумал и, конечно, что будет паладин, который может и сражаться, и магией кидаться. И весь отдел художников уже работает над новыми анимациями, а Китай уже подписал договор, по которому наша игра выйдет на их рынке. Я молча посмотрел на код своего прототипа, глубоко задумался, выделил всё и удалил.
Я верю, что со временем каждый программист на базе своего опыта начинает видеть очевидные проблемы, с которыми он может столкнуться. Особенно, если долго работает в команде с одним гейм-дизайнером. У нас появилось куча новых требований и фич. И наша старая «архитектура» очевидно с этим не справится.

Когда вам зададут подобную задачу на собеседовании — обязательно постараются вас подловить. Они может быть в самых разных формах — крокодилы, которые могут и плавать, и бегать. Танки, которые могут стрелять из пушки или из пулемета и так далее. Самое главное свойство таких задач — у вас есть объект, который может делать несколько разных действий. И ваше наследование никак не может справится, ведь невозможно унаследоваться и от FlyingObject и от SwimmingObject И разные объекты могут делать разные действия. В этот момент мы отказываемся от наследования и переходим к композиции:

class Character {

    abilities  = [];

    addAbility (...abilities) {
        for (const a of abilities) {
            this.abilities.push(a);
        }
        return this;
    }

    getAbility (AbilityClass) {
        for (const a of this.abilities) {
            if (a instanceof AbilityClass) {
                return a;
            }
        }
        return null;
    }

}

///////////////////////////////////////
// 
// Тут будет список абилок, которые могут быть у персонажа
// Каждая абилка может иметь свое состояние
// 
///////////////////////////////////////

class Ability {}

class HealthAbility extends Ability {
    health     = 100;
    maxHealth  = 100;
}
class MovementAbility extends Ability {
    x = 0;
    y = 0;

    moveTo(x, y) {
        this.x = x;
        this.y = y;
    }
}
class SpellCastAbility extends Ability {
    mana       = 100;
    maxMana    = 100;

    cast () {
        this.mana--;
    }
}
class MeleeFightAbility extends Ability {
    stamina    = 100;
    maxStamina = 100;

    constructor (power) {
        this.power = power;
    }

    hit () {
        this.stamina--;
    }
}

///////////////////////////////////////
// 
// А тут создаются персонажи со своими абилками
// 
///////////////////////////////////////

class CharactersFactory {
    createMage () {
        return new Character().addAbility(
            new MovementAbility(),
            new HealthAbility(),
            new SpellCastAbility()
        );
    }
    createWarrior () {
        return new Character().addAbility(
            new MovementAbility(),
            new HealthAbility(),
            new MeleeFightAbility(3)
        );
    }
    createPaladin () {
        return new Character().addAbility(
            new MovementAbility(),
            new HealthAbility(),
            new SpellCastAbility(),
            new MeleeFightAbility(2)
        );
    }
}


Каждое возможное действие теперь — это отдельный класс со своим состоянием и при необходимости мы можем создавать уникальных персонажей, накидывая им необходимое количество абилок. К примеру, очень легко создать бессмертное магическое дерево:

createMagicTree () {
    return new Character().addAbility(
        new SpellCastAbility()
    );
}

У нас пропало наследование и вместо него появилась композиция. Теперь мы создаем персонажа и перечисляем его возможные абилки. Но это не значит, что наследование — всегда плохо, просто в даном случае оно не подходит. Лучший способ понять, подходит ли наследование — ответить для себя на вопрос, какую связь оно отображает. Если эта связь «is-a», то есть вы указываете, что MeleeFightAbility — это абилка, то оно идеально подходит. Если же связь создается только потому что вы хотите добавить действие и отображает «has-a», то стоит подумать о композиции.
Я с удовольствием посмотрел на прекрасный результат. Он шикарно и без багов работает, архитектура мечты! Уверен, что она выдержит не одно испытание временем и нам ещё долго не придется её переписывать. Я так восторгался своим кодом, что даже не заметил, как ко мне подошел Джун Петя.

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

— Художники нарисовали просто божественные анимации — быстро затараторил он — не могу дождаться, когда мы их уже прикрутим. Особо шикарные вылетающие плюсики, когда применяется заклинание лечения. Они такие зелёные и такие плюсики!

Я про себя выругался, ведь совершенно забыл о том, что нам ещё прикручивать вьюшку. Черт, кажется, придется переписывать архитектуру.
В подобных статьях обычно описывается только работа с моделью, потому что она абстрактная и взрослая, а «картиночки показывать» можно отдать и джуну и неважно, какая там будет архитектура. Тем не менее, наша модель должна предоставлять максимум информации для вьюшки, чтобы та могла сделать свое дело. В ГеймДеве для этого, обычно, используется паттерн «Команда». В двух словах — мы имеем стейт без логики, а любое изменение должно происходить в соответствующих командах. Это может казаться усложнением, но это дает множество преимуществ:
— Они отлично комбятся, когда одна команда вызывает другую
— Каждая команда, когда выполняется является, по сути, событием, на которое можно подписаться
— Мы их можем легко сериализировать

К примеру, так может выглядеть команда нанесения урона. Именно её потом будут использовать воин при ударе мечем и маг при ударе заклинанием огня. Сейчас, для простоты, я реализовал валидацию команд через исключения, но пото их можно переписать а коды возврата.

class DealDamageCommand extends Command {
    constructor (target, damage) {
        this.target = target;
        this.damage = damage;
    }

    execute () {
        const healthAbility = this.target.getAbility(HealthAbility);

        if (healthAbility == null) {
            throw new Error('NoHealthAbility');
        }

        const resultHealth = healthAbility.health - this.damage;

        healthAbility.health = Math.max( 0, resultHealth );
    }
}

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

class MeleeHitCommand extends Command {
    constructor (source, target, damage) {
        this.source = source;
        this.target = target;
        this.damage = damage;
    }

    execute () {
        const fightAbility = this.source.getAbility(MeleeFightAbility);

        if (fightAbility == null) {
            throw new Error('NoFightAbility');
        }

        this.addChildren([
            new DealDamageCommand(this.target, fightAbility.power);
        ]);
    }
}

В этих двух командах есть все необходимое для наших анимаций. Рендерщик может просто подписаться на события и отобразить всё, что пожелают художники следующим кодом:

async onMeleeHit (meleeHitCommand) {
    await view.drawMeleeHit( meleeHitCommand.source, meleeHitCommand.target );
}

async onDealDamage (dealDamageCommand) {
    await view.showDamageNumbers( dealDamageCommand.target, dealDamageCommand.damage );
}

Я сбился со счета, который раз подряд засиживаюсь на работе до темноты. С самого детства разработка игр меня манила, казалась мне чем-то магическим и даже сейчас, когда я много лет этим занимаюсь — трепетно отношусь к этому. Несмотря на то, что я узнал секрет о том, как они создаются — я не потерял веру в магию. И эта магия заставляет меня с таким вдохновением сидеть по ночам и писать свой код. Ко мне подошел Вася. Он совершенно не умеет программировать, но разделает мое отношение к играм.

— Вот — гейм-дизайнер положил передо мною талмуд страниц на 200, распечатанные на листах А4. Хотя дизайн-документ велся в конфлюэнсе мы любили на важных этапах распечатать его, чтобы почувствовать эту работу в физическом воплощении. Я открыл его на случайной странице и попал на огромный список самых разных заклинаний, которые могут сделать маг и паладин, описание их эффектов, требований к интеллекту, цену в мане и приблизительное описание для художников, как необходимо их отобразить. Работы на много месяцев, потому сегодня я снова задержусь на работе.
Наша архитектура легко позволяет создавать сложные комбинации заклинаний. Просто каждое заклинание может возвращать список команд, которые необходимо выполнить при касте

class CastSpellCommand extends Command {
    constructor (source, target, spell) {
        this.source = source;
        this.target = target;
        this.spell  = spell;
    }

    execute () {
        const spellAbility = this.source.getAbility(SpellCastAbility);

        if (spellAbility == null) {
            throw new Error('NoSpellCastAbility');
        }

        this.addChildren(new PayManaCommand(this.source, this.spell.manaCost));
        this.addChildren(this.spell.getCommands(this.source, this.target));
    }
}

class Spell {
    manaCost = 0;
    
    getCommands (source, target) { return []; }
}

class DamageSpell extends Spell {
    manaCost = 3;
    
    constructor (damageValue) {
        this.damageValue = damageValue;
    }

    getCommands (source, target) {
        return [ new DealDamageCommand(target, this.damageValue) ];
    }
}

class HealSpell extends Spell {
    manaCost = 2;
    
    constructor (healValue) {
        this.healValue = healValue;
    }

    getCommands (source, target) {
        return [ new HealDamageCommand(target, this.healValue) ];
    }
}
class VampireSpell extends Spell {
    manaCost = 5;

    constructor (value) {
        this.value = value;
    }

    castTo (source, target) {
        return [
            new DealDamageCommand(target, this.value),
            new HealDamageCommand(source, this.value)
        ];
    }
}

Полтора года спустя

Стенд-ап как всегда затянулся. Я покачивался на стуле и висел в ноуте пока миддл Петя спорил с тестером о заведенном баге. Он со всей искренностью старался убедить тестера, что отсутствие управления через правую кнопку мыши в нашей новой игре не должно отмечаться как баг, ведь такой задачи никогда не стояло и её не прорабатывали ни гейм-дизайнеры, ни юишники. У меня возникло ощущение дежавю, но новое сообщение в дискорде отвлекло меня:

— Слушай — писал гейм-дизайнер — у меня есть отличная идея...

Комментарии (27)


  1. Jofr
    01.02.2019 21:44
    +4

    Кажется, вы переизобрели ECS.


    1. TheShock Автор
      01.02.2019 22:02
      +3

      Сегодня утром я как раз рекомендовал человеку почитать статью на тему ECS. Я ни в коем случае не говорю, что сказанное в статье — моя идея. Более того, я даже указал в статье, что это известный в гейм-деве паттерн «команда».

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


      1. Jofr
        01.02.2019 22:12
        +2

        Значит, я просто потерялся в литературных вставках и к концу статьи уже забыл ее начало и неправильно понял основной посыл. Полностью моя вина, приношу извинения :)


        1. TheShock Автор
          01.02.2019 22:15
          +1

          Та нет, ну что вы. Ссылка на ECS как дальнейшее развитие идеи — крайне полезна.


      1. Danik-ik
        02.02.2019 11:01
        +2

        На эту тему пишут странные вещи в любой среде. И довольно часто даже толковые вещи оказываются переусложнёнными недоупрощёнными, а часто ещё и невыносимо синтетическими. А у Вас получилось и предельно упростить, и доходчиво привязать к реальности, и показать последствия выбора. И всё это интересно.

        Спасибо, получилось здорово.


  1. Igor_ku
    02.02.2019 02:07

    Если честно, то я прочитал только желтые вырезки, уж больно интересно они написаны :)


    1. TheShock Автор
      02.02.2019 05:06

      Я рад, что они получились интересными, но ведь без кода нет истории)


  1. yizraor
    02.02.2019 13:04
    +1

    Спасибо за интересную публикацию!
    Зачастую даже в умных книгах авторы не всегда утруждаются подачей вразумительных примеров.
    У Вас в тексте просто по диагонали глазами пробежался, и вроде ничего нового… но то, что знал ранее, стало гораздо более понятным, как-то лучше в голове уложилось вместе с полезной рекомендацией ("is-a" / "has-a" для выбора между наследованием и композицией)...


    P.S.:
    Такой магии бы да побольше…
    Пешыте исчо!


  1. SiliconValleyHobo
    02.02.2019 15:32
    -3

    ведь невозможно унаследоваться и от FlyingObject и от SwimmingObject

    Дальше не читал


    1. TheShock Автор
      02.02.2019 19:06
      +2

      Почему? Устали? Мама сказала идти обедать? Ребенок попросил с ним поиграть? Позвонили из лотереи и сказали, что вы победили? Дом рухнул из-за взрыва газа, вы сейчас под обломками и не можете читать дальше?


      1. nikudyshko
        03.02.2019 19:38
        +1

        Вероятно, автор подразумевал возможность множественного наследования в некоторых языках программирования. Жаль, не хватило терпения объяснить)


        1. SiliconValleyHobo
          03.02.2019 20:36

          Отож


          1. TheShock Автор
            03.02.2019 20:45

            Хотите что-то сказать — говорите. Не пустые «дальше не читал», «отож» и остальную бессмысленную фигню.

            Давайте, опишите, в чем я неправ. Расскажите, как удобно все это реализуется в C++ при использовании множественного наследования и, что я зря нагородил огород. Тогда ваши комментарии будут полезны.

            Заодно расскажите, как при множественном наследовании можно дать абилку. Ну, к примеру, все танки могут ездить, но если игрок играет за немцев, то они еще и прыгать могут. Но если играет за немцев луны, то только прыгать, а ездить уже не могут.

            Именно на С++ я не писал, но насколько я знаю, там тоже множественное наследование стараются избегать. А в Шарпах его нету как и в ЖС.

            И да, в тегах явно указан язык, на котором написаны примеры.


            1. SiliconValleyHobo
              03.02.2019 21:25

              >там тоже множественное наследование стараются избегать
              Да вовсе нет.

              Неправы вы, на мой взгляд, только в том, что пишете игры на JS. Я читал со смартфона и проглядел язык в тегах. Зашел и бомбанул.


              1. TheShock Автор
                03.02.2019 21:29
                +1

                Я, в основном, пишу игры на C#, но бывает всякое. Иногда игры и на JS нужно пописать. Да и подход этот может использоваться не только в играх.


  1. aSimpleMan
    02.02.2019 18:48
    +1

    Статья понравилась, легко читается и понятные примеры. Есть небольшой вопрос, вот эта часть кода у вас повторяется, стоит ли её вынести и куда лучше вынести если нужно?

    if (spellAbility == null) {
        throw new Error('NoSpellCastAbility');
    }
    


    1. TheShock Автор
      02.02.2019 19:05
      +2

      Мне кажется, что тут нечего выносить. Ну на самом деле можно реализовать что-то вроде такого, пока мы пользуемся исключениями и не типизируем их:

      execute () {
          const spellAbility = this.source.getAbility(SpellCastAbility);
      
          ensureNotNull(spellAbility, 'NoSpellCastAbility')


      Можно даже пойти дальше и создать метод, который вместо null возвращает ошибку, тогда не придется вообще писать этого:

      execute () {
          const spellAbility = this.source.requireAbility(SpellCastAbility);


      Но, во-первых, в идеале нужно отказаться от исключений в пользу кодов возврата:

      execute () {
      	const spellAbility = this.source.getAbility(SpellCastAbility);
      
      	if (spellAbility == null) {
      		return Status.NoSpellCastAbility;
      	}
      
      	this.addChildren(new PayManaCommand(this.source, this.spell.manaCost));
      	this.addChildren(this.spell.getCommands(this.source, this.target));
      	
      	return Status.Success;
      }
      


      А тогда не будет возможности вынести это в метод, ведь всё-равно нужно if и return.

      Во-вторых, что более важно, я не совсем согласен, что это самоповторение. Там разные абилки и разные коды возврата. И, на самом деле, таких if (!cond) return error у нас на практике будет много совершенно разных. Все мы никак не сможем красиво заDRYить.


  1. yuriki
    03.02.2019 01:12
    +1

    Отлично написано! Отличные примеры — помогли лучше усвоить и запомнить! (а ещё они такие жизненные!)


  1. dtho-dtho
    03.02.2019 04:01

    Хочется упомянуть «Узловую систему» Node System.
    Когда каждая сущность — это и узел и компонент, знающий об своем родителе и своих детях.
    Древовидная структура. И каждый дочерний узел перемещается в родительских координатах, если они есть.

    Сейчас наверное самый яркий показатель этой идеологи это Godot движок. Там все — это узлы, а любой узел — это еще и сцена. Упомянул бы cocos2d, но там не все гладко с реализацией.


    1. TheShock Автор
      03.02.2019 04:11

      Судя по вашему описанию и скриншотам на офф странице, Unity3D тоже вполне подходит. Уверен, что и Unreal.

      Я глянут на Godot. Хорошо, что он опенсорсный. Но, с другой стороны, в нем явный уклон на свой скриптовый язык (Юнити не зря отказались от ДжаваСкрипта), а еще хуже, что Шарповая версия тоже страдает от динамичной природы. К примеру, тут:

      var plusButton = (Button)GetNode("PlusButton");
      plusButton.Connect("pressed", this, "ModifyValue", new object[] { 1 });


      На шарпах лучше бы выглядело так:

      var plusButton = GetNode<Button>("PlusButton");
      plusButton.Connect("pressed", ModifyValue);


      Или так, если с аргументами:
      var plusButton = GetNode<Button>("PlusButton");
      plusButton.Connect("pressed", () => ModifyValue(-1));


      Мне кажется, им стоит сконцентрироваться на Шарповой версии.

      Но почему бы вам не написать о нем статью на Хабру с разбором сильных и слабых сторон?


      1. dtho-dtho
        03.02.2019 04:45

        UE4 все же компонентный (Actor component). И Юнити тоже юзает компоненты, как мне известно. Компоненты не могут существовать без родителя. У годота — может, потому что это не компоненты, а узлы). В godot можно целые ветки сцен менять как вздумается на ходу. Я сам не так давно открыл его для себя, и довольно впечатлен. Простой, легкий, а мощный какой… Ничего лишнего нет.

        Его собственный скриптовый язык не плох, строгая типизация, синтаксис питона, но упрощенный.
        Ну и надо не забывать, что можно писать на C и C++ нативные модули. (Это как в UE4. Ты можешь блюпринты использовать, а можешь код писать)

        Я не так давно на него перешел с cocos2d-x. Лично на кокосе я заколебался для простых вещей писать полотна текста на плюсах. Так еще и баги движка закрывать, который оказываются «Фичами». (например у FastTMX отрисовка тайлов не правильная при перемещении сцены, а не узла).

        Но у того же годота нету поведенческого дерева (behavioral tree) из коробки. Нету визуального редактора шейдеров. И т.д.

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


  1. megasuperlexa
    03.02.2019 19:38
    -1

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


    1. TheShock Автор
      03.02.2019 20:00

      Такое и композируется лучше, и по иерархиям бегать потом не надо, чтобы понять что происходит со стейтом, да и юнит тесты писать проще.
      Начнем с того, что это обычная маркетологичная чушь, которой нас пичкают на всех афтепати. Нет, композируется точно так же, по иерархиям бегать надо точно так же, юнит тесты писать точно так же. По сути от замены класса на функцию для всех этих факторов меняется только форма записи. Почему вдруг должны упроститься эти вещи о которых вы говорите — я не знаю. Может вам не понравилось использование addChildren вместо прямого вызова метода класса? Но тут дело не в классах. На функциях тоже можно написать такой код через каррирование.

      Вообще когда я вижу класс с единственным методом, я не очень понимаю, почему это не может быть функцией
      Жаль, что вы из тех, кто бездумно повторяет за своим кумиром. Видите ли, в топике было раскрыто, зачем нужно классы и почему их нельзя заменить, к примеру, на функции. А если точнее — вы, как и предыдущий автор, совершенно забыли про вьюшку. Да, для простых вещей, которые пишутся на Редакс подойдет подход «изменили всю модель — перерисовали всю вьюшку». Но проблема в том, что этот подход просто отвратительно анимируется. Вот представьте себе настолку и там правило: «когда самурай стает на клетку рядом с противниками — он атакует каждого из них, но контратаку получает только от первого». Любой геймдевелопер знает, что это совсем обычная абилка для пошаговых игр. Итак, у нас получится такая иерархия:

      SAMURAY_ID = 100;
      ENEMY_1_ID = 201;
      ENEMY_2_ID = 202;
      ENEMY_3_ID = 203;
      
      Movement( SAMURAY_ID, 3, 5 )
      	Attack( SAMURAY_ID, ENEMY_1_ID )
      		DealDamage( ENEMY_1_ID, 3 )
      		CounterAttack( ENEMY_1_ID, SAMURAY_ID)
      			DealDamage( SAMURAY_ID, 1 )
      	Attack( SAMURAY_ID, ENEMY_2_ID )
      		DealDamage( ENEMY_2_ID, 3 )
      			Death( ENEMY_2_ID )
      				GiveMoney( PLAYER_1, 300 )
      	Attack( SAMURAY_ID, ENEMY_3_ID )
      		DealDamage( ENEMY_3_ID, 3 )
      


      При подходе с классами у меня есть вся эта иерархия на клиентской модели или на сервере, я ее сериализую и отправляю во вьюшку, а вьюшка отображает это так:

      await animateMovement(SAMURAY_ID, 3, 5);
      await animateAttack(SAMURAY_ID, ENEMY_1_ID);
      await showDamage(ENEMY_1_ID, 3)
      await animateAttack(SAMURAY_ID, ENEMY_1_ID);
      


      Что же будет при вашем «удобном» подходе с функциями? Все функции синхронно выполнятся, И все, что мы можем послать во вьюшку будет:

      У самурая новое положение и на 3 меньше хит-поинтов
      У двох врагов на 3 меньше хит-поинтов
      Один враг умер
      


      Вы посылаете вот эту скупую информацию на клиент и говорите: «у меня все удобно и круто тестируется». А клиентский программист шлет вас подальше и увольняется. А игра никогда не выпускается, потому что программист, который отвечал за модель перечитал, но не понял ФПшников.

      А если вы скажете: «так пусть рисует по тому, что получилось в результате в модели» — я вам отвечу: «попробуйте реализовать следующее»:
      Самурай имеет абилку телепортации и может ходить на 1 клетку.
      Игрок решает сначала телепорироваться с клетки (3, 3) на клетку (3, 4).
      А потом походить с клетки (3, 4) на (3, 5).
      Художники, неожиданно, нарисовали разные анимации для телепортации и для ходьбы.


      1. megasuperlexa
        03.02.2019 20:30
        -2

        Что-то много слов и каких-то левых ссылок. Я всего лишь попросил раскрыть заявленное — показать реализацию базового класса. Потому что без него непонятен поинт использования паттерна Command, который как известно, был придуман для замены отсутствующей на то время у товарищей GoF фичи языка, а именно first-class functions.
        Считаю, что с заявленной темой вы не справились, а только показали собственную предубежденность и склонность к некоему карго-культу, используя паттерн только потому, что он описан в широко известной книге. Впрочем, довольно старой.


        1. TheShock Автор
          03.02.2019 20:41

          Реализация базового класса неважна, пусть даже пустая.
          Я вам явно рассказал, почему функции не подходят, а нужны классы, которые могут переносить данные. Но сложно говорить с человеком, которому читать так тяжело, что он читает только половину статьи или комментария и сразу бежит отвечать.
          Еще хуже, что вы не хотите не только читать, но и думать. Задуматься, почему ваше бессмысленное повторение известной статьи (нет, это не левая ссылка) никак не относится к текущей теме и почему вы своим подходом не решите задачи, которые есть.

          Карго-культ именно у вас. Я показываю задачу и рассказываю о ее решение, а вы только кричите: «нееет, классы фигня, функции! пишите все на функциях! вы пишете на классах! фу! зачем, если есть функции?!»

          Еще раз. Перечитайте статью и комментарий. Если хоть немного подумаете, то поймете, почему вы предлагаете глупость.


          1. megasuperlexa
            03.02.2019 20:50
            -1

            Классы не переносят данные, а группируют данные и операции над ними. Для переноски данных существуют рекорды, в том числе и в любимом вами ФП.
            А я спрашивал про паттерн Command, но к сожалению вы даже с третьего раза не смогли этого понять. Увы.


            1. TheShock Автор
              03.02.2019 20:55

              Для переноски данных есть рекорды
              Ну давайте, напишите пример. Холиварить — не мешки ворочать. Я явно описал задачу и написал со своей стороны ее решение, но вы отказываетесь рассказать, как же, по вашему, ее нужно решать. Покажите класс. Научите нас, какой должна быть современная архитектура мечты.

              Классы не переносят данные, а группируют данные и операции над ними. Для переноски данных есть рекорды
              А Рекорды не реализуются через классы в некоторых языках, не?