Stuart Sierraの講演「Thinking In Data」を見て、私が作っているこのゲームのデザイン原則として、そのアイデアの1つを取り上げました。違いは、彼は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
利点は次のとおりです。
- テストが簡単
- これらの機能を簡単に分離して見ることができます
- この1行を簡単にコメントアウトし、1つのステップを削除することで結果を確認
- 各サブプロセスは、状態にさらに情報を追加できます。サブプロセス1がサブプロセス3に何かを伝える必要がある場合、キー/値を追加するのと同じくらい簡単です。
- 保存するためだけに必要なデータを状態から抽出するための定型はありません。状態全体を渡し、サブプロセスに必要なものを割り当てさせるだけです。
さて、私の状況に戻ります。このレッスンを自分のゲームに適用しました。つまり、私の高レベル関数のほとんどすべてがgameState
オブジェクトを取得して返します。このオブジェクトには、ゲームのすべてのデータが含まれます。EG:badGuysのリスト、メニューのリスト、地上の戦利品など。更新機能の例を次に示します。
update(gameState)
...
gameState = handleUnitCollision(gameState)
...
gameState = handleLoot(gameState)
...
ここで私が尋ねたいのは、関数型プログラミング言語でのみ実用的であるという考えをひっくり返すような嫌悪感を作り出したということです。 JavaScriptは慣用的に機能的ではなく(そのように書くことはできますが)、不変のデータ構造を書くことは本当に困難です。私が懸念することの1つは、これらの各サブプロセスが純粋であると想定していることです。なぜその前提を立てる必要があるのですか?私の関数のいずれかが純粋であることはめったにありません(それによって、それらが頻繁に変更されることを意味しgameState
ます。それ以外の複雑な副作用はありません)。不変のデータがない場合、これらのアイデアはばらばらになりますか?
いつか目を覚ますと、このデザイン全体が偽物であることに気付き、Big Ball Of Mudアンチパターンを実際に実装しているだけだと心配しています。
正直なところ、私はこのコードを何ヶ月も取り組んできましたが、それは素晴らしかったです。彼が主張するすべての利点を手に入れているように感じます。私のコードは非常に簡単に推論できます。しかし、私は一人のチームなので、知識の呪いがあります。
更新
このパターンで6か月以上コーディングしてきました。通常、この時までに自分がやったことを忘れてしまい、そこに「これをきれいに書いたのですか?」遊びに来ます。そうでない場合は、本当に苦労します。これまでのところ、私はまったく苦労していません。
保守性を検証するには、別の目がどのように必要になるかを理解しています。私が言えることは、何よりもまず保守性を気にすることです。私はどこで働いていても、いつもきれいなコードのための最も大きな伝道者です。
このコーディング方法で個人的な経験が既にない人には直接返信したいと思います。そのときは知りませんでしたが、実際には2つの異なるコード記述方法について話していると思います。私がそれをやった方法は、他の人が経験したものよりも構造化されているように見えます。「すべてが地図である」という悪い個人的な経験があるとき、彼らはそれが維持するのがどれほど難しいかについて話します:
- 関数に必要なマップの構造がわからない
- どの関数でも、予期しない方法で入力を変更できます。特定のキーがどのようにマップに入ったのか、なぜ消えたのかを調べるには、コードベース全体を調べる必要があります。
そのような経験のある人にとっては、おそらくコードベースは「すべてがN種類のマップのうちの1つを取る」でした。私は、「すべてが1種類のマップのうちの1つを取る」です。その1タイプの構造を知っていれば、すべての構造を知っています。もちろん、その構造は通常時間とともに成長します。それが理由です...
リファレンス実装(スキーマなど)を探す場所が1つあります。このリファレンス実装は、ゲームが使用するコードであるため、古くなることはありません。
2番目の点については、参照実装の外部でマップにキーを追加/削除するのではなく、既存のものを変更するだけです。自動テストの大規模なスイートもあります。
このアーキテクチャが最終的に自力で崩壊した場合、2つ目の更新を追加します。そうでなければ、すべてがうまくいっていると仮定します:)