Как стать автором
Обновить

Комментарии 15

Не совсем корректно сравнивать реализации композиции и наследования. Ведь композицию можно сделать иначе — без пересоздания объекта на каждый чих.
А если сделать объекты без методов? Пусть будут stateless сервисы по колдунству, что умеют работать с колдунами и паладинами, а с рыцарями пусть не работают? Взамен они будут возвращать или измененный объект или новый инстанс объекта с измененными свойствами.

Не совсем корректно сравнивать реализации композиции и наследования. Ведь композицию можно сделать иначе — без пересоздания объекта на каждый чих.

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

Во-первых, нафиг такое усложнение с Object.assign, которое ломает автодополнение?

function castSpell () {
  console.log(`${this.name} casts ${spell}!`);
  this.mana--;
}

function fight () {
  console.log(`${this.name} slashes at the foe!`);
  this.stamina--;
}

const fighter = (name) => ({{
    name,
    health: 100,
    stamina: 100,
    fight
})

const mage = (name) => ({
  name,
  health: 100,
  mana: 100,
  castSpell
})

const paladin = (name) => ({
  name,
  health: 100,
  mana: 100,
  stamina: 100,
  fight,
  castSpell
});


Но про такую фигню статью не напишешь, да? Да и бгомеркий this, который используют только грязныё джависты. О, кстати, давайте соблюдать DRY.

const character = (name) => ({
  name,
  health: 100,
});

const fighter = (name) => ({
  ...character(name),
  stamina: 100,
  fight
})

const mage = (name) => ({
  ...character(name),
  mana: 100,
  castSpell
})

const paladin = (name) => ({
  ...character(name),
  mana: 100,
  fight,
  castSpell
});


Опс, наследование получилось! Фигня какая. Это ведь почти горила-жрущая-банан! Упс. Тут или как мерзкие джависты, или по-модному, копипастя самого себя три раза в каждом объекте.
Я уж молчу, что ваше предложение вообще не гарантирует, что добавив fight в очередной инстанс программист добавит так же stamina.

найти ложку дегтя в предложенном решении...
… достаточно зачерпнуть ложкой в любом месте вашего решения.

Я уж молчу, что последнее решение точно так же делается на классах, как и без них.

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

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

Ну вот, к примеру: habr.com/ru/company/pixonic/blog/413729

А наследование и композиция просто должны применятся в разных местах. ОБА этих инструмента по-своему полезны.
И кстати, от того, что вы используете грязные процедуры в сокращенном виде, ваш процедурный код аж никак не становится функциональным.

const canCast = (state) => ({
  cast: (spell) => {
    console.log(`${state.name} casts ${spell}!`);
    state.mana  -  ; // <=== процедурщина 
  }
}) 


return Object.assign(
  state, // <=== процедурщина 
  canCast(state)
); 


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

Во-первых, нафиг такое усложнение с Object.assign, которое ломает автодополнение?

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

Но про такую фигню статью не напишешь, да? Да и бгомеркий this, который используют только грязныё джависты. О, кстати, давайте соблюдать DRY.

Да, в комментариях ниже предложили более красивое решение с сохранением той же логики.

Комментарий полезный, спасибо, но откуда такая агрессия, особенно к самому себе (вы про «джавистов» — это про себя же?) — не понятно. У автора статьи не было никаких наездов с личностными оттенками и он старался использовать ссылки на уважаемые ресурсы.
Потому что: «вот был плохой код с классами, мы отказались от классов, теперь у нас хороший код потому что без классов, а мы пишем в функциональном стиле!»

Если послушаете всяких фанатов Редакса — поймете, откуда агрессия)

«Джависты» — это все те устаревшие разработчики, которые продались корпорациям, не пьют смузи и до сих пор используют такие неудобные классы вместо того, чтобы перейти на столь гениальные и удобные подходы без единых изъянов. Я к ним, как вы уже поняли, отношусь
Починил ваш код, чтобы компилятор смог его cоптимизировать:

const canCast = (state) => {
  state.mana = 100
  state.cast = function() {
    console.log(`${this.name} casts fireball!`);
    this.mana  --  ;
  }
}

const character = (state) => {
  state.health = 100
}

const mage = (name) => {
  let state = { name }
  character(state)
  canCast(state)
  return state;
}
Красота, и читабельно и быстро, особенно в Firefox'e, спасибо за такой вариант.
1. cast и fight можно не инлайнить и пересоздавать, а объявить заранее.
2. тесты ОЧЕНЬ сильно скачут, то одно, то другое быстрее, разница в 2-3 раза иногда
А зачем пересоздавать функции если все равно this используем?

function cast() {
  console.log(`${this.name} casts fireball!`);
  this.mana--;
}
const canCast = (state) => {
  state.mana = 100
  state.cast = cast
}

// ну и как альтернатива замыканию
const boundCast = (state) => {
  state.mana = 100
  state.cast = cast.bind(state)
}
если немного изменить пример, то станет быстрее и меньше памяти будет потреблять, не больше чем с наследованием из примера
Код
const canCast = (state) => {
    state.cast = (spell) => {
        console.log(`${state.name} casts ${spell}!`);
        state.mana--;
    }
}

const canFight = (state) => {
    state.fight = () => {
        console.log(`${state.name} slashes at the foe!`);
        state.stamina--;
    }
}

function FighterComp(name) {
    this.name = name;
    this.health = 100;
    this.stamina = 100;
    canFight(this);
}

function MageComp(name) {
    this.name = name;
    this.health = 100;
    this.mana = 100;
    canCast(this);
}

function Paladin(name) {
    this.name = name;
    this.health = 100;
    this.mana = 100;
    this.stamina = 100;
    canCast(this);
    canFight(this);
}


но я практически уверен, что можно сделать еще лучше
Мне кажется есть проблемма с пониманием JS и фабрик. Попрубуйте так:
const allProperties = {
  addCommon(state, name) {
    state.name = name;
    state.health = 100;
  },
  addMagic(state) {
    state.mana = 100;
  },
  addFight(state, name) {
    state.stamina = 100;
  }
};

const allMethods = {
  cast(spell) {
    console.log(`${this.name} casts ${spell}!`);
    this.mana--;
  },
  fight(spell) {
    console.log(`${this.name} slashes at the foe!`);
    this.stamina--;
  },
};

function Mage2(name) {
  allProperties.addCommon(this, name);
  allProperties.addMagic(this);
}
Mage2.prototype.cast = allMethods.cast;

function Fighter2(name) {
  allProperties.addCommon(this, name);
  allProperties.addFight(this);
}
Fighter2.prototype.fight = allMethods.fight;


Результат 57 против 496. То есть в 6ть раз быстрее.
Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.