"Все - це карта", чи правильно я це роблю?


69

Я спостерігав, як Стюарт Сьєрра говорив " Думаючи дані ", і взяв одну з ідей, як принцип дизайну в цій грі, яку я створюю. Різниця в тому, що він працює в Clojure, а я працюю в JavaScript. Я бачу деякі основні відмінності між нашими мовами в тому, що:

  • Clojure - це ідіоматично функціональне програмування
  • Більшість станів незмінні

Я взяв ідею зі слайда "Все - це карта" (від 11 хвилин, 6 секунд до> 29 хвилин). Деякі речі, які він каже:

  1. Щоразу, коли ви бачите функцію, яка бере 2-3 аргументи, ви можете зробити випадок, щоб перетворити її на карту і просто передати карту. Є багато переваг у цьому:
    1. Вам не потрібно турбуватися про порядок аргументів
    2. Вам не потрібно турбуватися про будь-яку додаткову інформацію. Якщо є додаткові ключі, це насправді не наше занепокоєння. Вони просто перетікають, вони не заважають.
    3. Не потрібно визначати схему
  2. На відміну від передачі в Об'єкт немає даних. Але він вважає, що приховування даних може спричинити проблеми і завищено:
    1. Продуктивність
    2. Простота реалізації
    3. Як тільки ви спілкуєтесь через мережу або через різні процеси, ви все одно повинні домовитися про представлення даних обох сторін. Це додаткова робота, яку можна пропустити, якщо ви просто працюєте над даними.
  3. Найбільш актуально для мого питання. Це 29 хвилин: "Зробіть свої функції композиційними". Ось зразок коду, який він використовує для пояснення поняття:

    ;; Bad
    (defn complex-process []
      (let [a (get-component @global-state)
            b (subprocess-one a) 
            c (subprocess-two a b)
            d (subprocess-three a b c)]
        (reset! global-state d)))
    
    ;; Good
    (defn complex-process [state]
      (-> state
        subprocess-one
        subprocess-two
        subprocess-three))
    

    Я розумію, що більшість програмістів не знайомі з Clojure, тому я перепишу це в імперативному стилі:

    ;; Good
    def complex-process(State state)
      state = subprocess-one(state)
      state = subprocess-two(state)
      state = subprocess-three(state)
      return state
    

    Ось переваги:

    1. Легкий для тестування
    2. Легко дивитися на ці функції ізольовано
    3. Легко прокоментувати один рядок цього і побачити, який результат, видаливши один крок
    4. Кожен підпроцес може додати більше інформації про стан. Якщо підпроцесу потрібно щось передавати підпроцесу три, це так само просто, як додавання ключа / значення.
    5. Немає шаблону для вилучення потрібних вам даних із стану лише для того, щоб ви могли зберегти їх назад. Просто перейдіть у весь стан і нехай підпроцес призначить те, що йому потрібно.

Тепер повернемося до своєї ситуації: я взяв цей урок і застосував його до своєї гри. Тобто майже всі мої функції високого рівня беруть і повертають gameStateоб’єкт. Цей об’єкт містить усі дані гри. EG: Список badGuys, список меню, лут на місці тощо. Ось приклад моєї функції оновлення:

update(gameState)
  ...
  gameState = handleUnitCollision(gameState)
  ...
  gameState = handleLoot(gameState)
  ...

Що я тут хочу запитати, чи створив я якусь гидоту, яка перекрутила ідею, практичну лише для функціональної мови програмування? JavaScript не є ідіоматично функціональним (хоча це може бути написано таким чином), і писати незмінні структури даних дуже складно. Що мене хвилює, це він припускає, що кожен із цих підпроцесів є чистим. Чому потрібно зробити таке припущення? Рідко буває, що будь-яка з моїх функцій є чистою ( gameStateмаючи на увазі, я маю на увазі, що вони часто змінюють . У мене немає інших складних побічних ефектів, окрім цього). Чи розпадаються ці ідеї, якщо у вас немає незмінних даних?

Я переживаю, що одного разу я прокинусь і зрозумію, що весь цей дизайн - це шахрайство, і я дійсно щойно реалізував антидіаграму Big Ball Of Mud .


Чесно кажучи, я працював над цим кодом місяцями, і це було чудово. Я відчуваю, що отримую всі переваги, на які він стверджує. Мій код дуже просто для мене міркує. Але я єдина команда, тому я маю прокляття знань.

Оновлення

Я кодую цю схему 6+ місяців. Зазвичай до цього часу я забуваю, що я зробив, і ось де "чи це я написав чисто?" вступає в гру. Якби цього не було, я б справді боровся. Поки що я зовсім не борюся.

Я розумію, як необхідний ще один набір очей, щоб підтвердити його ремонтопридатність. Все, що я можу сказати, - це я переймаюся ремонтом в першу чергу. Я завжди найгучніший євангеліст за чистий код, де б я не працював.

Я хочу відповісти безпосередньо тим, у кого вже є поганий особистий досвід із таким способом кодування. Я тоді цього не знав, але думаю, що ми дійсно говоримо про два різні способи написання коду. Те, що я зробив це, здається більш структурованим, ніж те, що відчували інші. Коли у когось поганий особистий досвід із "Все - це карта", вони говорять про те, як важко підтримувати, оскільки:

  1. Ви ніколи не знаєте структури карти, якої вимагає функція
  2. Будь-яка функція може вимкнути вхід таким чином, якого ви ніколи не очікували. Ви повинні переглянути всю кодову базу, щоб дізнатись, як конкретний ключ потрапив на карту чи чому він зник.

Для тих, хто має такий досвід, можливо, базою коду було: "Все займає 1 з N типів карт". Моє, "Все бере 1 з 1 типу карти". Якщо ви знаєте структуру 1-го типу, ви знаєте структуру всього. Звичайно, ця структура зазвичай росте з часом. Ось чому...

Є одне місце, де можна шукати посилання на реалізацію (тобто: схему). Ця реалізація посилань - це код, який гра використовує, тому вона не може застаріти.

Що стосується другого пункту, я не додаю / видаляю ключі до карти поза межами еталонної реалізації, я просто мутую те, що вже є. У мене також є великий набір автоматизованих тестів.

Якщо ця архітектура врешті-решт руйнується під власною вагою, я додам друге оновлення. Інакше припустимо, що все йде добре :)


2
Класне питання (+1)! Я вважаю дуже корисною вправою спробувати реалізувати функціональні ідіоми нефункціональною (або не дуже сильно функціональною) мовою.
Джорджіо

15
Кожен, хто збирається сказати вам, що приховування інформації у стилі OO (із властивостями та функціями аксесуара) - погана річ через (як правило, мізерно) враження від продуктивності, а потім скаже вам перетворити всі параметри на карту, яка дає вам (набагато більша) накладні витрати на пошук хешу кожного разу, коли ви намагаєтеся отримати значення, можна безпечно ігнорувати.
Мейсон Уілер

4
@MasonWheeler дозволяє сказати, що ви маєте рацію з цим. Ви збираєтесь анулювати будь-який інший пункт, який він робить через те, що одна справа не так?
Даніель Каплан

9
У Python (і я вважаю, що більшість динамічних мов, включаючи Javascript), об'єктом справді є лише синтаксичний цукор для дикта / карти.
Лі Лі Райан

6
@EvanPlaice: Нотація Big-O може бути оманливою. Простий факт полягає в тому, що все є повільним порівняно з прямим доступом з двома-трьома інструкціями для індивідуального машинного коду, і на щось, що трапляється так часто, як виклик функції, ця накладні витрати зникнуть дуже швидко.
Мейсон Уілер

Відповіді:


42

Я раніше підтримував програму, де "все є картою". Це жахлива ідея. БУДЬ ласка, не робіть цього!

Коли ви вказуєте аргументи, які передаються функції, це дозволяє дуже легко зрозуміти, які значення функції потрібні. Це дозволяє уникнути передачі сторонніх даних функції, яка просто відволікає програміста - кожне передане значення означає, що воно необхідне, і це змушує програміста, який підтримує ваш код, повинні з'ясувати, для чого потрібні дані.

З іншого боку, якщо ви передасте все як карту, програміст, що підтримує ваш додаток, повинен буде повністю зрозуміти викликану функцію всіляко, щоб знати, які значення має містити карта. Ще гірше - дуже спокусливо повторно використовувати карту, передану в поточну функцію, для передачі даних наступним функціям. Це означає, що програміст, який підтримує вашу програму, повинен знати всі функції, викликані поточною функцією, щоб зрозуміти, що робить поточна функція. Це якраз протилежна цілі написання функцій - відволікаючи проблеми, щоб вам не довелося думати про них! Тепер уявіть собі 5 дзвінків у глибині та 5 викликів у ширину кожного. Це пекло, що слід тримати в думці, і пекло багато помилок.

"все є картою" також, здається, призводить до використання карти як зворотного значення. Я це бачив. І, знову ж таки, біль. Викликані функції не повинні ніколи перезаписувати повернене значення один одного - якщо ви не знаєте функціональності всього і не знаєте, що значення X для вхідної карти потрібно замінити для наступного виклику функції. І поточній функції потрібно змінити карту, щоб повернути її значення, яке іноді повинно перезаписувати попереднє значення, а іноді й не.

редагувати - приклад

Ось приклад, де це було проблематично. Це веб-додаток. Вхід користувача було прийнято з рівня користувальницького інтерфейсу та розміщено на карті. Потім були викликані функції для обробки запиту. Перший набір функцій перевірить на помилкове введення. Якщо виникла помилка, повідомлення про помилку буде розміщене на карті. Функція виклику перевірить карту для цього запису і записала б значення в інтерфейсі, якщо воно існує.

Наступний набір функцій запустить бізнес-логіку. Кожна функція візьме карту, видалить деякі дані, змінила б деякі дані, оперувала даними на карті та поставила результат на карту тощо. Наступні функції очікували б результатів від попередніх функцій на карті. Щоб виправити помилку в наступній функції, вам довелося дослідити всі попередні функції, а також абонент, щоб визначити всюди, де може бути встановлено очікуване значення.

Наступні функції витягуватимуть дані з бази даних. Або, скоріше, вони перенесуть карту до рівня доступу до даних. DAL перевірить, чи карта містить певні значення, щоб контролювати, як виконується запит. Якщо "justcount" був ключовим, то запит буде "count select foo from bar". Будь-яка з функцій, яку раніше називали, може містити ту, яка додала на карту "justcount". Результати запиту будуть додані до тієї ж карти.

Результати будуть відповідати абоненту (бізнес-логіка), який би перевіряв карту, що робити. Частина цього походитиме з речей, які були додані на карту за допомогою початкової логіки бізнесу. Деякі будуть надходити з даних із бази даних. Єдиний спосіб дізнатися, звідки він узявся - це знайти код, який додав його. І інше місце, яке також може додати його.

Код фактично був монолітним безладом, який вам довелося зрозуміти в цілому, щоб знати, звідки походить один запис на карті.


2
Ваш другий абзац має для мене сенс, і це справді звучить так, ніби він смокче. З вашого третього абзацу я розумію, що ми дійсно не говоримо про один і той же дизайн. Справа в "повторному використанні". Було б неправильно уникати цього. І я справді не можу стосуватися вашого останнього абзацу. Я маю на увазі кожну функцію, gameStateне знаючи нічого про те, що сталося до або після неї. Він просто реагує на дані, які йому надаються. Як ви потрапили в ситуацію, коли функції наступали один на одного пальцями ніг? Чи можете ви навести приклад?
Даніель Каплан

2
Я додав приклад, щоб спробувати зробити це трохи зрозумілішим. Сподіваюся, це допомагає. Крім того, існує різниця між проходженням навколо чітко визначеного об'єкта стану порівняно з проходженням навколо крапки, яка встановлюється зміненою багатьма місцями з багатьох причин, змішуючи логіку інтерфейсу, бізнес-логіку та логіку доступу до бази даних
atk

28

Особисто я б не рекомендував цю схему в жодній парадигмі. Це полегшує написання спочатку за рахунок ускладнення міркувань про пізніше.

Наприклад, спробуйте відповісти на наступні запитання щодо кожної функції підпроцесу:

  • Які поля stateвін вимагає?
  • Які поля він змінює?
  • Які поля є незмінними?
  • Чи можете ви безпечно переставити порядок функцій?

За цією схемою ви не можете відповісти на ці запитання, не прочитавши всю функцію.

У мові, орієнтованій на об’єкти, модель має ще менше сенсу, оскільки стан відстеження - це те, що роблять об’єкти.


2
"переваги незмінюваності зменшуються тим, чим більше отримують ваші незмінні об'єкти" Чому? Це коментар щодо продуктивності чи ремонту? Будь ласка, докладіть детальніше це речення.
Даніель Каплан

8
@tieTYT Immutables добре працюють, коли є щось невелике (наприклад, числовий тип). Ви можете їх скопіювати, створити, відкинути, полегшити їх з досить низькою вартістю. Коли ви почнете мати справу з цілими станами гри, складеними з глибоких і великих карт, дерев, списків і десятків, якщо не сотень змінних, вартість їх копіювання або видалення зростає (а маховики стають непрактичними).

3
Я бачу. Це випуск "непорушних даних невідчутними мовами" чи "непорушних даних"? І.Є .: Можливо, це не проблема коду Clojure. Але я бачу, як це в JS. Болісно також написати весь код котла, щоб це зробити.
Даніель Каплан

3
@MichaelT та Карл: якщо чесно, то слід дійсно згадати іншу сторону історії незмінності / ефективності. Так, наївне використання може бути жахливо неефективним, тому люди придумали кращі підходи. Додаткову інформацію див. У роботі Кріса Окасакі.

3
@MattFenwick Мені особисто подобається незмінне. У роботі з ниткою я знаю речі про незмінне і можу працювати і копіювати їх безпечно. Я ставлю його до виклику параметра і передаю іншому, не переживаючи, що хтось змінить його, коли він повернеться до мене. Якщо говорити про складний ігровий стан (запитання використано це як приклад - я б з жахом подумав про щось настільки «просте», як стан гри в іграх, як непорушний), незмінність, мабуть, неправильний підхід.

12

Начебто ви робите це, фактично, ручна державна монада; що я б зробив - це створити (спрощений) зв'язуючий комбінатор і повторно виразити зв’язки між вашими логічними кроками, використовуючи це:

function stateBind() {
    var computation = function (state) { return state; };
    for ( var i = 0 ; i < arguments.length ; i++ ) {
        var oldComp = computation;
        var newComp = arguments[i];
        computation = function (state) { return newComp(oldComp(state)); };
    }
    return computation;
}

...

stateBind(
  subprocessOne,
  subprocessTwo,
  subprocessThree,
);

Ви навіть можете використовувати stateBindдля побудови різних підпроцесів з підпроцесів і продовжувати вниз по дереву зв'язуючих комбінаторів, щоб відповідним чином структурувати обчислення.

Для пояснення повної, непростої монади держави та відмінного ознайомлення з монадами взагалі в JavaScript дивіться цю публікацію в блозі .


1
Гаразд, я вивчу це (і прокоментую це згодом). Але що ви думаєте про ідею використання візерунка?
Даніель Каплан

1
@tieTYT Я думаю, що сама модель є дуже хорошою ідеєю; монада стану в цілому є корисним інструментом структурування коду для псевдозмінюваних алгоритмів (алгоритмів, які незмінні, але імітують незмінність).
Полум'я Птарієна

2
+1 за те, що зауважив, що ця модель є по суті монадою. Однак я не погоджуюся, що це гарна ідея мовою, яка насправді має незмінність. Монада - це спосіб забезпечити можливість глобальних / змінних станів мовою, яка не дозволяє мутувати. ІМО, мовою, яка не примушує незмінність, модель Монада - це просто психічна мастурбація.
Лі Лі Райан

6
@LieRyan Monads взагалі насправді не мають нічого спільного з незмінністю або глобалізацією; тільки державна монада спеціально робить (адже саме це і покликане робити). Я також не погоджуюся з тим, що монада держави не є корисною для мови з незмінністю, хоча реалізація, що покладається на мутаційність під нею, може бути більш ефективною, ніж та, яку я дав незмінний (хоча я в цьому зовсім не впевнений). Монадічний інтерфейс може забезпечити можливості високого рівня, які в іншому випадку не доступні. stateBindКомбінатор, який я дав, є дуже простим прикладом цього.
Полум'я Птарієна

1
@LieRyan I коментар другого Птарієна - більшість монардів - це не про стан чи незмінність, і навіть той, який є, конкретно, не про глобальну державу. Насправді монади працюють досить добре в мовах OO / імператив / змінні.

11

Отже, здається, що між Clojure є багато дискусій між ефективністю такого підходу. Я думаю, що може бути корисним переглянути філософію Річака Хікі щодо того, чому він створив Clojure, щоб таким чином підтримувати абстрагування даних :

Фогус: Отже, коли випадкові складності будуть зменшені, як Clojure може допомогти вирішити проблему? Наприклад, ідеалізована об'єктно-орієнтована парадигма має на меті сприяти повторному використанню, але Clojure не є класично об'єктно-орієнтованою - як можна структурувати наш код для повторного використання?

Хікі: Я б сперечався про OO та повторне використання, але, безумовно, можливість повторного використання речі робить проблему простою, оскільки ви не винаходите колеса замість того, щоб будувати машини. А Clojure, що знаходиться в JVM, надає багато колес - бібліотеки. Що робить бібліотеку багаторазовою? Він повинен добре виконати одну чи декілька речей, бути відносно самодостатнім та пред'являти мало вимог до клієнтського коду. Жодне з цього не випадає з ОО, і не всі бібліотеки Java відповідають цим критеріям, але багато хто з них.

Коли ми опустимось до рівня алгоритму, я думаю, що ОО може серйозно перешкодити повторному використанню. Зокрема, використання об'єктів для представлення простих інформаційних даних є майже злочинним у його генеруванні інформаційних мікро-мов, тобто класових методів, порівняно з набагато потужнішими, декларативними та загальними методами, такими як реляційна алгебра. Придумати клас із власним інтерфейсом для зберігання інформації - це як придумати нову мову для написання кожної новели. Це анти-повторне використання, і, я думаю, призводить до вибуху коду в типових програмах OO. Clojure уникає цього і натомість виступає за просту асоціативну модель інформації. З його допомогою можна написати алгоритми, які можна повторно використовувати для різних типів інформації.

Ця асоціативна модель є лише однією з кількох абстракцій, що постачаються Clojure, і це справжні основи її підходу до повторного використання: функції абстракції. Наявність відкритого та великого набору функцій працює на відкритому та малому наборах розширюваних абстракцій - це ключ до алгоритмічного повторного використання та сумісності бібліотеки. Переважна більшість функцій Clojure визначається з точки зору цих абстракцій, і автори бібліотек також проектують свої вхідні та вихідні формати з точки зору них, реалізуючи величезну сумісність між незалежно розробленими бібліотеками. Це суворо на відміну від DOM та інших подібних речей, які ви бачите в ОО. Звичайно, ви можете зробити подібну абстракцію в OO за допомогою інтерфейсів, наприклад, колекцій java.util, але ви можете так само легко, як у java.io.

Фогус ще раз повторює ці моменти у своїй книзі Функціональний Javascript :

У цій книзі я буду використовувати підхід до використання мінімальних типів даних для представлення абстракцій, від наборів до дерев до таблиць. Однак у JavaScript, хоча його типи об’єктів надзвичайно потужні, інструменти, що надаються для роботи з ними, не є цілком функціональними. Натомість більша схема використання, пов’язана з об’єктами JavaScript, полягає в приєднанні методів для поліморфної диспетчеризації. На щастя, ви також можете розглядати неназваний (не побудований за допомогою функції конструктора) об’єкт JavaScript як просто асоціативний сховище даних.

Якщо єдиними операціями, які ми можемо виконати на об’єкті Book або екземплярі типу Employee, є setTitle або getSSN, то ми заблокували наші дані в мікро-мовах на деталь інформації (Hickey 2011). Більш гнучкий підхід до моделювання даних - це асоціативна техніка даних. Об'єкти JavaScript, навіть мінус машини-прототипи, є ідеальними засобами для асоціативного моделювання даних, де названі значення можуть бути структуровані для формування моделей даних вищого рівня, до яких можна отримати однаковий спосіб.

Незважаючи на те, що інструменти для маніпулювання та доступу до об’єктів JavaScript, оскільки карти даних є рідкісними в самому JavaScript, на щастя, Underscore забезпечує безліч корисних операцій. Серед найпростіших функцій, які можна зрозуміти - _.keys, _.values ​​та _.pluck. І _.keys, і _.values ​​називаються відповідно до їх функціональності, тобто взяти об’єкт і повернути масив його ключів або значень ...


2
Я читав це інтерв'ю Фогуса / Хікі раніше, але досі не зміг зрозуміти, про що він говорив. Дякую за вашу відповідь. Досі не впевнений, чи дасть Хікі / Фогус моєму дизайну своє благословення. Я стурбований, я сприйняв дух їхніх порад до крайності.
Даніель Каплан

9

Адвокат диявола

Я думаю, що це питання заслуговує на захисника диявола (але, звичайно, я упереджений). Я думаю, що @KarlBielefeldt робить дуже хороші моменти, і я хотів би вирішити їх. Спершу хочу сказати, що його бали чудові.

Оскільки він згадав, що це не дуже вдалий зразок навіть у функціональному програмуванні, я розгляну JavaScript та / або Clojure у своїх відповідях. Одне надзвичайно важливе схожість між цими двома мовами - це їх динамічне введення. Я б більше погоджувався з його пунктами, якби я реалізував це на мові статичного типу, як Java чи Haskell. Але я вважаю, що альтернатива шаблону "Все - це карта" є традиційним дизайном OOP в JavaScript, а не мовою статичного типу (я сподіваюся, що я не налаштовую аргумент "сламан", роблячи це, будь ласка, дай мені знати).

Наприклад, спробуйте відповісти на наступні запитання щодо кожної функції підпроцесу:

  • Яких державних полів воно вимагає?

  • Які поля він змінює?

  • Які поля є незмінними?

Як ви зазвичай відповідаєте на ці запитання мовою, що динамічно набирається? Перший параметр функції може бути названий foo, але що це? Масив? Об'єкт? Об'єкт масивів об’єктів? Як дізнаєтесь? Єдиний спосіб, про який я знаю - це

  1. читати документацію
  2. Подивіться на функціональний орган
  3. подивіться на тести
  4. здогадайтесь і запустіть програму, щоб перевірити, чи працює вона.

Я не думаю, що модель "Все - це карта" не має жодного значення. Це все ще єдині способи, з яких я знаю, щоб відповісти на ці запитання.

Також майте на увазі, що в JavaScript та більшості імперативних мов програмування будь-який functionможе вимагати, змінювати та ігнорувати будь-який стан, до якого він може отримати доступ, і підпис не має ніякої різниці: Функція / метод може робити щось із глобальним станом або з одиночним. Підписи часто брешуть.

Я не намагаюся встановити помилкову дихотомію між "Все - це карта" та погано розробленим кодом ОО. Я просто намагаюся зазначити, що наявність підписів, які приймають менш / більш тонкі / грубозернисті параметри, не гарантує, що ви знаєте, як ізолювати, налаштувати та викликати функцію.

Але, якщо ви дозволите мені використовувати таку помилкову дихотомію: У порівнянні з написанням JavaScript традиційним способом OOP, "Все - це карта" здається кращим. Традиційним способом OOP функція може вимагати, змінювати або ігнорувати стан, який ви переходите, або заявляти, що ви не переходите. З цим шаблоном "Все є карта" ви вимагаєте лише змінити або проігнорувати стан, який ви передаєте в.

  • Чи можете ви безпечно переставити порядок функцій?

У моєму коді так. Дивіться мій другий коментар до відповіді @ Evicatos. Можливо, це лише тому, що я роблю гру, але не можу сказати. У грі, яка оновлює 60-кратну секунду, це насправді не має значення, dead guys drop lootтоді good guys pick up lootчи навпаки. Кожна функція все ще робить саме те, що має робити, незалежно від порядку, який вони виконують. Ті самі дані просто надходять до них під час різних updateдзвінків, якщо ви поміняєте замовлення. Якщо у вас good guys pick up lootтоді є dead guys drop loot, хороші хлопці підберуть бабло в наступному, updateі це не буде великою справою. Людина не зможе помітити різницю.

Принаймні, це був мій загальний досвід. Я відчуваю себе дуже вразливим, визнаючи це публічно. Можливо, вважати це нормальним - це дуже , дуже погано. Дайте мені знати, чи зробив я тут якусь страшну помилку. Але, якщо у мене є, це дуже легко переставити функції так , замовлення dead guys drop lootпотім good guys pick up lootзнову. На написання цього абзацу знадобиться менше часу, ніж пішло: P

Можливо, ви думаєте, що "мертві хлопці повинні спустити бабло спочатку. Було б краще, якби ваш код виконав цей наказ". Але, чому ворогам доводиться скидати бабло, перш ніж ви зможете забрати бабло? Для мене це не має сенсу. Можливо, бабло випала 100 років updatesтому. Не потрібно перевіряти, чи довільний поганий хлопець повинен забрати бабло, що вже є на землі. Тому я вважаю, що порядок цих операцій є абсолютно довільним.

Цілком природно писати зв'язані кроки з цим малюнком, але важко помітити ваші поєднані кроки в традиційному OOP. Якщо я писав традиційний ООП, природним, наївним способом мислення є зробити dead guys drop lootповернення Lootпредметом, який мені належить передати good guys pick up loot. Я б не зміг упорядкувати ці операції, оскільки перша повертає вхід другої.

У мові, орієнтованій на об’єкти, модель має ще менше сенсу, оскільки стан відстеження - це те, що роблять об’єкти.

Об'єкти мають стан, і ідіоматично мутувати стан, змушуючи його історію просто зникнути ... якщо ви не вручну пишете код для його відстеження. Яким чином відстежують стан "що вони роблять"?

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

Правильно, як я вже сказав, "рідко будь-яка моя функція є чистою". Вони завжди працюють лише за своїми параметрами, але вони мутують свої параметри. Це компроміс, який я відчував, що мені довелося зробити, застосовуючи цей шаблон до JavaScript.


4
"Перший параметр функції може бути названий foo, але що це?" Тому ви не називаєте свої параметри "foo", а "повторення", "parent" та інші імена, які дозволяють зрозуміти, що очікується в поєднанні з ім'ям функції.
Себастьян Редл

1
Я повинен погодитися з вами по всіх пунктах. Єдина проблема, яку Javascript справді виникає з цією схемою, полягає в тому, що ви працюєте над мутаційними даними, і, як такий, ви дуже ймовірно мутуєте стан. Однак є бібліотека, яка надає вам доступ до clojure структур даних у простому javascript, хоча я і забуваю, як це називається. Передача аргументів як об'єкта не є нічим чутим, jquery робить це декількома місцями, але документує, які частини об'єкта вони використовують. Хоча я особисто відокремив UI-поля та GameLogic-поля, але все, що для вас працює :)
Робін Хеггелунд Хансен

@SebastianRedl Що я повинен пройти parent? Чи є repetitionsі масив чисел чи рядків чи це не має значення? А може, повтори - це просто число, яке представляє кількість репресій, які я хочу? Там багато apis, які просто беруть об'єкт з опціями . Світ - це краще місце, якщо ти правильно називаєш речі, але це не гарантує, що ти будеш знати, як користуватися api, без запитань.
Даніель Каплан

8

Я виявив, що мій код, як правило, структурований так:

  • Функції, які беруть карти, як правило, більші і мають побічні ефекти.
  • Функції, які беруть аргументи, як правило, менше і є чистими.

Я не ставив собі за мету створити це розрізнення, але саме так воно часто опиняється в коді. Я не думаю, що використання одного стилю обов'язково заперечує інший.

Чисті функції легко перевірити. Більш великі з картами більше потрапляють у тестову зону "інтеграції", оскільки вони, як правило, залучають більше рухомих частин.

У javascript одна річ, яка дуже допомагає, - це використовувати щось на зразок бібліотеки відповідностей Meteor's для перевірки параметрів. Це дає зрозуміти, що функція очікує, і може обробляти карти досить чисто.

Наприклад,

function foo (post) {
  check(post, {
    text: String,
    timestamp: Date,
    // Optional, but if present must be an array of strings
    tags: Match.Optional([String])
    });

  // do stuff
}

Див. Http://docs.meteor.com/#match для отримання додаткової інформації.

:: ОНОВЛЕННЯ ::

Відеозапис Стюарта Сьєрри про Clojure / West "Clojure in the Large" також стосується цієї теми. Як і ОП, він контролює побічні ефекти як частину карти, тому тестування стає набагато простішим. У нього також є повідомлення в блозі, в якому викладається його поточний робочий процес Clojure, який видається актуальним.


1
Я думаю, що мої коментарі до @Evicatos будуть детальніше пояснити мою позицію тут. Так, я мутую, і функції не є чистими. Але мої функції дуже легко перевірити, особливо з огляду на дефекти регресії, які я не планував тестувати. Половина кредиту припадає на JS: Дуже просто побудувати "карту" / об'єкт із лише даними, які мені потрібні для мого тесту. Тоді це так само просто, як передавати його та перевіряти мутації. Побічні ефекти завжди представлені в карті, так що вони легко перевірити.
Даніель Каплан

1
Я вважаю, що прагматичне використання обох методів - це "правильний" шлях. Якщо для вас тестування просте і ви можете пом'якшити мета-проблему передачі необхідних полів іншим розробникам, це звучить як виграш. Дякую за запитання; Мені приємно читати цікаву дискусію, яку ви розпочали.
алан

5

Основний аргумент, який я можу подумати проти цієї практики, - це те, що дуже важко сказати, які дані фактично потрібні функції.

Це означає, що майбутні програмісти в кодовій базі повинні знати, як функція, що викликається, працює внутрішньо - і будь-які вкладені виклики функції - щоб викликати її.

Чим більше я думаю про це, тим більше ваш об'єкт gameState пахне глобальним. Якщо так воно використовується, навіщо його передавати?


1
Так, оскільки я зазвичай його мутую, це глобальний характер. Навіщо турбуватися пропускати його навколо? Я не знаю, це справедливе питання. Але моя кишка говорить мені, що якби я припинив її передавати, мою програму миттєво стало б важче міркувати. Кожна функція могла зробити все або нічого глобальній державі. Такий потенціал ви бачите в підписі функції. Якщо ви не можете сказати, я не впевнений ні в чому з цього :)
Даніель Каплан

1
BTW: re: головний аргумент проти цього: Це здається правдою, чи були це в clojure чи JavaScript. Але це важливий момент. Можливо, перераховані переваги значно переважають негатив цього.
Даніель Каплан

2
Тепер я знаю, чому я турбуюсь пропускати його навколо, навіть якщо це глобальна змінна: Це дозволяє мені писати чисті функції. Якби я змінити мій gameState = f(gameState)до f(), це набагато важче перевірити f. f()може повертати іншу річ кожного разу, коли я її закликаю. Але легко зробити f(gameState)повернення одне й те саме щоразу, коли йому дають один і той же вхід.
Даніель Каплан

3

Існує більше відповідна назва для того, що ви робите, ніж Велика кулька грязі . Те , що ви робите , це називається Бог об'єкт шаблон. На перший погляд це не так, але в Javascript дуже мало різниці між

update(gameState)
  ...
  gameState = handleUnitCollision(gameState)
  ...
  gameState = handleLoot(gameState)
  ...

і

{
  ...
  handleUnitCollision: function() {
    ...
  },
  ...
  handleLoot: function() {
    ...
  },
  ...
  update: function() {
    ...
    this.handleUnitCollision()
    ...
    this.handleLoot()
    ...
  },
  ...
};

Чи це гарна ідея, мабуть, залежить від обставин. Але це, безумовно, відповідає способу Clojure. Однією з цілей Clojure є усунення того, що Річ Хікі називає "випадковою складністю". Кілька комунікаційних об'єктів, безумовно, складніші, ніж один об'єкт. Якщо ви розділите функціональність на кілька об'єктів, вам раптом доведеться турбуватися про спілкування, координацію та розподіл обов'язків. Це ускладнення, які є лише випадковими для вашої первісної мети написання програми. Ви повинні побачити розмову Rich Hickey Simple, яку легко зробити . Я думаю, що це дуже гарна ідея.


Пов'язане, більш загальне запитання [ programmers.stackexchange.com/questions/260309/… моделювання даних проти традиційних класів)
user7610

"В об'єктно-орієнтованому програмуванні об'єкт" бог "- це об'єкт, який знає занадто багато або робить занадто багато. Об'єкт" бог "є прикладом антидіаграму." Тож об'єкт бога - це не дуже добре, але, здається, ваше повідомлення говорить про зворотне. Це трохи бентежить мене.
Даніель Каплан

@tieTYT ви не займаєтесь об’єктно-орієнтованим програмуванням, тож це нормально
user7610

Як ти прийшов до такого висновку ("це нормально")?
Даніель Каплан

Проблема з об'єктом Бога в ОО полягає в тому, що "Об'єкт настільки усвідомлює все або всі об'єкти стають настільки залежними від об'єкта бога, що коли є зміна або помилка, яку потрібно виправити, вона стає справжнім кошмаром для здійснення". Джерело У вашому коді є ще один об'єкт поруч із об'єктом Бога, тому друга частина не є проблемою. Що стосується першої частини, то об'єктом вашого Бога є програма, і ваша програма повинна знати про все. Так що це теж нормально.
користувач7610

2

Я просто зіткнувся з цією темою раніше сьогодні, граючи з новим проектом. Я працюю в Clojure, щоб зробити гру в покер. Я представив номінали та костюми як ключові слова і вирішив представити карту так, як карта

{ :face :queen :suit :hearts }

Я міг так само скласти їх списки або вектори двох елементів ключових слів. Я не знаю, чи відрізняє це пам'ять / продуктивність, тому я зараз просто збираюся з картами.

Якщо пізніше я передумав, я вирішив, що більшість частин моєї програми повинні пройти "інтерфейс" для доступу до фрагментів карти, щоб деталі реалізації контролювалися та приховувались. У мене є функції

(defn face [card] (card :face))
(defn suit [card] (card :suit))

що використовує решта програми. Картки передаються до функцій як карти, але функції використовують узгоджений інтерфейс для доступу до карт, і тому вони не повинні мати можливість псуватися.

У моїй програмі карта, ймовірно, буде колись лише двозначною картою. У питанні весь стан гри передається навколо як карта. Стан гри буде набагато складнішим, ніж одна картка, але я не думаю, що винні використовувати карту. В мові, необхідній для об'єктів, я міг би так само мати один великий об'єкт GameState і викликати його методи, і мати ту ж проблему:

class State
  def complex-process()
    state = clone(this) ; or just use 'this' below if mutation is fine
    state.subprocess-one()
    state.subprocess-two()
    state.subprocess-three()
    return state

Тепер він орієнтований на об'єкти. Чи є в цьому щось особливо не так? Я не думаю, що ви просто делегуєте роботу функціям, які вміють поводитися з об'єктом State. Незалежно від того, чи працюєте ви з картами або об’єктами, вам слід насторожитися, коли розділити їх на більш дрібні шматки. Тому я кажу, що використовувати карти - це зовсім чудово, якщо ви використовуєте той самий догляд, який би ви використовували з об'єктами.


2

З того, що (мало) я бачив, використовуючи карти або інші вкладені структури, щоб створити єдиний глобальний незмінний стан об'єкта, як це, є досить поширеним у функціональних мовах, принаймні чистих, особливо при використанні штату Монада як @ Ptharien'sFlame mentioend .

Дві перешкоди для ефективного використання цього, про який я бачив / читав (та інші відповіді тут згадували):

  • Мутація (глибоко) вкладеного значення у (незмінному) стані
  • Приховування більшості штатів від функцій, які йому не потрібні, і просто даючи їм те небагато, що їм потрібно працювати / мутувати

Існує кілька різних методик / загальних моделей, які можуть допомогти полегшити ці проблеми:

Перший - це блискавки : вони дозволяють перейти та мутувати стан вглиб непорушної вкладеної ієрархії.

Інша - лінзи : вони дозволяють вам сфокусуватись у структурі до певного місця та прочитати / мутувати значення там. Ви можете комбінувати різні об'єктиви разом, щоб зосередитись на різних речах, подібних до регульованого ланцюга властивостей у OOP (де ви можете замінити змінні на фактичні назви властивостей!)

Нещодавно «Призматик» зробив публікацію в блозі про використання подібної техніки, серед іншого, в JavaScript / ClojureScript, яку слід перевірити. Вони використовують курсори (які вони порівнюють з блискавками) для стану вікна для функцій:

Om відновлює інкапсуляцію та модульність за допомогою курсорів. Курсори надають вікнам, що підтримують оновлення, у певні частини стану програми (подібно до блискавок), що дозволяє компонентам приймати посилання лише на відповідні частини глобального стану та оновлювати їх без контексту.

IIRC, вони також торкаються незмінності JavaScript у цій публікації.


У згадуваній ОП розмови також обговорюється використання функції оновлення для обмеження сфери застосування, яку функція може оновити до піддіаграму карти стану. Думаю, ще ніхто цього не виховував.
користувач7610

@ user7610 Хороший улов, я не можу повірити, що забув згадати цю - мені подобається ця функція (та assoc-inін.). Здогадуюсь, у мене був просто Хаскелл на мозку. Цікаво, чи хтось зробив порту JavaScript? Люди, мабуть, не підняли це, тому що (як і я) вони не дивилися розмову :)
Пауль

@paul у певному сенсі вони є, тому що це доступно в ClojureScript, але я не впевнений, що це "рахується" у вашому розумі. Він може існувати в PureScript, і я вважаю, що принаймні одна бібліотека, яка забезпечує незмінні структури даних у JavaScript. Я сподіваюся, що принаймні одна з них має це, інакше користуватися їм буде незручно.
Даніель Каплан

@tieTYT Я думав про вроджену реалізацію JS, коли я зробив цей коментар, але ви добре зазначаєте про ClojureScript / PureScript. Я повинен заглянути в незмінний JS і подивитися, що там, я раніше не працював з ним.
Пауль

1

Буде це гарна ідея чи ні, насправді буде залежати від того, що ви робите зі станом всередині цих підпроцесів. Якщо я правильно розумію приклад Clojure, словники, що повертаються, - це не ті самі державні словники, які передаються. Це копії, можливо, з доповненнями та модифікаціями, які (я припускаю) Clojure здатний ефективно створювати, оскільки від цього залежить функціональний характер мови. Оригінальні словники стану кожної функції не змінюються жодним чином.

Якщо я правильно зрозуміти, ви є зміни стану об'єктів ви проходите в свій яваскрипт функцію , а не повертати копію, а значить , ви робите що - то дуже, дуже, відмінне від того , що робить цей код Clojure. Як зауважив Майк Партридж, це в основному лише глобальний характер, на який ви явно переходите та повертаєтесь із функцій без реальних причин. На даний момент я думаю, що це просто змушує тебе думати, що ти робиш щось, чого насправді не є.

Якщо ви фактично створюєте копії стану, змінюєте його, а потім повертаєте цю змінену копію, продовжуйте продовжувати. Я не впевнений, що це обов'язково найкращий спосіб виконати те, що ви намагаєтеся зробити в Javascript, але це, ймовірно, "близький" до того, що робить цей приклад Clojure.


1
Зрештою, це справді "дуже, дуже різне"? У прикладі Clojure він замінює свій старий стан новим. Так, справжня мутація не відбувається, особистість просто змінюється. Але у своєму "хорошому" прикладі, як написано, він не має можливості отримати копію, передану в підпроцес-два. Ідентифікація цього значення була перезаписана. Тому я вважаю, що "дуже-дуже різна" річ - це лише деталізація мовної реалізації. Принаймні в контексті того, що ви виховуєте.
Даніель Каплан

2
З прикладом Clojure відбувається дві речі: 1) перший приклад залежить від виклику функцій у певному порядку, і 2) функції чисті, тому вони не мають жодних побічних ефектів. Оскільки функції у другому прикладі є чистими та мають однаковий підпис, ви можете їх переупорядкувати, не турбуючись про які-небудь приховані залежності від порядку, в якому вони викликаються. Якщо ви мутуєте стан своїх функцій, ви не маєте такої ж гарантії. Мутація стану означає, що ваша версія не є такою складовою, що було оригінальною причиною для словника.
Евікатос

1
Вам доведеться показати мені приклад, тому що, на моєму досвіді цього, я можу переміщувати речі за бажанням, і це має дуже незначний ефект. Тільки щоб довести це собі, я перемістив два випадкових виклику підпроцесору посеред своєї update()функції. Я перемістив одну вгору і одну вниз. Всі мої тести все-таки пройшли, і коли я грав у свою гру, я не помітив поганих наслідків. Я відчуваю , що мої функції просто , як компонований в якості прикладу Clojure. Ми обидва викидаємо старі дані після кожного кроку.
Даніель Каплан

1
Ваші тести, які проходять і не помічають жодних побічних ефектів, означають, що ви зараз не мутуєте жодного стану, який має несподівані побічні ефекти в інших місцях. Оскільки ваші функції не є чистими, хоча ви не маєте гарантії, що завжди буде так. Я думаю, що я повинен принципово нерозуміти щось щодо вашої реалізації, якщо ви скажете, що ваші функції не є чистими, але ви викидаєте старі дані після кожного кроку.
Евікатос

1
@Evicatos - Якщо бути чистим і мати однаковий підпис, це не означає, що порядок функцій не має значення. Уявіть собі розрахунок ціни із застосованими фіксованими та процентними знижками. (-> 10 (- 5) (/ 2))повертає 2.5. (-> 10 (/ 2) (- 5))повертає 0.
Зак

1

Якщо у вас є об’єкт глобального стану, який іноді називають "об'єктом бога", який передається кожному процесу, ви в результаті збиваєте з ряду факторів, які збільшують зв'язок, зменшуючи згуртованість. Усі ці фактори негативно впливають на довготривалість ремонту.

Зв'язування трампа Це виникає внаслідок передачі даних за допомогою різних методів, які не потребують майже всіх даних, для того, щоб дістати їх до місця, де реально з цим можна боротися. Цей тип зв'язку подібний до використання глобальних даних, але може містити більше вмісту. Зчеплення з Трампом - це протилежне "необхідно знати", яке використовується для локалізації ефектів і для стримування шкоди, яку може нанести один помилковий код у всій системі.

Навігація по даних Кожен підпроцес у вашому прикладі повинен знати, як дістатись до саме тих даних, які йому потрібні, і потрібно вміти обробляти їх і, можливо, конструювати новий глобальний об'єкт стану. Це логічний наслідок зчіплення бродяг; весь контекст даної потрібен для роботи над датою. Знову ж таки, немісцеві знання - це погана річ.

Якщо ви проходили в "блискавці", "лінзі" або "курсорі", як описано в публікації від @paul, це одне. Ви б містили доступ і дозволяли блискавці тощо контролювати читання та запис даних.

Порушення єдиної відповідальності Заявляючи, що кожен із "підпроцесів-один", "підпроцес-два" та "підпроцес-три" несуть лише одну відповідальність, а саме - створити новий глобальний державний об'єкт з правильними значеннями в ньому, є надзвичайним редукціонізмом. Зрештою, це все біти, чи не так?

Моя думка тут полягає в тому, що володіння всіма основними компонентами вашої гри має всі ті ж обов'язки, що і ваша гра перемагає призначення делегування та факторингу.

Системний вплив

Основний вплив вашого дизайну - низька ремонтопридатність. Той факт, що ви можете тримати всю гру в голові, говорить про те, що ви, швидше за все, відмінний програміст. Я створив багато речей, які я міг би тримати в голові протягом усього проекту. Однак це не сенс системної інженерії. Сенс у тому, щоб створити систему, яка може працювати на щось більше, ніж одна людина може тримати в голові одночасно .

Якщо додати іншого програміста, або двох, чи восьми, це призведе до того, що ваша система розпадеться майже відразу.

  1. Крива навчання об’єктів бога є плоскою (тобто, щоб стати компетентним у них, потрібно дуже багато часу). Кожному додатковому програмісту потрібно буде вивчити все, що ви знаєте, і тримати це в своїй голові. Ви зможете найняти лише програмістів, які кращі за вас, припускаючи, що ви можете заплатити їм достатньо, щоб постраждати за підтримку масивного об'єкта бога.
  2. У вашому описі тестове забезпечення є лише білим полем. Вам потрібно знати кожну деталь об'єкта "бог", а також перевірений модуль, щоб встановити тест, запустити його і визначити, що а) він зробив правильно, і б) він не зробив жодного з 10 000 неправильних речей. Коефіцієнти сильно складені проти вас.
  3. Додавання нової функції вимагає , щоб ви а) пройти через кожен подпроцесс і визначити , є чи функція поширюється на який - небудь код в ньому , і навпаки, б) йде через глобальне стан і розробити доповнення, і с) пройти через кожен тест блок і змінити його щоб перевірити, чи жоден тестований пристрій не вплинув на нову функцію негативно .

Нарешті

  1. Змінні об'єкти бога були прокляттям мого існування програмування, деяким моїм власним заняттям, а також тим, що я потрапив у пастку.
  2. Державна монада не змінюється. Держава росте експоненціально, з усіма наслідками для тестування та операцій, що передбачає. Те, як ми контролюємо стан у сучасних системах, - це шляхом делегування (розподілу обов'язків) та обстеження (обмеження доступу лише до підмножини держави). Підхід "все - це карта" - це явна протилежність контролюючого стану.
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.