web-dev-qa-db-ja.com

非常に大きな関数/プログラムを小さな関数に分割します。効果的ですか?

関数名は非常に表現力豊かなものになる可能性があることを知っています。そのため、プログラムを特定の関数に分割し、それらを大きな「監視可能な」関数ファイルから呼び出すのは魅力的です。

しかし、これはプログラマにとって実際に効果的ですか?各関数は通常、前の関数から提供された入力、戻り値、グローバル変数、または言語が使用するその他のものに依存しているため、機能することが多く、前​​の関数との関連でのみ意味があります。以前の関数に変更を加えると、2番目の関数の機能が明らかに破壊されることがよくあります。

1つのプログラムが関数ファイルに乱雑に分割されている場合。変更による影響は必ずしも明確ではありません。それで、このように分割することは良い考えでしょうか?

4

最近それを行った人として、数枚の長いコードを小さな関数のセットにリファクタリングしたので、それが効果的であると証明できます。

私はあなたがUnixの原則について聞いたと思います:「一つのことをして、うまくやる」。それは非常に役立ちます。

短い関数は、観察、推論、テストが簡単です。重要なことは、他のほとんどの関数がない場合でも通常は簡単にテストでき、依存しているオブジェクトがあれば、少数のオブジェクトをモックするだけです。

短い関数を書くときは、それが何をするのかを正確に考え、適切な名前(doIt()が適合しない)、適切なパラメーター名などを考え出す必要があります。これにより、コードがよりわかりやすくなり、あなたはコードをよりよく理解します。また、プログラムを記述するための上位レベルの概念も提供します。

複雑な状態遷移を短い関数に分割することで、プログラムのデータフローについて考え、可能な限り解きほぐすことができます。これにより、データの依存関係が少なくなり、多くの場合、一部のデータの寿命が短くなり(データが巨大な場合に重要です)、データの競合、状態の更新が互いに重なり合うなどのエラーが少なくなります。

画面全体よりも大きいコードのフラグメントを関数に分解すると、わずかに変更されていても、コピーして貼り付けられたコードに気づき、排除できます。共通のパターンを見つけることは理解に役立ちます。

一部のライブラリー関数が既に行っているのと(ほとんど)同じことを行うため、すぐに認識できる関数になることがあります。行数を削減し、他の誰かの既存の作業(および今後の改善とバグ修正)を再利用します。

逆に、抽出した関数は他のプログラムで再利用できます。モノリシックコードのフラグメントは、コピーアンドペーストしない限り再利用できません。

しかし、多くの人は短い関数を書くのに苦労しています。彼らは散文の章全体を書くように、すべてを行う長いモノリシックなスクリプトを書く傾向があります。対策の1つは、コードを記述するだけでなく、すぐに実行することです。

PythonまたはJSのような動的言語、あるいはScalaのようなものさえあれば、常にREPLを開いておいてください。フラグメントを思いついたらコードの例をREPLで試してみてください。その依存オブジェクトが必要ですか?各依存オブジェクトを提供する関数を記述してください。意味のあるものを生成するコードフラグメントを思いついたら、それも関数にラップしてください。このようにして、必要なプログラム全体を構成する一連の関数が作成され、そのほとんどがすでに実行されています。

REPLがすぐに提供されない言語の場合、状況はさらに難しくなります。ここで「テスト駆動設計」が役立つ場合があります。REPLでコードフラグメントを自由に操作することはできませんが、実行できますテストとして安価に。あなたが行くにつれて、プログラム全体と、それを検証および説明する一連のテストで終わります。モノリスでは、これは不可能です。

プログラムが使い捨てのスクリプトであり、本番環境で実行する前にピアレビューするほど重要ではない場合、コードの壁は問題ないかもしれません。

それ以外の場合は、コードを小さな要素に分解することで、拡張、変更、他の誰かによるコードの変更を確認、または単に参照する必要があるたびにお金がかかります。

7
9000

各関数は通常、前の関数から提供された入力、戻り値、グローバル変数、または言語が使用するその他のものに依存するため、多くの場合、機能するだけであり、前の関数との関連で意味があります。

私は常に、巨大で広大なif/elseツリーよりもコンパクトで自己完結型の関数を優先していますが、実際のプログラムは、私たちが望むほどナイスで端正な(もつれていない)ことはめったにありません。アプリケーションの状態(グローバルかどうか)は一般に避けられません。

物事をますます小さな機能に分割することへの還元が減っているのも事実だと私は信じています。論理的な極端については、Robert C. Martin著の本 Clean Code をご覧ください。誰もが私に同意するわけではありませんが、彼の例の多くは実際には少し少ない理解しやすい後でリファクタリングした(---)とわかります。 (たぶん私は十分に書いていないJavaそれらを評価するために?)

そうは言っても、私は個人的に関数は私たちが利用できる最も強力な抽象化であると信じています。確かに、関数は誤って使用できます。グローバル変数であるこれらの邪悪な武器がなくても、小さな関数の束でひどく絡み合ったコードを書くのを妨げるものは絶対にありません。しかし全体として、機能は善と光の力です。

実際に可能な場合は、「クリーン」な機能を作成する際に群衆の標準的な知恵に注意してください。

  • 関数内のany外部状態を変更しないでください。
  • 関数は決定論的である必要があります。同じ入力は常に同じ出力を生成する必要があります。

これら2つのルールだけで、最も一般的な設計の頭痛の90%を回避できます。また、関数のテストを簡単に作成することもできます。

それを超えて、できるだけ「クリーン」な機能だけを作るように頑張ってください。アプリケーションによっては、これは実際には最初は非常に難しい練習になる場合があります。しかし、人はそれでより良くなります。ほとんどの場合と同様に、それは本当の技術であり、それを正しく行うには経験(特にプログラミングの一般的な経験と、特にそのプロジェクトの経験)が必要です。

もつれのないコードは発生するだけではありません。それは大変な仕事であり、芸術です...そしてそれだけの価値があります。

6
DaveGauer

はい、これは一般的に良い考えです。通常、関数が小さいほど読みやすく、理解しやすく、さまざまなコンテキストで再利用できます。 refactoring のプロセスは時々いくつかのことを壊すことができますが、それを適切に行うと(そして彼らは壊れてユニットテストを使用して壊れるものを検出するのを助けます)そして最終結果は通常それだけの価値があります。

各関数は通常、前の関数から提供された入力、戻り値、グローバル変数、または言語が使用するその他のものに依存しているため、多くの場合、機能するだけで、前の関数との関連で意味があります。

^このステートメントは、コードを1つの大きな関数のままにしても当てはまります。相互に依存する小さな関数の代わりに、相互に依存する大きな関数内のコード行があります。実行のどの段階のどのコードにどの変数が適用されるかを示す方法がないため、これはさらに悪いことです。ローカルスコープのすべてが関数のすべてに利用できます。

それを一連の小さな関数に分解する場合は、関数をパラメーターとして宣言し、各関数のローカルスコープを利用することで、どの関数がどの依存関係で機能するかに関する制約を定義できます。したがって、大きな問題を一連の小さな問題に分割できます。(1)明確な区切りがあり、それぞれが理解しやすくなります。(2)コードで定義された明確な関係があり、問題がどのように発生するかを示します。サブ問題に分解されます。

これは、プログラマーが上下にスクロールして動作を理解する必要がある多くのページをカバーする長い関数よりもはるかに望ましい方法です。長年のプログラミングの後、私は経験からこれらが厄介なバグでいっぱいになる傾向がある関数であることを伝えることができます。

4
John Wu

それはしばしば機能するだけであり、以前の機能との関連で理にかなっています。以前の関数に変更を加えると、2番目の関数の機能が明らかに破壊されることがよくあります。

その場合は、関数の設計が不適切です。それらには明確な責任または契約がなく、優れた抽象化を表していません。

適切に設計された関数は、実装の詳細を知らなくても、関数名から概念的に理解しやすい1つのことを行い、ほとんどの場合、アプリケーションの他の部分から分離されています。

実装する機能の真の抽象化であるそのような関数がある場合、それらは異なるコンテキストで再利用可能であることがはるかに多く、同時に、それらが変更されたときに望ましくない副作用が発生する可能性が低くなります。

不適切に設計された関数では、関数に変更を加えると、それが呼び出された他の5つの場所で何かが壊れるのではないかと心配になります。うまく設計された関数を使用すると、それに加えた変更は何か(バグ修正など)となり、呼び出し元の他の5つの場所にwantを適用します。

2

しかし、これはプログラマにとって実際に効果的ですか?

関数型プログラミング言語は、関数ですべてを実行します。関数型プログラミングをほとんどサポートしていない言語でも、かなり近づくことができるはずです。あ、はい。

以前の関数に変更を加えると、2番目の関数の機能が明らかに破壊されることがよくあります。

その後、それをしないでください。古い関数をそのままにして、新しい関数を作成します。おそらく古い関数を使用してほとんどの作業を行い、出力を調整します。

関数型プログラミング言語の一般的な哲学は、専門用語の語彙を構築し、それを使って何かを書くようなものです。そして、どのように専門用語を入手しますか?さて、あなたはすでに持っている他の言葉で定義を書きます。ランダムな場所に分割して関数に押し込む必要がある既存のコードの巨大な部分と考えるのではなく、最上位の関数を簡単に記述できるようにする関数の語彙を作成すると考えてください。

1
Michael Shaw