web-dev-qa-db-ja.com

長い関数やメソッドを他のものから呼び出されなくても、小さなものに分割しても問題ありませんか?

最近、長いメソッドをいくつかの短いメソッドに分割しようとしています。

例:私はprocess_url()関数を使用して、URLをコンポーネントに分割し、それらをメソッドを介していくつかのオブジェクトに割り当てます。これらすべてを1つの関数に実装するのではなく、process_url()で分割するためのURLを準備し、それをprocess_components()関数に渡します。これにより、コンポーネントがassign_components() 関数。

最初は、これは読みやすさを改善するように思われました。なぜなら、巨大な「神」のメソッドと関数の代わりに、よりわかりやすい名前の小さなものを使ったからです。しかし、私がそのように記述したいくつかのコードを見ると、これらの小さな関数が他の関数またはメソッドによって呼び出されるかどうか、今は分からないことがわかりました。

前の例を続ける:コードを見る人は、process_components()機能が関数に抽象化されていると考えるかもしれません。関数、実際にはprocess_url()によってのみ呼び出されます。

これは少し間違っているようです。もう1つの方法は、長いメソッドと関数を引き続き記述することですが、それらのセクションをコメントで示します。

説明した関数分割手法は間違っていますか?大規模な関数とメソッドを管理するための好ましい方法は何ですか?

UPDATE:私の主な懸念は、コードを関数に抽象化すると、それが他の複数の関数によって呼び出される可能性があることを意味する可能性があるということです。

参照:/ r/programming でのredditに関するディスカッション(ここでのほとんどの回答ではなく、異なる視点を提供します)および / r/readablecode

165
sbichenko

多くのことを行うコードのテストは困難です。

多くのことを行うコードのデバッグは困難です。

これら両方の問題の解決策は、多くのことを行わないコードを書くことです。各関数は、1つのことだけを実行するように記述します。これにより、ユニットテストで簡単にテストできます(数十のユニットテストは必要ありません)。

私の同僚は、特定のメソッドをより小さなメソッドに分割する必要があるかどうかを判断するときに使用するフレーズを持っています。

コードのアクティビティを別のプログラマーに説明するときにWordの「and」を使用する場合、メソッドを少なくとも1つ以上の部分に分割する必要があります。

あなたが書いた:

URLをコンポーネントに分割するprocess_url()関数andがあり、それらをメソッドを介していくつかのオブジェクトに割り当てます。

これは少なくとも2つの方法である必要があります。公開されている1つのメソッドでそれらをラップしてもかまいませんが、動作は2つの異なるメソッドである必要があります。

230
user40980

はい、長い関数の分割は正常です。これは、Robert C. Martinの著書 Clean Code で推奨されていることを行う方法です。特に、自己文書化コードの形式として、関数の非常にわかりやすい名前を選択する必要があります。

77
Scott Whitlock

人々が指摘したように、これにより読みやすさが向上します。 process_url()を読んでいる人は、いくつかのメソッド名を読むだけで、URLを処理するための一般的なプロセスをより明確に理解できます。

問題は、他の人々がそれらの関数がコードの他の部分で使用されていると考え、それらのいくつかを変更する必要がある場合、それらの関数を保持して新しい関数を定義することを選択できることです。これは、一部のコードが到達不能になることを意味します。

これに対処するにはいくつかの方法があります。最初は、コード内のドキュメントとコメントです。第二に、カバレッジテストを提供するツール。いずれにせよ、これはプログラミング言語に大きく依存します。これらは、プログラミング言語に応じて適用できるいくつかのソリューションです。

  • オブジェクト指向言語は、いくつかのプライベートメソッドを定義して、他の場所で使用されないようにすることができます。
  • 他の言語のモジュールは、外部から見える関数などを指定するかもしれません。
  • Pythonのような非常に高水準の言語は、とにかく単純な1つのライナーなので、いくつかの関数を定義する必要をなくす可能性があります。
  • prologなどの他の言語では、条件ジャンプごとに新しい述語を定義する必要がある(または強く推奨する)場合があります。
  • 場合によっては、それらを使用する関数(ローカル関数)内に補助関数を定義するのが一般的です。これらは、匿名関数(コードクロージャ)である場合があります。これは、JavaScriptコールバック関数で一般的です。

つまり、簡単に言うと、いくつかの関数に分割することは、通常、読みやすさの点で良い考えです。関数が非常に短く、これがgoto効果を生み出す場合、または名前が実際に説明的でない場合、それは実際には良くないかもしれません。これらの関数のスコープと使用に関する懸念については、一般的に言語に依存する、いくつかの対処方法があります。

一般に、最良のアドバイスは常識を使用することです。厳格なルールは間違っている可能性が非常に高く、最終的には人によって異なります。私はこれを読みやすいと思います:

process_url = lambda url: dict(re.findall('([^?=&]*)=([^?=&]*)', url))

個人的には、いくつかのコードファイルをジャンプして検索するのではなく、やや複雑であっても1つのライナーを好みます。コードの他の部分を見つけるのに3秒以上かかる場合は、知らないこともあります。とにかく何をチェックしていたのか。 ADHDに悩まされていない人は覚えやすい説明的な名前を好むかもしれませんが、結局あなたがいつもやっているのは、コード、行、段落、関数、ファイル、モジュールなどのさまざまなレベルの複雑さのバランスをとることです。

したがって、キーワードはbalanceです。 1000行の関数は、カプセル化がなく、コンテキストが非常に大きくなるため、それを読む人にとっては地獄です。関数が1000個の関数に分割され、それぞれに1行が含まれている場合は、さらに悪くなる可能性があります。

  • あなたはいくつかの名前を持っています(あなたは行のコメントとして提供することができたでしょう)
  • あなたは(うまくいけば)グローバル変数を排除していて、状態を心配する必要はありません(参照透過性を持っています)
  • しかし、読者は前後にジャンプする必要があります。

したがって、ここには特効薬はありませんが、経験とバランスが取れています。私見これを行う方法を学ぶ最良の方法は、他の人が書いた多くのコードを読んで、なぜあなたがそれを読むのが難しいのか、そしてそれをより読みやすくする方法を分析することです。それは貴重な経験を提供するでしょう。

39
Trylks

場合によります。

分割してprocess_url_partNなどの名前を付けるために分割するだけの場合は、[〜#〜] no [〜#〜]、しないでください。自分や他の誰かが何が起こっているのかを理解する必要があるときに、後で追跡するのが難しくなるだけです。

自分でテストできる明確な目的でメソッドを引き出す場合、(他の誰も使用していない場合でも)自分で理にかなっている場合は[〜#〜] yes [〜#〜]


あなたの特定の目的のために、あなたは2つの目標を持っているようです。

  1. URLを解析し、そのコンポーネントのリストを返します。
  2. これらのコンポーネントで何かを行います。

私は最初の部分を個別に記述し、簡単にテストでき、後で再利用できる可能性があるかなり一般的な結果を返すようにします。さらに良いことに、私はあなたの言語/フレームワークですでにこれを行っている組み込み関数を探し、あなたの構文解析が超特別でない限り、代わりにそれを使用します。それが非常に特殊な場合でも、別のメソッドとして記述しますが、おそらく2番目のクラスを処理するクラスのプライベート/保護メソッドとして「バンドル」します(オブジェクト指向のコードを記述している場合)。

2番目の部分は、URL解析に最初の部分を使用する独自のコンポーネントとして記述します。

17
Svish

私は自分が従うパターンであるため、他の開発者が大きなメソッドを小さなメソッドに分割することに問題を抱えたことはありません。 「神」の方法は、陥りがちな恐ろしい罠であり、経験の浅い、または単に気にしない人は、捕まえられることが多いです。言われていること...

小さなメソッドで適切なアクセス識別子を使用することは非常に重要です。小さなパブリックメソッドが散らばっているクラスを見つけるのはイライラします。アプリケーション全体でメソッドがどこでどのように使用されているかを見つけるのにまったく自信がなくなるからです。

私はC#の土地に住んでいるので、publicprivateprotectedinternalがあり、それらの単語を見ると、疑いの余地がありません。メソッドのスコープと呼び出しを探す場所。プライベートの場合は、メソッドが1つのクラスでのみ使用されていることを知っており、リファクタリングを行う際に十分な自信があります。

Visual Studioの世界では、複数のソリューション(.sln)IDE/Resharperの「Find Usages」ヘルパーはオープンソリューション外の使用法を見つけられないため、このアンチパターンを悪化させます。

14
Ryan Rodemoyer

プログラミング言語でサポートされている場合は、process_url()関数のスコープ内で「ヘルパー」関数を定義して、個別の関数の可読性の利点を得ることができます。例えば.

_function process_url(url) {

    function foo(a) {
        // ...
    }

    function bar(a) {
        // ...
    }

    return [ foo(url), bar(url) ];

}
_

プログラミング言語がこれをサポートしていない場合は、foo()およびbar()process_url()のスコープ外に移動することができます(他の関数から見えるようにするため/メソッド)-ただし、プログラミング言語がこの機能をサポートしていないため、これを導入した「ハック」と見なしてください。

関数をサブ関数に分割するかどうかは、特に、パーツに意味のある/有用な名前があるかどうか、および各関数の大きさなどに依存します。

11
mjs

誰かがこの質問に関するいくつかの文献に興味がある場合:これはまさに、ジョシュアケリエフスキーが彼の「パターンへのリファクタリング」(Addison-Wesley)で「構成方法」と呼んでいるものです。

ロジックを、同じ詳細レベルで少数の意図を明らかにするステップに変換します。

ここでは、メソッドの「詳細レベル」に応じた正しいネストが重要だと思います。

出版社のサイトで 抜粋 を参照してください:

私たちが作成するコードの多くは、最初は単純ではありません。簡単にするために、私たちはそれについて単純ではないことを振り返り、絶えず「どうしてもっと簡単にできるのか」と尋ねる必要があります。完全に異なるソリューションを検討することで、コードを単純化できることがよくあります。この章のリファクタリングは、メソッド、状態遷移、およびツリー構造を簡素化するためのさまざまなソリューションを示しています。

Composeメソッド(123)は、メソッドの実行方法と実行方法を効率的に伝達するメソッドを作成することです。構成されたメソッド[Beck、SBPP]は、すべて同じ詳細レベルにある適切な名前のメソッドの呼び出しで構成されています。システムをシンプルにしたい場合は、Composeメソッド(123)をどこにでも適用するようにしてください...

http://ptgmedia.pearsoncmg.com/images/ch7_9780321213358/elementLinks/07fig01.jpg

補遺:ケントベック( "実装パターン" )は、これを「構成された方法」と呼んでいます。彼はあなたに次のことを勧めています:

[c]メソッドを他のメソッドの呼び出しから除外します。各メソッドは、ほぼ同じレベルの抽象化です。構成が不十分なメソッドの兆候の1つは、抽象化レベルの混合[。]です。

ここでも、異なる抽象化レベルを混在させないようにという警告(強調は私のもの)です。

9
Sebastian

同様のレベルで近くの抽象化を使用することをお勧めします( この答え のセバスチャンによってよりよく定式化されます。)

つまり低レベルのものを扱う(大きな)関数があり、より高いレベルの選択も行う場合は、低レベルのものを除外してみてください。

void foo() {

     if(x) {
       y = doXformanysmallthings();
     }

     z = doYforthings(y);

     if (z != y && isFullmoon()) {
         launchSpacerocket()
     }
}

小さな関数にものを移動することは、いくつかの概念的な「大きな」ステップで構成される関数内に多数のループを設けるよりも、通常は優れています。 (それを比較的小さなLINQ/foreach/lambda式に組み合わせることができない限り...)

5
Macke

これらの関数に適したクラスを設計できる場合は、それらをプライベートにしてください。言い換えれば、適切なクラス定義を使用すると、公開する必要があるものだけを公開できます。

4
Tom Haley

これは人気のある意見ではないと確信していますが、まったく問題ありません。参照の局所性は、あなたや他の人が関数を確実に理解するのに非常に役立ちます(この場合は、特にメモリではなくコードを参照しています)。

すべてのように、それはバランスです。 「常に」または「決して」とあなたに言う人にはもっと気を配るべきです。

4
Fred

この単純な関数について考えてみます(私はScalaに似た構文を使用していますが、Scalaの知識がなくてもアイデアが明確になることを願っています)。

_def myFun ... {
    ...
    if (condition1) {
        ...
    } else {
        ...
    }
    if (condition2) {
        ...
    } else {
        ...
    }
    if (condition3) {
        ...
    } else {
        ...
    }
    // rest
    ...
}
_

この関数には、条件の評価方法に応じて、コードを実行する方法が8つまであります。

  • つまり、この部分をテストするためだけに8つの異なるテストが必要になります。さらに、いくつかの組み合わせは不可能である可能性が高いため、それらが何であるかを注意深く分析する必要があります(そして、可能な組み合わせを見逃さないように注意してください)-多くの作業。
  • コードとその正確さについて推論することは非常に困難です。 ifブロックのそれぞれとその状態はいくつかの共有ローカル変数に依存する可能性があるため、それらの後に何が起こっているかを知るために、コードを扱うすべての人がそれらすべてのコードブロックと8つの可能な実行パスを分析する必要があります。ここで間違いを犯すのは非常に簡単で、コードを更新している誰かが何かを見逃してバグを導入する可能性が最も高いです。

一方、計算を次のように構成すると、

_def myFun ... {
    ...
    val result1 = myHelper1(...);
    val result2 = myHelper2(...);
    val result3 = myHelper3(...);
    // rest
    ...
}

private def myHelper1(/* arguments */): SomeResult = {
    if (condition1) {
        ...
    } else {
        ...
    }
}
// Similarly myHelper2 and myHelper3
_

あなたはできる:

  • 各ヘルパー関数を簡単にテストします。各ヘルパー関数には、2つの実行パスしかありません。
  • myFunを調べると、_result2_が_result1_に依存しているかどうかがすぐにわかります(myHelper2(...)への呼び出しがそれを使用して引数の1つを計算しているかどうかを確認します。(仮定ヘルパーは一部のグローバル状態を使用しないことに注意してください。)また、how依存していることも明らかです。これは、前のケースでは理解するのがはるかに困難です。さらに、3つの呼び出しの後、計算の状態がどのように見えるかを明確にします。_result1_、_result2_、_result3_だけでキャプチャされます。他のローカル変数が変更されているかどうかを確認する必要はありません。
3
Petr Pudlák

メソッドがより具体的な責任を負うほど、テスト、読み取り、および保守がより簡単になります。他のものはそれらを呼び出しませんが。

将来、他の場所からこの機能を使用する必要がある場合は、それらのメソッドを簡単に抽出できます。

2
lucasvc