オブジェクト指向コードを機能コードにリファクタリングするために使用できるいくつかのテクニックは何ですか?


9

JavaScriptとHTML5キャンバスを使用してゲームの一部を開発するのに約20〜40時間費やしました。私が始めたとき、私は何をしているのか全く分かりませんでした。したがって、これは概念実証として始まり、現在は順調に進んでいますが、自動テストはありません。ゲームは複雑になり始めており、自動化されたテストの恩恵を受けることができますが、コードがグローバル状態の変化に依存しているため、実行するのは難しいようです。

JavaScript用の関数型プログラミングライブラリであるUnderscore.jsを使用して、すべてをリファクタリングしたいと思います。私の一部は、関数型プログラミングスタイルとテストを使用して、ゼロから始めるべきだと思っています。しかし、命令型コードを宣言型コードにリファクタリングすることは、より優れた学習体験であり、現在の機能の状態に到達するためのより安全な方法になると思います。

問題は、コードを最終的にどのように見せたいかはわかっていますが、現在のコードをそれに変換する方法がわかりません。私はここの何人かの人々がリファクタリングの本とレガシーコードで効果的に働くことのいくつかのヒントを私に与えることを望んでいます。


たとえば、最初のステップとして、私はグローバルステートの「禁止」について考えています。グローバル変数を使用するすべての関数を取り、代わりにパラメーターとして渡します。

次のステップは、ミューテーションを「禁止」し、常に新しいオブジェクトを返すことです。

任意のアドバイスをいただければ幸いです。これまでにOOコードを取得して、Functionalコードにリファクタリングしたことがありません。


1
「次のステップは、ミューテーションを「禁止」し、常に新しいオブジェクトを返すことです。」:IMOミューテーションを完全に禁止する必要はありません。参照の透明性が重要です。参照の透明性を維持する場合、最適化手法として破壊的な更新を使用できます。
ジョルジオ

@ジョルジオ知っておくと良い。ただし、最初に突然変異を「禁止」し、2番目に最適化することをお勧めします。
Daniel Kaplan 2013年

1
最初の2つのステップは素晴らしいアイデアだと思います。モナド的に構成された一連の関数を作成するために私が書いた(小さな)関数型JSライブラリをご覧いただければ光栄です。関数のチェーンを通じて状態を渡しますが、変更することはありません。連鎖する各関数は、渡された値ではなく、新しい値を返すように値が確実に複製されるようにします。github.com/JimmyHoffa/Machinadモナドに慣れていない場合、lib自体はかなり複雑ですが、例を見ると非常に使いやすいことがわかります。実装は複雑でした。
ジミーホファ2013年

1
関数型のスタイルで物事をやりたいのであれば、コードがある場合でも継承はコードから洗い流すことになります。関数を作成し、javascriptの優れた機能であるレキシカルスコープを使用して、階層ツリーの代わりに必要な場所で機能を利用できるようにします。データ構造をそれらと同じように定義し、関数を追加するだけで、データ構造を最初のパラメーターとして使用するのと同じように、流暢なスタイルを支援できます。OOではこれは貧血モデルです。FPについては、構成はこれらの欠点を回避するための鍵となります。
ジミーホファ2013年

@tieTYT:もちろんあなたにも同意します。時期尚早の最適化は悪いことです。
ジョルジオ

回答:


9

関数型のJavaScriptでいくつかのプロジェクトを作成したので、私の経験を共有できます。具体的な実装の問題ではなく、高レベルの考慮事項に焦点を当てます。特効薬はありません。特定の状況に最適な方法を実行してください。

純粋な機能スタイルと機能スタイル

この機能的リファクタリングから何を取得したいかを考えてください。純粋な関数型実装が本当に必要ですか、それとも、参照の透過性など、関数型プログラミングスタイルがもたらすメリットの一部だけですか。

後者のアプローチ、つまり私が関数型スタイルと呼んでいるものをJavaScriptでうまくメッシュ化できることがわかりました。また、選択的な非機能概念を、意味のあるコードで注意深く使用することもできます。

より純粋に機能的なスタイルでコードを記述することはJavaScriptでも可能ですが、常に最も適切なソリューションであるとは限りません。そして、JavaScriptが純粋に機能することは決してありません。

機能的なスタイルのインターフェース

関数型Javascriptで最も重要な考慮事項は、関数型コードと命令型コードの境界です。この境界を明確に理解して定義することが不可欠です。

関数型のインターフェイスを中心にコードを整理します。これらは、個々の機能からモジュール全体に至るまで、機能的なスタイルで動作するロジックのコレクションです。インターフェースと実装の概念的な分離は重要です。命令型の実装が境界を越えてリークしない限り、関数型のインターフェースを命令型で実装できます。

残念ながら、Javascriptでは、機能的なスタイルのインターフェースを強制する負担は完全に開発者にあります。また、例外などの言語機能により、完全に明確な分離が不可能になります。

既存のコードベースをリファクタリングするので、関数スタイルにきれいに移動できるコード内の既存の論理インターフェイスを特定することから始めます。驚くべき量のコードがすでに機能的なスタイルである可能性があります。良い出発点は、必須の依存関係がほとんどない小さなインターフェースです。コードを移動するたびに、既存の必須の依存関係が排除され、より多くのコードを移動できます。

結果に満足するまで、繰り返し、徐々に外側に向かって作業し、プロジェクトの大部分を境界の機能面に押し込みます。この漸進的なアプローチにより、個々の変更をテストでき、境界の識別と適用が容易になります。

アルゴリズム、データ構造、設計の見直し

命令型ソリューションと設計パターンは、機能的なスタイルでは意味を成さないことがよくあります。特にデータ構造の場合、命令型コードを関数型スタイルに直接移植すると、醜くて遅くなる可能性があります。簡単な例:先頭のリストのすべての要素をコピーするか、既存のリストに要素を連結しますか?

問題に対する既存の機能的アプローチを調査および評価します。関数型プログラミングは単なるプログラミング手法ではなく、問題について考え、解決する別の方法です。lispやhaskellなどのより伝統的な関数型プログラミング言語で書かれたプロジェクトは、この点で優れたリソースです。

言語とビルトインを理解する

これは、機能的な命令境界を理解する上で重要な部分であり、インターフェースの設計に影響を与えます。関数スタイルのコードで安全に使用できる機能と使用できない機能を理解します。ビルトインが例外をスローし[].pushたり、オブジェクトを変更したり、参照によって渡されるオブジェクトやプロトタイプチェーンの動作に至るまでのすべて。プロジェクトで使用する他のライブラリについても同様に理解する必要があります。

このような知識により、不適切な入力や例外を処理する方法や、コールバック関数が予期しない何かをした場合に何が起こるかなど、情報に基づいた設計決定を行うことができます これにはコードで強制できるものもありますが、適切な使用方法に関するドキュメントが必要なものもあります。

もう1つのポイントは、実装の厳密性です。最大のコールスタックエラーに対して、またはユーザーがいくつかの組み込み関数を再定義するときに、正しい動作を強制してみますか?

適切な場所で機能以外の概念を使用する

特にJavaScriptのような言語では、機能的アプローチが常に適切であるとは限りません。より大きな入力に対して最大のコールスタック例外が発生し始めるまで、再帰的ソリューションは洗練されている可能性があるため、おそらく反復的なアプローチの方が良いでしょう。同様に、コード編成、コンストラクターでの割り当て、および意味のある不変オブジェクトにも継承を使用します。

機能インターフェースに違反していない限り、意味のあることを行い、テストと保守が最も簡単なものを実行します。


これは、非常に単純な実際のコードを関数型スタイルに変換する例です。プロセスの大きな例はあまりありませんが、この小さな例では、いくつかの興味深い点に触れています。

このコードは、文字列からトライを構築します。John Resigのtrie-jsビルドトライモジュールの上部セクションに基づくコードから始めます。多くの簡素化/フォーマットの変更が行われ、私の意図は元のコードの品質についてコメントすることではありません(トライを構築するためのより明確でよりクリーンな方法があるため、このCスタイルのコードがGoogleで最初に登場する理由は興味深いです。ここにあります。reduceを使用する迅速な実装)。

この例が好きなのは、本当の機能的な実装、つまり機能的なインターフェースにまでわざわざ移動させる必要がないためです。ここが出発点です:

var txt = SOME_USER_STRING,
    words = txt.replace(/\n/g, "").split(" "),
    trie = {};

for (var i = 0, l = words.length; i < l; i++) {
    var word = words[i], letters = word.split(""), cur = trie;
    for (var j = 0; j < letters.length; j++) {
        var letter = letters[j];
        if (!cur.hasOwnProperty(letter))
            cur[letter] = {};
        cur = cur[ letter ];
    }
    cur['$'] = true;
}

// Output for text = 'a ab f abc abf'
trie = {
    "a": {
        "$":true,
        "b":{
            "$":true,
            "c":{"$":true}
        "f":{"$":true}}},
    "f":{"$":true}};

これがプログラム全体であるとしましょう。このプログラムは2つのことを行います。文字列を単語のリストに変換し、単語のリストからトライを作成します。これらはプログラムの既存のインターフェースです。以下は、インターフェースをより明確に表現したプログラムです。

var _get_words = function(txt) {
    return txt.replace(/\n/g, "").split(" ");
};

var _build_trie_from_list = function(words, trie) {
    for (var i = 0, l = words.length; i < l; i++) {
        var word = words[i], letters = word.split(""), cur = trie;
        for (var j = 0; j < letters.length; j++) {
            var letter = letters[j];
            if (!cur.hasOwnProperty(letter))
                cur[letter] = {};
            cur = cur[ letter ];
        }
        cur['$'] = true;
    }
};

// The 'public' interface
var build_trie = function(txt) { 
    var words = _get_words(txt), trie = {};
    _build_trie_from_list(words, trie);
    return trie;
};

インターフェースを定義するだけ_get_wordsで、境界の機能スタイル側に移動できます。どちらも、replaceまたはsplit元の文字列を変更しません。また、build_trie非常に命令的なコードと相互作用するにもかかわらず、私たちのパブリックインターフェイスも機能的なスタイルです。実際、ほとんどの場合、これは適切な停止点です。コードが多すぎると圧倒されるので、他のいくつかの変更について概説しましょう。

まず、すべてのインターフェースを機能的なスタイルにします。この場合、これは_build_trie_from_list取るに足らないことです。オブジェクトを変更するのではなく、オブジェクトを返すだけです。

悪い入力の処理

build_trie文字の配列で呼び出すとどうなるかを考えてみましょうbuild_trie(['a', ' ', 'a', 'b', 'c', ' ', 'f'])。呼び出し元は、これが関数スタイルで動作すると想定していましたが.replace、配列で呼び出されると例外をスローします。これは意図された動作である可能性があります。または、明示的な型チェックを実行して、入力が期待どおりであることを確認することもできます。しかし、私はアヒルのタイピングを好みます。

文字列は単なる文字の配列であり、配列はlengthプロパティと負でない整数をキーとする単なるオブジェクトです。したがって、文字列の配列のようなオブジェクトを操作する新しいメソッドreplacesplitメソッドをリファクタリングして記述した場合、それらは文字列である必要さえありません。コードは正しいことを行うだけです。(String.prototype.*ここでは機能しません。出力を文字列に変換します)。ダックタイピングは関数型プログラミングとは完全に別のものですが、大きな点は、悪い入力は常に考慮されるべきだということです。

根本的な再設計

さらに基本的な考慮事項もあります。関数型にもトライを構築したいとします。命令型コードでは、アプローチは一度に1つの単語を構成することでした。ダイレクトポートでは、オブジェクトの変更を回避するために挿入を行う必要があるたびに、トライ全体をコピーする必要があります。明らかにそれはうまくいきません。代わりに、トライをノードごとに、下から上に構築することができます。これにより、ノードが完了すると、ノードに再度触れる必要がなくなります。または、別のアプローチの方が完全に良いかもしれません。クイック検索で、試行の既存の機能実装の多くが示されます。

例が少し明確にしてくれることを願っています。


全体として、Javascriptで関数型のコードを書くことは、やりがいのある楽しい試みだと思います。Javascriptは、デフォルトの状態では冗長すぎますが、驚くほど機能的な関数型言語です。

プロジェクトで頑張ってください。

関数型のJavaScriptで記述されたいくつかの個人プロジェクト:

  • parse.js-パーサーコンビネーター
  • Nu-レイジーストリーム
  • Atum-関数スタイルのJavascriptで記述されたJavascriptインタープリター。
  • Khepri-関数型Javascript開発に使用するプロトタイププログラミング言語。関数型のJavascriptで実装されています。

これを書くために時間を割いてくれて本当にありがとう。いくつかの例は大いに評価されます(そして、個人プロジェクトは「後」を示し、「前」からそこに到達する方法を示していないため、これには役立ちません)。これは、「Functional Style Interfaces」というタイトルのセクションで最も役立ちます。
Daniel Kaplan 2013年

簡単な例を追加しました。変換プロセスを示すより大きな例を見つけるのは難しいので、できるだけ早く実際のコードで作業を開始することを強くお勧めします。これは実際には非常にプロジェクト固有のプロセスであり、実際に機能しますが、コードは多くのことを教えてくれます。たぶん最初のパスは素晴らしいものではないかもしれませんが、結果に満足するまで繰り返し学習し続けます。また、他の言語用の機能設計の優れた例がオンラインでたくさんあります(私は個人的に、スキームとクロージュールが多くのトピックの最良の出発点であると思います)。
Matt Bierner 2013年

私は、各パートupvoteことができるように、この答えは、いくつかに分けた希望
ポール・
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.