Я смотрел выступление Стюарта Сьерры « Мышление в данных » и использовал одну из идей в качестве принципа дизайна в этой игре, которую я делаю. Разница в том, что он работает в Clojure, а я работаю в JavaScript. Я вижу некоторые основные различия между нашими языками в этом:
- Clojure - идиоматически функциональное программирование
- Большая часть государства неизменна
Я взял идею со слайда «Все это карта» (от 11 минут 6 секунд до> 29 минут). Некоторые вещи, которые он говорит:
- Всякий раз, когда вы видите функцию, которая принимает 2-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
Вот преимущества:
- Легко проверить
- Легко смотреть на эти функции в отдельности
- Легко закомментировать одну строку этого и посмотреть, каков будет результат, удалив один шаг
- Каждый подпроцесс может добавить больше информации о состоянии. Если для подпроцесса нужно что-то сообщить подпроцессу 3, это так же просто, как добавить ключ / значение.
- Нет шаблона для извлечения необходимых данных из состояния, чтобы вы могли сохранить их обратно. Просто передайте все состояние и позвольте подпроцессу назначить то, что ему нужно.
Теперь вернемся к моей ситуации: я взял этот урок и применил его к своей игре. То есть почти все мои высокоуровневые функции принимают и возвращают gameState
объект. Этот объект содержит все данные игры. Например: список badGuys, список меню, добыча на земле и т. Д. Вот пример моей функции обновления:
update(gameState)
...
gameState = handleUnitCollision(gameState)
...
gameState = handleLoot(gameState)
...
Здесь я хочу спросить, создал ли я какую-то мерзость, извращающую идею, которая практична только для функционального языка программирования? JavaScript не является идиоматически функциональным (хотя он может быть написан таким образом), и действительно сложно написать неизменяемые структуры данных. Меня беспокоит то, что он предполагает, что каждый из этих подпроцессов чист. Почему необходимо сделать это предположение? Редко когда какие-либо из моих функций чистые (я имею в виду, что они часто модифицируют gameState
. У меня нет никаких других сложных побочных эффектов, кроме этого). Разве эти идеи разваливаются, если у вас нет неизменных данных?
Я волнуюсь, что однажды я проснусь и пойму, что весь этот дизайн - обман, и я действительно только что внедрил антишаблон Big Ball Of Mud .
Честно говоря, я работал над этим кодом несколько месяцев, и это было здорово. Я чувствую, что получаю все преимущества, на которые он претендовал. Мой код очень прост для меня, чтобы рассуждать о. Но я команда из одного человека, поэтому у меня есть проклятие знаний.
Обновить
Я кодировал 6+ месяцев с этим шаблоном. Обычно к этому времени я забываю, что я сделал, и именно здесь "я написал это чисто?" вступает в игру. Если бы не я, я бы действительно боролся. Пока я не борюсь вообще.
Я понимаю, как другой набор глаз был бы необходим для проверки его ремонтопригодности. Все, что я могу сказать, это прежде всего забота о ремонтопригодности. Я всегда самый громкий евангелист за чистый код, где бы я ни работал.
Я хочу прямо ответить на те, которые уже имеют плохой личный опыт с этим способом кодирования. Тогда я этого не знал, но думаю, что мы действительно говорим о двух разных способах написания кода. То, как я это сделал, выглядит более структурированным, чем то, что пережили другие. Когда кто-то имеет плохой личный опыт работы с «Все это карта», они говорят о том, как трудно поддерживать, потому что:
- Вы никогда не знаете структуру карты, которая требуется для функции
- Любая функция может изменить входные данные способами, которые вы никогда не ожидаете. Вы должны просмотреть всю кодовую базу, чтобы узнать, как тот или иной ключ попал в карту или почему он исчез.
Для тех, у кого такой опыт, возможно, кодовая база гласила: «Все требует 1 из N типов карт». Моя, «Все занимает 1 из 1 типа карты». Если вы знаете структуру этого 1 типа, вы знаете структуру всего. Конечно, эта структура обычно растет со временем. Вот почему...
Есть одно место для поиска эталонной реализации (т.е. схема). Эта эталонная реализация является кодом, используемым в игре, поэтому она не может устареть.
Что касается второго пункта, я не добавляю / удаляю ключи к карте вне эталонной реализации, я просто изменяю то, что уже есть. У меня также есть большой набор автоматизированных тестов.
Если эта архитектура рухнет под собственным весом, я добавлю второе обновление. В противном случае предположим, что все идет хорошо :)
источник
Ответы:
Я поддерживал приложение, где «все это карта» раньше. Это ужасная идея. ПОЖАЛУЙСТА, не делай этого!
Когда вы указываете аргументы, которые передаются в функцию, очень легко узнать, какие значения нужны функции. Он избегает передачи посторонних данных в функцию, которая просто отвлекает программиста - каждое переданное значение подразумевает, что оно необходимо, и это заставляет программиста, поддерживающего ваш код, выяснить, зачем нужны данные.
С другой стороны, если вы передадите все как карту, программист, поддерживающий ваше приложение, должен будет полностью понимать вызываемую функцию, чтобы знать, какие значения должна содержать карта. Хуже того, очень заманчиво повторно использовать карту, переданную текущей функции, чтобы передать данные следующим функциям. Это означает, что программист, поддерживающий ваше приложение, должен знать все функции, вызываемые текущей функцией, чтобы понять, что делает текущая функция. Это прямо противоположно цели написания функций - абстрагирование проблем, чтобы вам не приходилось думать о них! Теперь представьте 5 глубоких и 5 телефонных звонков каждый. Это чертовски много, чтобы иметь в виду, и чертовски много ошибок, чтобы сделать.
«все является картой» также, по-видимому, приводит к использованию карты в качестве возвращаемого значения. Я видел это. И, опять же, это боль. Вызываемые функции никогда не должны перезаписывать возвращаемое значение друг друга - если вы не знаете функциональности всего и не знаете, что значение X входной карты необходимо заменить для следующего вызова функции. И текущей функции необходимо изменить карту, чтобы она возвращала свое значение, которое иногда должно перезаписывать предыдущее значение, а иногда нет.
редактировать - пример
Вот пример того, где это было проблематично. Это было веб-приложение. Пользовательский ввод был принят со слоя пользовательского интерфейса и помещен в карту. Затем были вызваны функции для обработки запроса. Первый набор функций будет проверять ошибочный ввод. Если произошла ошибка, сообщение об ошибке будет помещено в карту. Вызывающая функция проверит карту для этой записи и запишет значение в пользовательском интерфейсе, если оно существует.
Следующий набор функций запустит бизнес-логику. Каждая функция берет карту, удаляет некоторые данные, изменяет некоторые данные, оперирует данными на карте и помещает результат в карту и т. Д. Последующие функции ожидают результатов от предыдущих функций на карте. Чтобы исправить ошибку в последующей функции, вам нужно было исследовать все предыдущие функции, а также вызывающую функцию, чтобы определить везде, где могло быть установлено ожидаемое значение.
Следующие функции будут получать данные из базы данных. Или, скорее, они передадут карту в слой доступа к данным. DAL проверит, содержит ли карта определенные значения, чтобы контролировать выполнение запроса. Если бы 'justcount' был ключом, то запрос был бы 'count select foo from bar'. Любая из ранее вызванных функций могла иметь ту, которая добавила 'justcount' на карту. Результаты запроса будут добавлены на ту же карту.
Результаты выдаются вызывающей стороне (бизнес-логике), которая проверяет карту, что делать. Отчасти это может быть связано с тем, что было добавлено на карту с помощью исходной бизнес-логики. Некоторые приходят из данных из базы данных. Единственный способ узнать, откуда он взялся, - найти код, который его добавил. И другое место, которое также может добавить его.
Код фактически представлял собой монолитный беспорядок, который вы должны были полностью понять, чтобы знать, откуда взялась единственная запись на карте.
источник
gameState
не зная ничего о том, что произошло до или после нее. Он просто реагирует на данные, которые ему дают. Как вы попали в ситуацию, когда функции будут наступать друг на друга? Можете привести пример?Лично я бы не рекомендовал эту модель в любой парадигме. Это облегчает первоначальную запись за счет усложнения рассуждений о последующих.
Например, попробуйте ответить на следующие вопросы о каждой функции подпроцесса:
state
это требует?С этим шаблоном вы не сможете ответить на эти вопросы, не прочитав всю функцию.
В объектно-ориентированном языке шаблон имеет еще меньше смысла, потому что отслеживание состояния - это то, что делают объекты.
источник
То, что вы, похоже, делаете, это, по сути, ручная государственная монада; то, что я хотел бы сделать, это создать (упрощенный) комбинатор связывания и повторно выразить связи между вашими логическими шагами, используя это:
Вы можете даже использовать его
stateBind
для создания различных подпроцессов из подпроцессов и продолжения вниз по дереву комбинаторов связывания, чтобы правильно структурировать вычисления.Для объяснения полной, не упрощенной государственной монады и отличного введения в монады в целом в JavaScript, см. Этот пост в блоге .
источник
stateBind
мною комбинатор является очень простым примером этого.Таким образом, кажется, что существует много дискуссий между эффективностью этого подхода в Clojure. Я думаю, что было бы полезно взглянуть на философию Рича Хики о том, почему он создал Clojure для поддержки абстракций данных следующим образом :
Фогус повторяет эти моменты в своей книге « Функциональный Javascript» :
источник
Адвокат дьявола
Я думаю, что этот вопрос заслуживает защиты дьявола (но, конечно, я пристрастен). Я думаю, что @KarlBielefeldt делает очень хорошие замечания, и я хотел бы обратиться к ним. Сначала я хочу сказать, что его очки великолепны.
Поскольку он упомянул, что это не очень хорошая модель даже в функциональном программировании, я буду рассматривать JavaScript и / или Clojure в своих ответах. Одним из чрезвычайно важных сходств между этими двумя языками является то, что они динамически типизированы. Я был бы более согласен с его идеями, если бы реализовывал это на языке статической типизации, таком как Java или Haskell. Но я собираюсь рассмотреть альтернативу шаблону «Все это карта» как традиционный дизайн ООП в JavaScript, а не в статически типизированном языке (надеюсь, я не буду настраивать аргумент бессмысленного действия, выполняя это, пожалуйста, дайте мне знать).
В динамически типизированном языке, как бы вы обычно отвечали на эти вопросы? Первый параметр функции может быть назван
foo
, но что это? Массив? Объект? Объект из массивов объектов? Как ты узнал? Единственный способ, которым я знаю, этоЯ не думаю, что паттерн «Все это карта» имеет здесь какое-то значение. Это все еще единственный способ ответить на эти вопросы.
Также имейте в виду, что в JavaScript и большинстве императивных языков программирования любой
function
может требовать, изменять и игнорировать любое состояние, к которому он имеет доступ, и подпись не имеет значения: функция / метод может что-то делать с глобальным состоянием или с единичным. Подписи часто врут.Я не пытаюсь установить ложную дихотомию между «Все это карта» и плохо разработанным ОО-кодом. Я просто пытаюсь указать, что наличие подписей, которые принимают менее / более точные / грубые параметры, не гарантирует, что вы знаете, как изолировать, настроить и вызвать функцию.
Но, если вы позволите мне использовать эту ложную дихотомию: по сравнению с написанием JavaScript традиционным способом ООП, «Все это карта» кажется лучше. Традиционным способом ООП функция может требовать, изменять или игнорировать состояние, которое вы передаете, или состояние, которое вы не передаете. С этим шаблоном «Все это карта» вам требуется только изменить, или игнорировать состояние, которое вы передаете. в.
В моем коде да. Смотрите мой второй комментарий к ответу @ 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
назад. Нет необходимости проверять, должен ли произвольный плохой парень собирать добычу, которая уже находится на земле. Поэтому я считаю, что порядок этих операций совершенно произвольный.С помощью этого шаблона естественно написать разделенные шаги, но трудно заметить ваши связанные шаги в традиционном ООП. Если бы я писал традиционный ООП, естественный, наивный способ мышления - сделать
dead guys drop loot
возвращениеLoot
объектом, который я должен передать вgood guys pick up loot
. Я не смог бы переупорядочить эти операции, так как первая возвращает ввод второй.Объекты имеют состояние, и идиоматично изменять его состояние, заставляя его историю просто исчезать ... если только вы не пишете код вручную, чтобы отслеживать его. Как отслеживается состояние «что они делают»?
Правильно, как я уже сказал: «Редко, когда какая-либо из моих функций чиста». Они всегда работают только со своими параметрами, но они изменяют свои параметры. Это компромисс, который я чувствовал, когда должен был применить этот шаблон к JavaScript.
источник
parent
? Естьrepetitions
и массив чисел или строк или это не имеет значения? Или, может быть, повторения - это просто число, представляющее количество представлений, которые я хочу? Есть много apis там, которые просто берут объект параметров . Мир лучше, если вы правильно называете вещи, но это не гарантирует, что вы будете знать, как использовать API, без вопросов.Я обнаружил, что мой код имеет такую структуру:
Я не собирался создавать это различие, но именно так оно и бывает в моем коде. Я не думаю, что использование одного стиля обязательно отрицает другой.
Чистые функции просты в модульном тестировании. Большие карты с картами попадают в область тестирования «интеграции», так как в них больше движущихся частей.
В javascript одна вещь, которая очень помогает, - это использовать что-то вроде библиотеки совпадений Meteor для проверки параметров. Это делает очень ясным, что ожидает функция и может обрабатывать карты довольно чисто.
Например,
Смотрите http://docs.meteor.com/#match для получения дополнительной информации.
:: ОБНОВИТЬ ::
Видеозапись Стюарта Сьерры из Clojure / West "Clojure in the Large" также затрагивает эту тему. Как и OP, он контролирует побочные эффекты как часть карты, поэтому тестирование становится намного проще. У него также есть пост в блоге с изложением его текущего рабочего процесса Clojure, который кажется актуальным.
источник
Главный аргумент, который я могу придумать против этой практики, заключается в том, что очень сложно определить, какие данные действительно нужны функции.
Это означает, что будущие программисты в кодовой базе должны будут знать, как вызываемая функция работает внутренне - и любые вызовы вложенных функций - чтобы вызывать ее.
Чем больше я думаю об этом, тем больше ваш объект gameState пахнет как глобальный. Если это то, как это используется, зачем раздавать это?
источник
gameState = f(gameState)
наf()
, это будет намного сложнее проверитьf
.f()
может возвращать разные вещи каждый раз, когда я это называю. Но легко заставитьf(gameState)
возвращать одно и то же каждый раз, когда ему дают одинаковые входные данные.Есть более подходящее название для того, что вы делаете, чем Большой шарик грязи . То, что вы делаете, называется шаблоном объекта Бога . На первый взгляд, это не так, но в Javascript разница между
а также
Является ли это хорошей идеей, вероятно, зависит от обстоятельств. Но это, безусловно, соответствует пути Clojure. Одна из целей Clojure - удалить то, что Рич Хики называет «случайной сложностью». Несколько взаимодействующих объектов, безусловно, сложнее, чем один объект. Если вы разделите функциональность на несколько объектов, вам внезапно придется задуматься о связи, координации и распределении обязанностей. Это осложнения, которые связаны только с вашей первоначальной целью написания программы. Вы должны увидеть выступление Рича Хикки « Простое - легко» . Я думаю, что это очень хорошая идея.
источник
Я только что столкнулся с этой темой ранее сегодня, когда играл с новым проектом. Я работаю в Clojure, чтобы сделать игру в покер. Я представлял номиналы и масти как ключевые слова и решил представить карту как карту
С таким же успехом я мог бы составить их списки или векторы двух ключевых элементов. Я не знаю, влияет ли это на память / производительность, поэтому сейчас я просто собираюсь использовать карты.
Однако на случай, если позже я передумаю, я решил, что большинство частей моей программы должно проходить через «интерфейс» для доступа к частям карты, чтобы детали реализации контролировались и скрывались. У меня есть функции
что остальная часть программы использует. Карты передаются в функции в виде карт, но функции используют согласованный интерфейс для доступа к картам и, следовательно, не должны быть в состоянии испортить.
В моей программе карта, вероятно, будет только двухзначной картой. В этом вопросе все игровое состояние передается как карта. Состояние игры будет намного сложнее, чем у одной карты, но я не думаю, что есть какая-то ошибка в использовании карты. В объектно-императивном языке я также мог бы иметь один большой объект GameState и вызывать его методы, и у меня была бы та же проблема:
Теперь это объектно-ориентированный. Что-то особенно не так с этим? Я так не думаю, вы просто делегируете работу функциям, которые знают, как обрабатывать объект State. И независимо от того, работаете ли вы с картами или объектами, вам следует опасаться, когда нужно разделить его на более мелкие части. Поэтому я говорю, что использование карт - это прекрасно, если вы будете использовать ту же заботу, что и объекты.
источник
Из того, что (мало) я видел, использование карт или других вложенных структур для создания единого глобального неизменяемого объекта состояния, подобного этому, довольно распространено в функциональных языках, по крайней мере, в чистых, особенно при использовании State Monad в качестве @ Ptharien'sFlame mentioend .
Два препятствия для эффективного использования этого, о которых я видел / читал (и другие ответы здесь упоминались):
Есть несколько различных методов / общих шаблонов, которые могут помочь облегчить эти проблемы:
Первый - это застежки-молнии : они позволяют проходить и изменять состояние глубоко внутри неизменной вложенной иерархии.
Другой - линзы : они позволяют вам сфокусироваться на структуре в определенном месте и прочитать / изменить значение там. Вы можете комбинировать разные линзы вместе, чтобы сосредоточиться на разных вещах, что-то вроде цепочки настраиваемых свойств в ООП (где вы можете заменить переменные для фактических имен свойств!)
Prismatic недавно сделал сообщение в блоге об использовании такого рода техники, среди прочего, в JavaScript / ClojureScript, которую вы должны проверить. Они используют курсоры (которые они сравнивают с застежками-молниями) для состояния окна для функций:
IIRC, они также касаются неизменности в JavaScript в этом посте.
источник
assoc-in
др.). Думаю, у меня только что был Хаскелл в мозгу. Интересно, кто-нибудь сделал это с портом JavaScript? Люди, вероятно, не поднимали это, потому что (как я) они не смотрели разговор :)Хорошая ли это идея или нет, будет зависеть от того, что вы делаете с состоянием внутри этих подпроцессов. Если я правильно понимаю пример Clojure, возвращаемые словари состояний не являются теми же словарями состояний, которые передаются. Это копии, возможно, с дополнениями и модификациями, которые (я предполагаю) Clojure способен эффективно создавать, потому что от этого зависит функциональная природа языка. Исходные словари состояния для каждой функции никак не изменяются.
Если я правильно понять, вы являетесь изменения состояния объектов вы проходите в свой яваскрипте функцию , а не возвращать копию, а значит , вы делаете что - то очень, очень, отличное от того , что делает этот код Clojure. Как указал Майк Партридж, это по сути всего лишь глобальный объект, который вы явно передаете и возвращаете из функций без реальной причины. На данный момент, я думаю, это просто заставляет вас думать, что вы делаете то, чего на самом деле не делаете.
Если вы на самом деле явно делаете копии состояния, модифицируете его, а затем возвращаете эту измененную копию, продолжайте. Я не уверен, что это обязательно лучший способ выполнить то, что вы пытаетесь сделать в Javascript, но он, вероятно, «близок» к тому, что делает этот пример Clojure.
источник
update()
функции. Я переместил один наверх и один на дно. Все мои тесты все еще прошли, и когда я играл в свою игру, я не заметил никаких побочных эффектов. Я чувствую, что мои функции так же сложны, как и пример Clojure. Мы оба выбрасываем наши старые данные после каждого шага.(-> 10 (- 5) (/ 2))
возвращает 2.5.(-> 10 (/ 2) (- 5))
возвращает 0.Если у вас есть глобальный объект состояния, иногда называемый «божественным объектом», который передается каждому процессу, вы в конечном итоге смешиваете ряд факторов, каждый из которых увеличивает связь, одновременно снижая сплоченность. Все эти факторы негативно влияют на долговременную ремонтопригодность.
Tramp Coupling Это происходит из-за передачи данных через различные методы, которые не нужны почти для всех данных, чтобы доставить их туда, где они действительно могут иметь дело. Этот тип связи похож на использование глобальных данных, но может быть более ограниченным. Бродячая связь является противоположностью «необходимости знать», которая используется для локализации эффектов и сдерживания ущерба, который один ошибочный фрагмент кода может нанести на всю систему.
Навигация данных Каждый подпроцесс в вашем примере должен знать, как получить именно те данные, которые ему нужны, и он должен уметь обрабатывать их и, возможно, создавать новый глобальный объект состояния. Это логическое следствие сцепления бродяги; весь контекст элемента данных необходим для работы с датумом. Опять же, нелокальные знания это плохо.
Если вы передавали «молнию», «линзу» или «курсор», как описано в посте @paul, это одно. Вы будете содержать доступ и позволять молнии и т. Д. Контролировать чтение и запись данных.
Нарушение единой ответственности Утверждать, что каждое из «subprocess-one», «subprocess-two» и «subprocess-three» несет только одну ответственность, а именно создавать новый объект глобального состояния с правильными значениями в нем, является вопиющим редукционизмом. Это все биты в конце концов, не так ли?
Моя точка зрения заключается в том, что все основные компоненты вашей игры имеют те же обязанности, что и ваша игра, побеждающая цель делегирования и факторинга.
Влияние систем
Основное влияние вашего дизайна - низкая ремонтопригодность. Тот факт, что вы можете держать всю игру в голове, говорит о том, что вы, скорее всего, отличный программист. Я разработал множество вещей, которые я мог бы сохранить в голове для всего проекта. Это не главное для системной инженерии. Суть в том, чтобы создать систему, которая могла бы работать для чего-то большего, чем один человек может держать в голове одновременно .
Добавление другого программиста, или двух, или восьми, приведет к тому, что ваша система развалится почти сразу.
в заключение
источник