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


69

Stuart Sierraの講演「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は慣用的に機能的ではなく(そのように書くことはできますが)、不変のデータ構造を書くことは本当に困難です。私が懸念することの1つは、これらの各サブプロセスが純粋であると想定しいることです。なぜその前提を立てる必要があるのですか?私の関数のいずれかが純粋であることはめったにありません(それによって、それらが頻繁に変更されることを意味しgameStateます。それ以外の複雑な副作用はありません)。不変のデータがない場合、これらのアイデアはばらばらになりますか?

いつか目を覚ますと、このデザイン全体が偽物であることに気付き、Big Ball Of Mudアンチパターンを実際に実装しているだけだと心配しています。


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

更新

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

保守性を検証するには、別の目がどのように必要になるかを理解しています。私が言えることは、何よりもまず保守性を気にすることです。私はどこで働いていても、いつもきれいなコードのための最も大きな伝道者です。

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

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

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

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

2番目の点については、参照実装の外部でマップにキーを追加/削除するのではなく、既存のものを変更するだけです。自動テストの大規模なスイートもあります。

このアーキテクチャが最終的に自力で崩壊した場合、2つ目の更新を追加します。そうでなければ、すべてがうまくいっていると仮定します:)


2
クールな質問(+1)!非機能的(またはあまり機能的ではない)言語で機能的なイディオムを実装しようとすることは、非常に有用な演習であることがわかりました。
ジョルジオ

15
(通常は無視できる)パフォーマンスヒットのため、オブジェクト指向スタイルの情報の隠蔽(プロパティとアクセサー関数を含む)は悪いことであると言う人は誰でも、すべてのパラメーターをマップに変えるように指示します。値を取得しようとするたびにハッシュルックアップの(はるかに大きい)オーバーヘッドは無視しても安全です。
メイソンウィーラー

4
@MasonWheelerは、あなたがこれについて正しいと言うことができます。この1つが間違っているため、彼が指摘する他のすべてのポイントを無効にするつもりですか?
ダニエルカプラン

9
Python(およびJavascriptを含むほとんどの動的言語)では、オブジェクトは実際にはdict / mapの単なる構文シュガーです。
ライライアン

6
@EvanPlaice:Big-O表記は誤解を招く可能性があります。単純な事実は、あるものは、 2つのまたは3つの個別のマシンコード命令との直接アクセスに比べて遅く、関数呼び出しとして頻繁に起こる何かに、そのオーバーヘッドが非常にすぐに追加されます。
メイソンウィーラー14

回答:


42

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

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

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

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

編集-例

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

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

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

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

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


2
あなたの2番目の段落は私にとって理にかなっており、それは本当にひどいようです。3番目の段落から、同じデザインについては実際には話していないという感覚を得ています。「再利用」がポイントです。それを避けるのは間違っているでしょう。そして、私はあなたの最後の段落に本当に関係することができません。私はすべての機能を、そのgameState前後に何が起こったかについて何も知らずに実行させます。与えられたデータに単純に反応します。どのようにして機能がお互いの足指を踏む状況になりましたか?例を挙げていただけますか?
ダニエルカプラン

2
少し明確にするために例を追加しました。それが役に立てば幸い。また、UIロジック、ビジネス・ロジック、およびデータベース・アクセス・ロジック混合、多くの理由のために多くの場所で変更セットブロブの周りに渡し対明確に定義された状態オブジェクトの周りに渡すことの違いがあります
ATKを

28

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

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

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

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

オブジェクト指向言語では、状態の追跡がオブジェクトの機能であるため、パターンはさらに意味がありません。


2
「不変の利点は、不変オブジェクトが大きくなるほど低下します」なぜですか?それはパフォーマンスや保守性に関するコメントですか?その文について詳しく説明してください。
ダニエルカプラン

8
@tieTYT Immutablesは、小さなもの(たとえば、数値型)がある場合にうまく機能します。かなり低コストで、コピー、作成、破棄、フライウェイトできます。深くて大きなマップ、ツリー、リスト、および数百ではないにしても数十の変数で構成されるゲーム全体の状態を処理し始めると、それをコピーまたは削除するコストが高くなります(そして、フライウェイトは実用的ではなくなります)。

3
そうですか。それは「命令型言語の不変データ」の問題ですか、それとも「不変データ」の問題ですか?IE:これはClojureコードの問題ではないかもしれません。しかし、JSでどのように表示されるかを確認できます。また、それを行うためにすべての定型コードを書くのも苦痛です。
ダニエルカプラン

3
@MichaelTとKarl:公平を期すために、不変性/効率性のストーリーの反対側に本当に言及すべきです。はい、単純な使用法は恐ろしく非効率的である可能性があります。それが人々がより良いアプローチを思いついた理由です。詳細については、Chris Okasakiの研究を参照してください。

3
@MattFenwick私は個人的には不変物が好きです。スレッドを扱うとき、私は不変についてのことを知っていて、安全に動作してコピーできます。私はそれをパラメータ呼び出しに入れて、誰かが私に戻ったときにそれを変更することを心配せずに別のものに渡します。複雑なゲームの状態について話している場合(質問はこれを例として使用しました-nethackのゲームの状態を不変として「単純」と考えると恐ろしいでしょう)、不変性はおそらく間違ったアプローチです。

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

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


1
OK、私はそれを調べます(そしてそれについては後でコメントします)。しかし、パターンを使用するという考えについてどう思いますか?
ダニエルカプラン

1
@tieTYTパターン自体は非常に良いアイデアだと思います。Stateモナドは一般に、疑似可変アルゴリズム(不変であるが可変性をエミュレートするアルゴリズム)に役立つコード構造化ツールです。
プサリアンの炎

2
このパターンは本質的にモナドであることに注意してください。しかし、実際に可変性を持っている言語では良い考えだとは思いません。モナドは、突然変異を許可しない言語でグローバル/可変状態の機能を提供する方法です。IMOは、不変性を強制しない言語では、Monadパターンは単なるメンタルオナニーです。
ライライアン

6
@LieRyan Monadsは一般に、実際には可変性やグローバルとはまったく関係がありません。Stateモナドのみが具体的に実行します(特にそれが実行するように設計されているためです)。また、状態モナドは可変性を備えた言語では有用ではないことにも同意しますが、その下の可変性に依存する実装は、私が与えた不変のものよりも効率的かもしれません(私はそれについてはまったくわかりません)。モナドインターフェイスは、他の方法では簡単にアクセスできない高レベルの機能を提供できます。stateBind私が与えたコンビネーターは、その非常に単純な例です。
プサリアンの炎

1
@LieRyan I 2番目のPtharienのコメント-ほとんどのモナドは状態や可変性に関するものではなく、具体的にはグローバル状態に関するものでもありません。モナドは、実際にはオブジェクト指向/命令型/可変言語で非常にうまく機能します。

11

したがって、Clojureでのこのアプローチの有効性の間には多くの議論があるようです。Rich Hickeyの哲学を見て、なぜこのようにデータの抽象化をサポートするためにClojureを作成したのかを見ると便利だと思います。

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

ヒッキー:オブジェクト指向と再利用について議論しますが、確かに、車を作る代わりに車輪を再発明するのではないので、物事を再利用できると手元の問題が簡単になります。そして、ClojureがJVM上にあると、多くのホイール(ライブラリ)が利用可能になります。ライブラリを再利用可能にするものは何ですか?1つまたはいくつかのことをうまく行い、比較的自給自足であり、クライアントコードをほとんど要求しません。そのどれもオブジェクト指向から外れることはなく、すべてのJavaライブラリがこの基準を満たしているわけではありませんが、多くはそうしています。

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

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

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

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

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

JavaScriptオブジェクトをデータマップとして操作およびアクセスするためのツールはJavaScript内ではまばらですが、ありがたいことにUnderscoreは便利な操作を提供します。把握する最も簡単な関数には、_。keys、_。values、および_.pluckがあります。_.keysと_.valuesの両方は、オブジェクトを取得し、そのキーまたは値の配列を返すという機能に従って名前が付けられます...


2
以前にこのフォグス/ヒッキーのインタビューを読みましたが、彼が今まで話していたことを理解することができませんでした。ご回答有難うございます。それでも、Hickey / Fogusが私のデザインに彼らの祝福を与えるかどうかはわかりません。私は彼らの助言の精神を極限まで引き受けたのではないかと心配しています。
ダニエルカプラン

9

悪魔の擁護者

この質問は悪魔の擁護者に値するものだと思います(もちろん、私は偏見があります)。@KarlBielefeldtは非常に良い点を挙げていると思うので、それらに対処したいと思います。まず、彼のポイントは素晴らしいと言いたいです。

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

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

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

  • どのフィールドを変更しますか?

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

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

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

ここでは、「すべてが地図」パターンに違いがあるとは思わない。これらは、これらの質問に答える唯一の方法です。

また、JavaScriptおよびほとんどの命令型プログラミング言語でfunctionは、アクセスできる状態はすべて要求、変更、無視できることに注意してください。署名によって違いはありません。関数/メソッドは、グローバル状態またはシングルトンで何かを行うことができます。多くの場合、署名嘘をつきます。

「すべてが地図」と設計が不十分なオブジェクト指向コードとの間の誤った二分法を設定しようとはしていません。私は、より少ない/より細かい/粗いパラメータを受け取る署名があるからといって、関数を分離、設定、呼び出す方法を知っているとは限らないことを指摘しようとしています。

しかし、もしあなたが私にその誤った二分法を使用することを許可するなら:伝統的なOOPの方法でJavaScriptを書くことと比較して、「すべては地図です」はより良いようです。従来のOOPの方法では、関数は、渡す状態または渡さない状態を要求、変更、または無視する場合があります。この「すべてがマップ」パターンでは、渡す状態のみを要求、変更、または無視しますに。

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

私のコードでは、はい。@Evicatosの回答に対する2番目のコメントを参照してください。おそらく、これは私がゲームを作っているからだ、とは言えません。1秒間に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前に落とされたのでしょう。任意の悪者がすでに地面にある戦利品を拾わなければならないかどうかをチェックする必要はありません。だからこそ、これらの操作の順序は完全にarbitrary意的だと思います。

このパターンで分離されたステップを記述するのは自然ですが、従来のOOPで結合されたステップに気付くことは困難です。私が伝統的なOOPを書いていた場合、自然で素朴な考え方は、dead guys drop loot戻り値Lootをに渡さなければならないオブジェクトにすることgood guys pick up lootです。最初の操作は2番目の入力を返すため、これらの操作を並べ替えることはできません。

オブジェクト指向言語では、状態の追跡がオブジェクトの機能であるため、パターンはさらに意味がありません。

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

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

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


4
「関数の最初のパラメーターの名前はfooかもしれませんが、それは何ですか?」そのため、パラメーターに「foo」という名前を付けず、「繰り返し」、「親」、および関数名と組み合わせたときに予想されるものを明確にする他の名前を付けます。
セバスチャン・レッド

1
すべての点であなたに同意する必要があります。Javascriptがこのパターンで実際に引き起こす唯一の問題は、可変データで作業していることです。そのため、状態を変更する可能性が非常に高くなります。ただし、プレーンジャバスクリプトのclojureデータ構造にアクセスできるライブラリがありますが、その名前は忘れています。引数をオブジェクトとして渡すことも前代未聞ではありません。jqueryはこれを複数の場所で行いますが、オブジェクトのどの部分を使用するかを文書化します。個人的には、私はUIフィールドとGameLogicフィールドを分離しますが、何でもあなたのために動作します:)
ロビンHeggelundハンセン

@SebastianRedl私は何のために渡すことになっていparentますか?であるrepetitionsと数字や文字列の配列またはそれは問題ではないのですか?それとも、繰り返しは、私が望む繰り返しの数を表す単なる数字ですか?オプションオブジェクトを取るだけのAPIがたくさんあります。正しい名前を付けると世界はより良い場所になりますが、APIの使用方法がわかるとは限りません。質問はありません。
ダニエルカプラン

8

私のコードは最終的に次のような構造になる傾向があることがわかりました。

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

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

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

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を参照してください。

::更新::

Stuart SierraのClojure / Westの「Clojure in the Large」のビデオ録画もこの主題に触れています。OPのように、彼はマップの一部として副作用を制御するため、テストがはるかに簡単になります。彼はまた、彼の現在のClojureワークフローの概要を説明しているブログ投稿を持っていますが、これは関連があると思われます。


1
@Evicatosへの私のコメントは、ここでの私の立場を詳しく説明すると思います。はい、私は変化しています、そして、関数は純粋ではありません。しかし、私の機能はテストが非常に簡単です。特に、テストを計画していなかったリグレッションの欠陥については、後知恵です。クレジットの半分はJSに割り当てられます。テストに必要なデータのみを使用して「マップ」/オブジェクトを作成するのは非常に簡単です。それを渡すと、突然変異をチェックするのと同じくらい簡単です。副作用は常に表現されているマップので、それらをテストするのは簡単です。
ダニエルカプラン

1
両方の方法を実際に使用するのが「正しい」方法だと思います。テストが簡単で、必要なフィールドを他の開発者に伝えるというメタ問題を軽減できる場合、それは勝利のように思えます。ご質問ありがとうございます。私はあなたが始めた興味深い議論を読んで楽しんでいます。
14:17にアランニング

5

この慣習に反して考えることができる主な論点は、関数が実際に必要とするデータを伝えるのは非常に難しいということです。

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

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


1
はい、私は通常それを変異させるので、それはグローバルです。なぜわざわざ渡すのですか?わかりません、それは有効な質問です。しかし、私の腸は、私がそれを渡すことをやめた場合、私のプログラムはすぐに推論するのが難しくなることを教えてくれます。すべての機能は、どちらか行うことができ、すべてまたは何もグローバルな状態にします。現状では、関数シグネチャにその可能性があります。あなたが言うことができない場合、私はこれのいずれにも自信がありません:)
ダニエルカプラン

1
ところで:再:それに対する主な議論:これはclojureまたはjavascriptであったかどうかに当てはまるようです。しかし、それは重要なポイントです。おそらく、記載されている利点は、そのマイナスをはるかに上回っています。
ダニエルカプラン

2
グローバル変数であるにもかかわらず、それをわざわざ渡す理由がわかりました。純粋な関数を書くことができます。に変更gameState = f(gameState)するとf()、テストするのがはるかに難しくなりますff()私はそれを呼び出すたびに異なるものを返すことがあります。しかしf(gameState)、同じ入力が与えられるたびに同じものを返すのは簡単です。
ダニエルカプラン14

3

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

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

そして

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

それが良いアイデアであるかどうかは、おそらく状況に依存します。しかし、それは確かにClojureの方法と一致しています。Clojureの目的の1つは、Rich Hickeyが「偶発的な複雑さ」と呼ぶものを削除することです。複数の通信オブジェクトは確かに単一のオブジェクトよりも複雑です。機能を複数のオブジェクトに分割すると、突然コミュニケーションと調整、責任の分割について心配する必要があります。これらは、プログラムを書くという本来の目標に付随する合併症です。Rich Hickeyの話Simpleが簡単になったことを確認できます。これは非常に良い考えだと思います。


関連する、より一般的な質問[ Programmers.stackexchange.com/questions/260309/…データモデリングと従来のクラス)
user7610 14年

「オブジェクト指向プログラミングでは、ゴッドオブジェクトとは、あまりにも多くのことを知っている、またはあまりにも多くのオブジェクトです。ゴッドオブジェクトは、アンチパターンの例です。」したがって、神のオブジェクトは良いことではありませんが、あなたのメッセージは反対を言っているようです。それは私を少し混乱させます。
ダニエルカプラン14年

@tieTYTオブジェクト指向プログラミングを行っていないので大丈夫です
user7610 14年

どのようにしてその結論に達しましたか(「大丈夫」という結論)。
ダニエルカプラン14年

OOのGodオブジェクトの問題は、「オブジェクトがすべてに気づき、すべてのオブジェクトが神オブジェクトに依存するようになるため、修正または変更するバグがある場合、実装するのが本当の悪夢になります。」ソースコードには、Godオブジェクトの横に他のオブジェクトがあるため、2番目の部分は問題になりません。最初の部分に関しては、あなたの神のオブジェクトプログラムであり、あなたのプログラムはすべてを知っているべきです。それで大丈夫です。
user7610 14年

2

新しいプロジェクトで遊んでいる間、私は今日このトピックに少しばかり直面しました。私は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

私が見てきたことから、マップまたは他のネストされた構造を使用してこのような単一のグローバルな不変の状態オブジェクトを作成することは、特に純粋なものでは、特にState Monadを@ Ptharien'sFlameとして使用する場合、関数型言語ではかなり一般的ですmentioend

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

  • (不変)状態の(深く)ネストされた値を変更する
  • 状態の大部分を、それを必要としない関数から隠し、作業/突然変異に必要なほんの少しだけを与える

これらの問題を軽減するのに役立ついくつかの異なる手法/一般的なパターンがあります。

最初はジッパーです1つです。これら、不変のネストされた階層内で状態を深く移動し、変化させます。

もう1つはレンズです。これらにより、特定の場所の構造に焦点合わせ、そこの値を読み取り/変更できます。さまざまなレンズを組み合わせて、OOPの調整可能なプロパティチェーンのように、さまざまなことに集中できます(実際のプロパティ名の代わりに変数を使用できます!)

Prismaticは最近、特にJavaScript / ClojureScriptでこの種の手法を使用することに関するブログ投稿を行っています。カーソル(ジッパーと比較)を使用して、関数のウィンドウ状態を表示します。

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

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


前述のトークOPでは、関数が状態マップのサブツリーに更新できる範囲を制限するための更新関数の使用についても説明しています。まだ誰もこれを育てていないと思います。
user7610 14年

@ user7610良いキャッチ、私はそのことを言及するのを忘れていたとは信じられない-私はその機能が好きだ(そしてassoc-in他)。私はちょうど脳にHaskellを持っていたと思います。誰かがJavaScriptの移植を行ったのだろうか?(私のように)彼らは話を見ていないので、人々はおそらくそれを持ち出しませんでした:
ポール14年

@paulはある意味で、ClojureScriptで利用可能だからです。しかし、それがあなたの心に「カウント」されるかどうかはわかりません。PureScriptに存在する可能性があり、JavaScriptで不変のデータ構造を提供するライブラリが少なくとも1つあると思います。少なくともそれらの1つがこれらを持っていることを願っています。
ダニエルカプラン14年

@tieTYTそのコメントを書いたとき、ネイティブのJS実装を考えていましたが、あなたはClojureScript / PureScriptについて良い点を述べています。不変のJSを調べて、そこに何があるかを確認する必要があります。以前はw /で働いたことはありません。
ポール14年

1

これが良いアイデアであるかどうかは、それらのサブプロセス内の状態で何をしているのかに本当に依存します。Clojureの例を正しく理解している場合、返される状態辞書は渡される状態辞書と同じではありません。言語の機能的性質はそれに依存します。各関数の元の状態辞書は変更されません。

私が正しく理解していた場合、あなたはしているあなたのjavascript機能にあなたが通過状態オブジェクトを変更するのではなく、あなたがそのClojureのコードが何をしているかの非常に、非常に、何か違うことをやっている意味のコピーを、返します。マイク・パートリッジが指摘したように、これは基本的にはグローバルであり、実際の理由なしに明示的に関数に渡したり、関数から返したりします。この時点で、それは単にあなたが実際にそうではないことをしていると思わせているだけだと思います。

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


1
最後に、それは本当に「非常に、非常に異なる」ですか?Clojureの例では、彼は古い状態を新しい状態で上書きします。はい、本当の突然変異は起こっていません、アイデンティティはただ変化しています。しかし、彼の「良い」例では、書かれているように、サブプロセス2に渡されたコピーを取得する方法がありません。その値の識別は上書きされました。したがって、「非常に異なる」というのは、実際には言語実装の詳細にすぎないと思います。少なくともあなたが育てることの文脈では。
ダニエルカプラン

2
Clojureの例では、2つのことが行われます。1)最初の例は、特定の順序で呼び出される関数に依存しています。2)関数は純粋であるため、副作用はありません。2番目の例の関数は純粋で、同じ署名を共有しているため、呼び出された順序の隠れた依存関係を心配することなく、それらの順序を変更できます。関数の状態を変更している場合、同じ保証はありません。ステートミューテーションとは、バージョンが構成可能でないことを意味します。これが、辞書の元々の理由でした。
エヴィカトス

1
これを使った私の経験では、思いのままに物事を動かすことができ、ほとんど効果がないので、あなたは私に例を示す必要があります。自分自身でそれを証明するために、update()関数の途中で2つのランダムなサブプロセス呼び出しを移動しました。1つを上に、もう1つを下に移動しました。私のテストはすべて合格し、ゲームをプレイしたときに悪影響はありませんでした。私は私の関数があると感じちょうど Clojureの例のように構成可能。両方のステップで古いデータを破棄しています。
ダニエルカプラン

1
テストに合格し、悪影響に気付かないということは、現在、他の場所で予期しない副作用がある状態を変更していないことを意味します。あなたの関数は純粋ではないので、常にそうなる保証はありません。関数が純粋ではないが、各ステップの後に古いデータを捨てていると言う場合、実装について何かを根本的に誤解しているに違いないと思います。
エヴィカトス

1
@Evicatos-純粋で同じ署名を持っているからといって、関数の順序が問題にならないという意味ではありません。固定割引とパーセンテージ割引が適用された価格を計算することを想像してください。(-> 10 (- 5) (/ 2))2.5を返します。(-> 10 (/ 2) (- 5))0を返します。-
ザック

1

各プロセスに渡される「ゴッドオブジェクト」と呼ばれることもあるグローバルステートオブジェクトがある場合、多くの要素を混乱させることになります。これらの要因はすべて、長期的な保守性に悪影響を及ぼします。

トランプカップリングこれは、データを実際に処理できる場所に到達するために、ほとんどすべてのデータを必要としないさまざまなメソッドにデータを渡すことから発生します。この種の結合は、グローバルデータの使用に似ていますが、より多くの結合が可能です。トランプカップリングは、「知る必要がある」の反対です。これは、効果をローカライズするために使用され、1つの誤ったコードがシステム全体に与える可能性のある損害を封じ込めるために使用されます。

データナビゲーションサンプルのすべてのサブプロセスは、必要なデータを正確に取得する方法を知る必要があり、それを処理し、おそらく新しいグローバルステートオブジェクトを構築できる必要があります。それは、トランプカップリングの論理的な結果です。データムを操作するには、データムのコンテキスト全体が必要です。繰り返しますが、非ローカルな知識は悪いことです。

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

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

ここでの私のポイントは、ゲームのすべての主要なコンポーネントが、委任とファクタリングの目的に反するのと同じ責任を持つということです。

システムへの影響

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

別のプログラマ、2人、または8人を追加すると、システムがほぼ即座に崩壊します。

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

最後に

  1. 可変の神オブジェクトは、私のプログラミングの存在の呪いであり、自分自身がやっていることの一部であり、閉じ込められているものもあります。
  2. Stateモナドはスケーリングしません。状態は指数関数的に成長しますが、テストと操作に対するすべての影響が暗示されています。現代のシステムで状態を制御する方法は、委任(責任の分割)とスコープ(状態のサブセットのみへのアクセスの制限)です。「すべてがマップ」アプローチは、状態を制御するのとは正反対です。
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.