web-dev-qa-db-ja.com

「すべては地図です」、私はこれを正しく行っていますか?

私はスチュアートシエラの講演「 Thinking In Data 」を見て、そのアイデアの1つをこのゲームのデザイン原則として取り入れました。違いは、彼は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. この1行をコメントアウトして、1つのステップを削除することで結果を確認するのは簡単です
    4. 各サブプロセスは、状態にさらに情報を追加できます。サブプロセス1がサブプロセス3に何かを通信する必要がある場合、キー/値を追加するのと同じくらい簡単です。
    5. 状態から必要なデータを抽出して保存するためのボイラープレートはありません。状態全体を渡して、サブプロセスに必要なものを割り当てさせるだけです。

さて、私の状況に戻ります。このレッスンを受講し、ゲームに適用しました。つまり、ほとんどすべての高水準関数はgameStateオブジェクトを受け取って返します。このオブジェクトには、ゲームのすべてのデータが含まれています。 EG:badGuysのリスト、メニューのリスト、地上の戦利品など。ここに私の更新関数の例を示します。

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

私がここで尋ねているのは、関数型プログラミング言語でのみ実用的である考えを覆すような嫌悪感を生み出したのですか?JavaScriptはありません ' t 慣用的に機能的(そのように書くこともできます)で、不変のデータ構造を書き込むことは本当に困難です。私が気にすることの1つは、彼が仮定するこれらのサブプロセスのそれぞれが純粋であることです。なぜその仮定を行う必要があるのですか?私の関数のいずれかが純粋であることはまれです(つまり、gameStateを頻繁に変更します。それ以外に複雑な副作用はありません)。不変のデータがない場合、これらのアイデアはばらばらになりますか?

いつの日か目が覚めて、このデザイン全体が偽物であることに気づき、本当に Big Ball Of Mudアンチパターン を実装しているのではないかと心配しています。


正直なところ、私はこのコードに何ヶ月も取り組んできました。私は彼が主張しているすべての利点を手に入れているような気がします。私のコードは非常に簡単です私にとって推論する。しかし、私は一人のチームなので、知識の呪いを持っています。

更新

私はこのパターンで6か月以上コーディングしてきました。通常、この時までに私は自分がしたことを忘れており、それが「私がこれをきれいに書いたのですか?」場に出る。持っていない場合は、本当に苦労します。これまでのところ、私はまったく苦労していません。

保守性を検証するために別の目が必要になることを理解しています。私が言えることは、何よりもまず保守性を気にすることです。私は、どこで仕事をしても、クリーンなコードを書くのに常に最大のエバンジェリストです。

このコーディング方法ですでに個人的な経験が悪い人に直接返信したいと思います。そのときは知りませんでしたが、コードを書く方法は2つあります。私が行った方法は、他の人が経験した方法よりも構造化されているようです。誰かが「すべてが地図である」との悪い個人的な経験を持っているとき、彼らはそれが維持することがどれほど難しいかについて話します:

  1. 関数が必要とするマップの構造がわからない
  2. どの関数でも、予期しない方法で入力を変更できます。特定のキーがどのようにマップに組み込まれたのか、なぜキーが消えたのかを知るために、コードベース全体を調べる必要があります。

そのような経験を持つ人たちにとって、コードベースは「すべてがN種類のマップの1つをとる」でした。私のものは、「すべてが1種類のマップの1つを取る」です。その1タイプの構造を知っていれば、すべての構造を知っています。もちろん、その構造は通常、時間とともに成長します。それが理由です...

リファレンス実装(つまり、スキーマ)を探す場所が1つあります。このリファレンス実装はゲームが使用するコードであるため、古くなることはありません。

2番目のポイントについては、リファレンス実装の外でマップにキーを追加/削除せず、すでにそこにあるものを変更するだけです。自動テストの大規模なスイートもあります。

このアーキテクチャが最終的に自重で崩壊する場合は、2つ目の更新を追加します。それ以外の場合は、すべてが順調に進んでいると想定します。

69
Daniel Kaplan

以前は「すべてが地図である」アプリケーションをサポートしていました。それはひどい考えです。しないでください!

関数に渡される引数を指定すると、関数が必要とする値を非常に簡単に知ることができます。プログラマーの注意をそらすだけの関数に無関係なデータを渡すことを避けます。渡されたすべての値はそれが必要であることを意味し、コードをサポートするプログラマーはデータが必要な理由を理解する必要があります。

一方、すべてをマップとして渡す場合、アプリをサポートするプログラマは、呼び出される関数をあらゆる方法で完全に理解して、マップに含める必要のある値を知る必要があります。さらに悪いことに、次の関数にデータを渡すために、現在の関数に渡されたマップを再利用するのは非常に魅力的です。つまり、アプリをサポートするプログラマーは、現在の関数の機能を理解するために、現在の関数によって呼び出されるすべての関数を知る必要があります。これは、関数を書く目的とは正反対です。問題を抽象化して、それらについて考える必要がないようにします。次に、深さ5の通話と幅5の通話をそれぞれ想像してください。それはあなたの心に留めておかなければならない多くの地獄であり、犯すべき多くの間違いの地獄です。

「すべてがマップです」も戻り値としてマップを使用することにつながるようです。見たことある。そして、再び、それは苦痛です。呼び出された関数は、互いの戻り値を決して上書きしないでください。すべての機能を理解していて、次の関数呼び出しのために入力マップ値Xを置き換える必要があることがわかっている場合を除きます。そして、現在の関数は、その値を返すようにマップを変更する必要があります。これは、前の値を上書きする場合と上書きしない場合があります。

編集-例

これが問題のあった場所の例です。これはWebアプリケーションでした。ユーザー入力はUIレイヤーから受け入れられ、マップに配置されました。次に、要求を処理するための関数が呼び出されました。最初の関数セットは、誤った入力をチェックします。エラーが発生した場合、エラーメッセージがマップに表示されます。呼び出し元の関数は、このエントリのマップをチェックし、存在する場合は、UIに値を書き込みます。

次の関数セットはビジネスロジックを開始します。各関数は、マップを取得し、一部のデータを削除し、一部のデータを変更し、マップ内のデータを操作して結果をマップに配置します。以降の関数は、マップ内の以前の関数からの結果を期待します。後続の関数のバグを修正するには、以前のすべての関数と呼び出し元を調査して、期待値が設定されている可能性があるすべての場所を特定する必要がありました。

次の関数はデータベースからデータを引き出します。または、マップをデータアクセスレイヤーに渡します。 DALは、クエリがどのように実行されるかを制御するために、マップに特定の値が含まれているかどうかをチェックします。 「justcount」がキーの場合、クエリは「count select foo from bar」になります。以前に呼び出された関数のいずれかが、マップに「justcount」を追加したものである可能性があります。クエリ結果は同じマップに追加されます。

結果は、何をすべきかについてマップをチェックする呼び出し元(ビジネスロジック)に送られます。これの一部は、最初のビジネスロジックによってマップに追加されたものに由来します。一部はデータベースのデータから取得されます。それがどこから来たかを知る唯一の方法は、それを追加したコードを見つけることでした。そして、それを追加できる他の場所。

コードは事実上、モノリシックな混乱であり、マップ内の単一のエントリがどこから来たのかを知るために全体を理解する必要がありました。

42
atk

個人的には、どちらのパラダイムでもそのパターンはお勧めしません。それは、後で推論することをより難しくすることを犠牲にして、最初に書くことをより簡単にします。

たとえば、各サブプロセス関数に関する次の質問に答えてみてください。

  • stateのどのフィールドが必要ですか?
  • どのフィールドが変更されますか?
  • どのフィールドが変更されていませんか?
  • 関数の順序を安全に再配置できますか?

このパターンでは、関数全体を読まないとこれらの質問に答えることができません。

オブジェクト指向言語では、状態を追跡することはオブジェクトが行うことなので、このパターンはあまり意味がありません。

28
Karl Bielefeldt

あなたがしているように見えるのは、事実上、手動のStateモナドです。私がやろうとしていることは、(単純化された)バインドコンビネーターを構築し、それを使用して論理ステップ間の接続を再表現することです。

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を使用してサブサブプロセスからさまざまなサブプロセスを構築し、バインディングコンビネーターのツリーを下に進んで計算を適切に構造化することもできます。

完全で単純化されたStateモナドの説明、およびJavaScriptでのモナド全般の優れた紹介については、 このブログ投稿 を参照してください。

12

したがって、Clojureでのこのアプローチの有効性には多くの議論があるようです。私はリッチ・ヒッキーの哲学を見るのに役立つと思います このようにしてデータの抽象化をサポートするためにClojureを作成した理由

Fogus:では、偶発的な複雑さが軽減されたら、Clojureは当面の問題をどのように解決できるでしょうか?たとえば、理想的なオブジェクト指向パラダイムは再利用を促進することを目的としていますが、Clojureは古典的なオブジェクト指向ではありません。再利用のためにコードをどのように構造化できるでしょうか。

ヒッキー:私はOOおよび再利用について議論しますが、確かに、物事を再利用できることは問題を抱えています車を建てるのではなく車輪を再発明しないので、より簡単です。また、JVMにあるClojureは、多くの車輪(ライブラリ)を利用できるようにします。ライブラリを再利用可能にするには、何が必要ですか。 、そしてクライアントコードにほとんど要求をしません。オブジェクト指向から外れるものはなく、すべてのJavaライブラリがこの基準を満たしているわけではありません。

アルゴリズムレベルにドロップすると、OOは再利用を大幅に妨げる可能性があります。特に、単純な情報データを表すためのオブジェクトの使用は、ピースごとの生成においてほとんど犯罪です情報のマイクロ言語、つまりクラスメソッド。リレーショナル代数のようなはるかに強力で宣言的で汎用的なメソッド。情報の一部を保持する独自のインターフェイスを持つクラスを発明することは、すべての短編小説を書くための新しい言語を発明するようなものです。これは再利用に反するものであり、典型的なOOアプリケーションではコードが爆発的に増加します。Clojureはこれを避け、代わりに単純な連想モデルを情報に推奨しています。これを使用すると、情報タイプ全体で再利用できるアルゴリズム。

この連想モデルは、Clojureで提供されるいくつかの抽象化の1つにすぎません。これらは、再利用へのアプローチの真の土台である抽象化の関数です。オープンで大きな関数のセットが、オープンで小さな拡張可能な抽象のセットを操作することが、アルゴリズムによる再利用とライブラリの相互運用性の鍵となります。 Clojure関数の大部分はこれらの抽象化の観点から定義されており、ライブラリの作成者はそれらの観点からも入出力形式を設計しており、独立して開発されたライブラリ間の途方もない相互運用性を実現しています。これは、DOMやその他のOOに表示されるものとはまったく対照的です。もちろん、同様の抽象化をOOで、たとえばJava.utilコレクションなどのインターフェースを使用して行うことができますが、Java.ioの場合と同じくらい簡単にはできません。

Fogusは彼の本Functional Javascriptでこれらのポイントを繰り返し述べています。

この本全体を通して、最小限のデータ型を使用して、セットからツリー、テーブルまでの抽象化を表現するアプローチをとります。ただし、JavaScriptでは、そのオブジェクトタイプは非常に強力ですが、それらを操作するために提供されているツールは完全には機能しません。代わりに、JavaScriptオブジェクトに関連付けられているより大きな使用パターンは、ポリモーフィックディスパッチの目的でメソッドをアタッチすることです。ありがたいことに、名前のない(コンストラクター関数を介して構築されていない)JavaScriptオブジェクトを、単なる連想データストアとして表示することもできます。

BookオブジェクトまたはEmployeeタイプのインスタンスで実行できる唯一の操作がsetTitleまたはgetSSNである場合、データを情報ごとのマイクロ言語にロックしました(Hickey 2011)。データをモデル化するためのより柔軟なアプローチは、連想データ手法です。 JavaScriptオブジェクトは、プロトタイプ機械を除いても、連想データモデリングに理想的な手段であり、名前付きの値を構造化して、より高レベルのデータモデルを形成し、均一な方法でアクセスできます。

JavaScriptオブジェクト内でJavaScriptオブジェクトを操作およびアクセスするためのツールはJavaScript自体の中で疎ですが、ありがたいことにUnderscoreは一連の便利な操作を提供します。把握する最も簡単な関数には、_。keys、_。values、および_.pluckがあります。 _.keysと_.valuesは、機能に応じて名前が付けられます。つまり、オブジェクトを取得して、そのキーまたは値の配列を返します...

11
pooya72

悪魔の擁護者

この質問は悪魔の支持者に値すると思います(もちろん私は偏見があります)。 @KarlBielefeldtは非常に良い点を作っていると思います。私はそれらに取り組みたいと思います。まず、彼のポイントは素晴らしいと思います。

これは関数型プログラミングでも良いパターンではないと彼が言ったので、返信でJavaScriptやClojureを検討します。これら2つの言語の非常に重要な類似点の1つは、動的に型付けされることです。 JavaまたはHaskellのような静的に型付けされた言語でこれを実装した場合、彼の点に同意します。しかし、「Everything is a Map "パターンは従来のOOPデザインJavaScriptの場合であり、静的に型付けされた言語ではありません(これにより、ストローマン引数を設定しないようにしてください。お知らせ下さい)。

たとえば、各サブプロセス関数に関する次の質問に答えてみてください。

  • どの状態のフィールドが必要ですか?

  • どのフィールドが変更されますか?

  • どのフィールドが変更されていませんか?

動的に型付けされた言語では、通常これらの質問にどのように答えますか?関数の最初のパラメーターはfooという名前になりますが、それは何ですか?配列?オブジェクト?オブジェクトの配列のオブジェクト?どのようにして見つけますか?私が知る唯一の方法は

  1. ドキュメントを読む
  2. 関数本体を見る
  3. テストを見て
  4. プログラムを推測して実行し、機能するかどうかを確認します。

「Everything is a Map」パターンがここで違いを生むとは思わない。これらは、私がこれらの質問に答えるために知っている唯一の方法です。

また、JavaScriptやほとんどの命令型プログラミング言語では、functionはアクセスできる状態を必要とし、変更し、無視することができ、シグネチャには違いがないことに注意してください。関数/メソッドは、グローバル状態またはシングルトン。署名しばしば嘘。

「すべてが地図である」と不完全に設計された OOコード)の間に誤った二分法を設定しようとしているのではありません。シグネチャが少ない/細かい/粗いシグネチャを持つシグネチャは、関数の分離、セットアップ、および呼び出しの方法を知っているとは限りません。

しかし、私がその誤った二分法を使用することを許可する場合:従来のOOPの方法でJavaScriptを書くのと比較して、「すべてがマップである」と思われます。従来のOOP方法、関数は、渡した状態を必要とするか、変更するか、無視する可能性がありますor渡さない状態。この「すべてがマップ」パターンでは、必要なのは、渡す状態を変更または無視します。

  • 関数の順序を安全に再配置できますか?

私のコードでは、はい。 @Evicatosの回答に対する2番目のコメントを参照してください。多分これは私がゲームを作っているからだけなのかもしれません。 1秒間に60倍の速度で更新されるゲームでは、dead guys drop lootgood guys pick up lootであるかどうか、またはその逆は問題ではありません。各関数は、実行される順序に関係なく、本来の機能を実行します。注文を交換すると、同じデータが別のupdate呼び出しでそれらに供給されます。あなたがgood guys pick up loot、次にdead guys drop lootを持っている場合、善意の者は次のupdateで略奪品を手に入れますが、それは大した問題ではありません。人間は違いに気づくことができません。

少なくともこれは私の一般的な経験でした。私はこれを公に認めることは本当に傷つきやすいと感じています。これを大丈夫だと考えるのはveryvery悪いことです。ここでひどい間違いを犯したかどうか教えてください。しかし、私が持っている場合、関数を再配置するのは非常に簡単なので、順序はdead guys drop loot、次にgood guys pick up lootになります。この段落を書くのにかかった時間よりも時間がかかりません:P

たぶん、あなたは「死んだ男たちすべき最初に戦利品を落とす。あなたのコードがその命令を強制した方がいいだろう」と思うかもしれません。しかし、戦利品を拾う前に敵が戦利品を落とさなければならないのはなぜですか?私にはそれは意味がありません。戦利品は100 updates前に落とされたのかもしれません。任意の悪者がすでに地上にいる戦利品を拾わなければならないかどうかを確認する必要はありません。そのため、これらの操作の順序は完全に任意であると思います。

このパターンを使用して分離ステップを作成するのは自然ですが、従来のOOPで結合ステップを認識するのは困難です。従来のOOPを作成していた場合、自然で素朴な考え方は、dead guys drop lootgood guys pick up lootに渡す必要があるLootオブジェクトを返すようにすることです。最初の操作は2番目の入力を返すため、これらの操作を並べ替えることはできません。

オブジェクト指向言語では、状態の追跡がオブジェクトの動作であるため、パターンの意味はさらに小さくなります。

オブジェクトには状態があり、状態を変化させてその履歴を消すのは慣用的です...追跡するためのコードを手動で記述しない限り。どのようにして「状態」を追跡していますか?

また、不変性の利点は、不変オブジェクトが大きくなるほど低くなります。

そうです、私が言ったように、「私の機能のいずれかが純粋であることはまれです」。 常にのみはパラメータを操作しますが、パラメータを変更します。これは、このパターンをJavaScriptに適用するときに私がしなければならないと感じた妥協です。

9
Daniel Kaplan

私のコードは次のように構造化される傾向があることがわかりました:

  • マップをとる関数は大きくなる傾向があり、副作用があります。
  • 引数を取る関数は、より小さく、純粋である傾向があります。

私はこの区別を作成しようとはしませんでしたが、それが多くの場合、それが私のコードで終わる方法です。 1つのスタイルを使用すると、必ずしも他のスタイルが無効になるとは思いません。

純粋な関数は単体テストが簡単です。より大きな可動部品が含まれる傾向があるため、マップを備えた大きなものほど「統合」テスト領域に入ります。

JavaScriptでは、MeteorのMatchライブラリのようなものを使用してパラメータの検証を実行することが、大きな助けになります。これにより、関数が何を期待しているかが非常に明確になり、マップを非常にきれいに処理できます。

例えば、

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" もこの主題に触れています。 OPと同様に、彼はマップの一部として副作用を制御するため、テストがはるかに簡単になります。彼は ブログ投稿 にも関連があると思われる現在のClojureワークフローの概要を示しています。

8
alanning

この実践に対して私が考えることができる主な議論は、関数が実際に必要とするデータを知るのは非常に難しいということです。

つまり、コードベースの将来のプログラマーは、呼び出される関数が内部でどのように機能するか、およびネストされた関数呼び出しをどのように呼び出すかを知る必要があります。

考えるほど、あなたのgameStateオブジェクトはグローバルのような香りがします。それがそのように使用されている場合、なぜそれを渡すのですか?

5
Mike Partridge

泥の大きなボール よりも、あなたがしていることにふさわしい名前があります。あなたがしていることは 神オブジェクト パターンと呼ばれています。一見するとそのようには見えませんが、Javascriptでは違いはほとんどありません

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

そして

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

それが良いアイデアかどうかは、おそらく状況によって異なります。しかし、それは確かにClojureの方法と一致しています。 Clojureの目的の1つは、 Rich Hickey が「偶発的な複雑さ」と呼ぶものを削除することです。複数の通信オブジェクトは、単一のオブジェクトよりも確かに複雑です。機能を複数のオブジェクトに分割すると、突然、コミュニケーションと調整、責任の分割について心配する必要があります。これらは、プログラムを作成するという本来の目的に付随するだけの複雑な問題です。リッチヒッキーの講演 Simple made easy が表示されます。これは非常に良い考えだと思います。

3
user7610

私が見てきたことから、このような単一のグローバルな不変状態オブジェクトを作成するためにマップまたは他のネストされた構造を使用することは、特に状態モナドを @ Ptharien'sFlame mentioend

これを効果的に使用するための2つの障害は、私が見た/読んだ(そしてここで述べた他の答えも)です:

  • (不変)状態で(深く)ネストされた値を変更する
  • 状態の大部分を、それを必要としない関数から隠して、それらに取り組む/変異するために必要な少しだけ与える

これらの問題を緩和するのに役立ついくつかの異なるテクニック/一般的なパターンがあります:

1つ目は Zippers です。これらを使用すると、不変のネストされた階層内の深い状態に移動して状態を変更できます。

もう1つは Lenses です。これにより、構造にfocusして特定の場所に移動し、そこで値を読み取り/変更できます。 OOP(実際のプロパティ名を変数に置き換えることができる!)の調整可能なプロパティチェーンのように、さまざまなレンズを組み合わせてさまざまなことに焦点を当てることができます。

Prismaticは最近、JavaScript/ClojureScriptでこの種のテクニックを使用することについて ブログ投稿 を行いました。関数のウィンドウ状態に Cursors (ジッパーと比較)を使用します。

Omは、カーソルを使用してカプセル化とモジュール性を復元します。カーソルは、更新可能なウィンドウをアプリケーション状態の特定の部分(ジッパーのように)に提供し、コンポーネントがグローバル状態の関連部分のみへの参照を取得して、コンテキストフリーの方法でそれらを更新できるようにします。

IIRC、彼らはその投稿でJavaScriptの不変性についても触れています。

2
paul

新しいプロジェクトで遊んでいる間、私はちょうど今日このトピックに少し前に直面しました。私はClojureでポーカーゲームを作っています。額面やスーツをキーワードにして、カードを地図のように表現することにしました

{ :face :queen :suit :hearts }

2つのキーワード要素のリストまたはベクトルを作成することもできます。それがメモリ/パフォーマンスの違いをもたらすかどうかはわからないので、今はマップだけを使用します。

後で気が変わった場合のために、実装の詳細が制御されて隠されるように、プログラムのほとんどの部分がカードの各部分にアクセスするために「インターフェース」を経由する必要があると判断しました。私は機能を持っています

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

プログラムの残りの部分が使用すること。カードはマップとして関数に渡されますが、関数は合意されたインターフェースを使用してマップにアクセスするため、混乱することはありません。

私のプログラムでは、カードはおそらく2値のマップにすぎません。質問では、ゲームの状態全体がマップとして渡されます。ゲームの状態は1枚のカードよりもはるかに複雑になりますが、マップを使用することについて指摘するのは間違いではないと思います。オブジェクト命令言語では、単一の大きな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
xen

これが良いアイデアかどうかは、実際にはそれらのサブプロセス内の状態で何をしているのかに依存します。 Clojureの例を正しく理解している場合、返される状態ディクショナリは、渡されている状態ディクショナリとは異なります。これらは、追加や変更が行われたコピーである可能性があります。Clojureは、言語の機能的な性質はそれに依存します。各関数の元の状態ディクショナリは、決して変更されません。

私が正しく理解している場合、あなたはareコピーを返すのではなく、JavaScript関数に渡す状態オブジェクトを変更します。つまり、Clojureのコードが行っていることとは非常に大きく異なることを行っています。 。 Mike Partridgeが指摘したように、これは基本的に、本当の理由なしに明示的に関数に渡して関数から返すグローバルです。この時点で私はそれが単にあなたを作ることだと思いますthinkあなたはあなたが実際にはしていないことをしているのです。

実際に状態のコピーを明示的に作成し、それを変更してから、その変更されたコピーを返す場合は、続行します。それが必ずしもJavaScriptで実行しようとしていることを達成するための最良の方法であるかどうかはわかりませんが、おそらく、Clojureの例が実行していることと「近い」でしょう。

1
Evicatos

「神オブジェクト」と呼ばれることもあるグローバル状態オブジェクトがあり、それが各プロセスに渡される場合、結束力を低下させながら結合を増加させる多くの要因を混乱させることになります。これらの要因はすべて、長期的な保守性に悪影響を及ぼします。

Tramp Couplingこれは、実際に処理できる場所にデータを取得するために、ほとんどすべてのデータを必要としないさまざまなメソッドを介してデータを渡すことから発生します。この種類のカップリングは、グローバルデータの使用に似ていますが、より限定的にすることができます。トランプカップリングは、「知る必要がある」の反対です。これは、効果を特定し、システム全体で1つの誤ったコードの一部が受ける可能性のある損傷を抑えるために使用されます。

Data Navigation例のすべてのサブプロセスは、必要なデータに正確に到達する方法を知る必要があり、データを処理し、おそらく新しいグローバル状態オブジェクトを構築できる必要があります。これは、トランプ結合の論理的な結果です。データムを操作するには、データムのコンテキスト全体が必要です。繰り返しますが、非地元の知識は悪いことです。

@paulによる投稿で説明されているように、「ジッパー」、「レンズ」、または「カーソル」を渡していた場合、それは1つのことです。アクセスを封じ込め、ジッパーなどがデータの読み取りと書き込みを制御できるようにします。

単一の責任の違反「サブプロセス1」、「サブプロセス2」、および「サブプロセス3」のそれぞれが単一の責任しか持たないと主張する、つまり、適切な値を持つ新しいグローバル状態オブジェクトを生成する、悪質な還元主義です。結局のところ、全部だよね?

ここでの私のポイントは、ゲームのすべての主要なコンポーネントを持つことは、ゲームが委任と因数分解の目的を打ち負かすのと同じ責任を持つということです。

システムへの影響

設計の主な影響は、保守性が低いことです。ゲーム全体を頭に留めることができるという事実は、あなたが優れたプログラマである可能性が非常に高いことを示しています。プロジェクト全体で頭の中に留めておくことができるように私が設計したものはたくさんあります。しかし、それはシステムエンジニアリングのポイントではありません。重要なのは、1人の人間が頭の中で維持できるよりも大きな何かに対応できるシステムを作成することです同時に

別のプログラマ、または2人、または8人を追加すると、システムがすぐにバラバラになります。

  1. 神のオブジェクトの学習曲線はフラットです(つまり、神のオブジェクトに習熟するには非常に長い時間がかかります)。追加の各プログラマーは、あなたが知っているすべてを学び、それを彼らの頭に留めておく必要があります。巨大な神のオブジェクトを維持することで苦しむだけのお金を払うことができるとすれば、あなたは自分より優れたプログラマーを雇うことができます。
  2. 説明では、テストプロビジョニングはホワイトボックスのみです。テストをセットアップして実行し、a)正しいことを行った、およびb)何も行わなかったと判断するためには、godオブジェクトの詳細とテスト対象のモジュールを知る必要があります。 10,000の間違った事の。オッズはあなたに対して大きく積み重ねられています。
  3. 新しい機能を追加するには、a)すべてのサブプロセスを実行して、機能がその中のコードに影響するかどうかを確認し、b)グローバル状態を確認して追加を設計し、c)すべてのユニットを通過する)テストして変更し、テスト中のユニットが新機能に悪影響を与えていないことを確認します

最終的に

  1. 変更可能な神のオブジェクトは、私のプログラミングの存在の呪いであり、私自身がしていることもあれば、私が閉じ込められていることもあります。
  2. Stateモナドはスケーリングしません。状態は指数関数的に成長し、テストと操作に影響を及ぼします。現代のシステムで状態を制御する方法は、委任(責任の分割)とスコーピング(状態のサブセットのみへのアクセスを制限する)によるものです。 「すべてが地図である」アプローチは、状態の制御とは正反対です。
1
BobDalgleish