多くの関数に分割されたプログラムで、次の場合に(もしあれば)望ましい場合、次々に自分自身を実行することを目的としています。
A)main()
?で次々と関数を実行します
または
B)main()
で1つの関数を実行し、その関数を実行する必要がある残りの関数にデイジーチェーンで接続しますか?
これを説明するには
プログラムのフローチャート:
Start -> Make foo -> Do bar? ->正しい-> Say Goodbye -> End
A:
void makeFoo() {
[...]
return;
}
bool doBar() {
bool bar;
[...]
return bar;
}
void sayGoodbye() {
[...]
return;
}
int main {
makeFoo();
if (doBar()) {
sayGoodbye();
}
return 0;
}
B:(デイジーチェーン)
void makeFoo() {
[...]
doBar();
return;
}
void doBar() {
bool bar;
[...]
if (bar) {
sayGoodbye();
}
return;
}
void sayGoodbye() {
[...]
return;
}
int main {
makeFoo();
return 0;
}
次の開発者がそのようにした理由が明らかな場合のデイジーチェーン
「コードは書かれているよりも頻繁に読み取られます。」 -Guido von Rossum、PEP 008
これに対する答えは1つではありませんが、おおまかな目安としてお答えします。関数の名前がその関数の1つの関数を実行して別の関数を呼び出すことを示している場合、それらをチェーンしても安全です。
2つの機能を実際に連鎖する必要がある場合があります。エラー処理の古典的な例を考えてみましょう
_result = doSomething();
if (result == badValue)
haltAndCatchFire();
_
このパターンが常に表示される場合、次の開発者は何度も何度もそれを読むでしょう。さらに悪いことに、それらは遅延したり忘れたりして、予期しない結果を引き起こす可能性があります。ただし、haltAndCatchFire
自体は非常に便利な関数であるため、その機能をdoSomething
に埋め込んで、二度と表示されないようにする必要はありません。解決策として:
_doSomethingOrHaltAndCatchFire();
_
長いですが、デイジーチェーンを使用する予定であることは明らかです。 doSomething
をフォローしたくない開発者は、通常、何か問題が発生した場合にhaltAndCatchFire
を呼び出すと想定できるため、次の開発者に情報を送信できます。彼らは複数の関数を回避して織り込む必要なしにmain()
を読み続けることができます。
名前の選択は、それが明確である限り、あなた次第です。例として、私が開発した1つのソフトウェアスイートで、パターンがありました。
_int getValue();
int tryGetValue();
_
何かがうまくいかない場合、getValue()
が例外処理関数を呼び出す必要があることが理解されました(社会契約)。 TryGetValueは単に戻る必要があります(多くの場合、問題が発生したことを知らせるダミー値を返します)。社会契約があまり明確でない他の状況では、getRegressionResultsForData()
のような関数があり、データを取得するためにいくつかの関数を実行してから、その関数に対してgetRegressionResults()
を呼び出しました。
要約すると、将来の開発者が(すべてのサブ関数に飛び込んでチェーンを探すのではなく)関数をスキミングすることで誤解を招く可能性がある状況では、チェーンしないでください。
ほとんどの場合、デイジーチェーンではありません。あなたの例を見て、フローチャートの利点なしに、バグを修正するためにこのコードを読んでいる将来のメンテナを想像してみてください。
関数名のダミーの例から見分けるのは難しいですが、実際のプログラムでは、main
関数を読み取って全体のフローを再構築し、関数の名前によって次にどこを見るべきかを伝えることができます。デイジーチェーンバージョンでは、複数のファイルにまたがる可能性のあるすべての機能を調べる必要があります。同様に、デイジーチェーンバージョンでは、1つのステップを変更すると、残りのコード全体に波及効果が生じる可能性があります。
ただし、すべてのステップをmain
関数に詰め込むことも、必ずしも答えであるとは限りません。読み取るすべてが同じ抽象化レベルにある関数を作成したいとします。おそらくmakeFoo
にはいくつかのサブステップがあります。それらをそのままにして、サブ結果をmain
に返すだけでかまいません。
メソッドは1つだけのことを行う必要があります。
たとえば、doBar
の一部に別れを告げていますか?
はいの場合、2番目のアプローチを使用する必要があります。
afterdoBar
でさようならを行う必要があるが、そうではないpartof doBar
の場合、最初のアプローチの方が優れています。
実例。アプリケーションの一部は、データを読み取って処理する必要があります。これは、次の5つの方法で行われます。
ConnectToDatabase()
接続を開き、データベースを照会できるようにします。
GetData()
有効なサニタイズされたデータをデータベースから取得します。
LoadData()
さらに処理するために、データベースからメモリにデータをロードします。
ValidateData()
データが有効であることを確認します。有効でない場合は例外をスローします。この状況は本当に例外的なものだからです。
SanitizeData()
処理するためにデータを変換します。非サニタイズされたデータはまだ有効ですが、処理の特定の制約に一致するようにサニタイズする必要があります。
ProcessData()
ダークマジックを使用してデータを処理します。
Output()
処理されたデータの結果を画面に表示します。結果には2つのグラフが含まれます。
GenerateCharts()
ユーザーに表示される最終的なチャートを生成します。
DisplayGlobalChart()
画面に最初のチャートを表示します。
DisplayPerMonthChart()
画面に2番目のグラフを表示します。
ConnectToDatabase()
、LoadData()
、SanitizeData()
はすべてGetData()
の一部であるように見えます。GetData()
を呼び出すと、データベースからロードされているか、フラットファイルからロードされているかは関係ありません。実際にどのようにロードされているか、サニタイズする必要があるかは問題ではありません。作業に使用できるデータセットが必要です。
チャートの生成と、チャートを表示する2つのメソッドがOutput()
に簡単に入るようにも思われます。ここでも、気にしませんhowOutput()
;を呼び出すと、情報が表示されます。あなたは情報がまだそこにないことを知っているだけです、そして今、それは画面上にあるはずです。
これまでのところ、呼び出しは次のとおりです。
_GetData()
{
ConnectToDatabase();
LoadData();
SanitizeData();
}
Output()
{
GenerateCharts();
DisplayGlobalChart();
DisplayPerMonthChart();
}
Main()
{
data = GetData();
result = ProcessData(data);
Output(result);
}
_
ValidateData()
のままです。これはトリッキーです。これはSanitizeData()
によって呼び出される可能性があります。情報の断片をサニタイズする前に、おそらく最初にそれを検証する必要があるため、それは理にかなっています。一方、これらのプロセスは十分に異なり、1つを他から独立して呼び出すことができる場合があります。この場合、ValidateData()
はGetData()
の直前のSanitizeData()
にあります。
絶対に絶対にしないでください。
2つの関数A()
およびB()
があるとします。 A()
がB()
を呼び出す必要がある場合、または機能しない場合は、A()
がB()
を呼び出す必要があります。そうでない場合、A()
はB()
の存在をまったく気にしないでください。
それ以外の場合は、次の2つのことを行います。
あなたはあなたのプログラムで起こっていることを難読化しています。 * Aを実行してからBを実行し、Bが成功してからCを実行するというフローがある場合、プログラムのどこかにこのプロセスを記述するコードがあるはずです。
A();
result = B();
if (result) then
C();
参照: カップリング(コンピュータプログラミング) Wikipedia
場合によります。通常、デイジーチェーンが実装の詳細であり、メインプロセスの一部ではない場合を除いて、機能をデイジーチェーンで接続しないでください。それが、結局main
関数を持っている理由です。他の誰か(6か月であなたを含む)がそのコードを読まなければならない場合、プログラムのアルゴリズムを記述する関数を見つけやすく、そのアルゴリズムのステップを実装する関数に分離されます。特にスクリプトでは、自己文書化コードを書く場合と同じように、手順をできるだけ明確に示す必要があります。
私は通常、次のような無効なプレースホルダーステートメントを多く含む中央関数を作成することから始めます。
def foo_the_bar(xkcd):
unfooed_bars = discover_bars(xkcd)
foo = load_foo_from_orbital_cannon()
return map(foo, unfooed_bars)
次に、完全なプログラムができるまで、これらの関数を実装します。ロジックを分離し、関数が何のために使用されるかを知らないようにします。
この方法で作成されたプログラムの動作を取得するには、内部の関数を詳しく調べる必要があります(ドキュメント化され、適切に名前が付けられている限り)。
デイジーチェーンを使用すると、次のような悪影響があります。
最後のポイントは重要です:人間にはコンテキストスタックがあり、ある意味ではコンピューターよりもはるかに制限されています。コンピューターは手順の組み合わせ全体を理解する必要がないため、現在の状態とその指示のみを気にします。
人間がデイジーコードを読み取るたびに、新しいコンテキストマネージャをブレインスタックに追加する必要があります。この操作にはしばらく時間がかかりますが、上位のコンテキストを参照する必要がある場合は、正しいコンテキストにポップして、元の場所に戻る必要があります。
簡単に言えば:デイジーチェーンを使用する場合は、関数をまったく使用せずにスクラップする可能性があります。