可読性と保守性、ネストされた関数呼び出しを記述する特別な場合


57

ネストされた関数呼び出しのコーディングスタイルは次のとおりです。

var result_h1 = H1(b1);
var result_h2 = H2(b2);
var result_g1 = G1(result_h1, result_h2);
var result_g2 = G2(c1);
var a = F(result_g1, result_g2);

私は最近、次のコーディングスタイルが非常に使用されている部門に変更しました。

var a = F(G1(H1(b1), H2(b2)), G2(c1));

コーディングの私の方法の結果、クラッシュする機能の場合、Visual Studioは対応するダンプを開き、問題が発生した行を示すことができます(特にアクセス違反が心配です)。

最初の方法でプログラムされた同じ問題によるクラッシュの場合、どの関数がクラッシュを引き起こしたかを知ることができないのではないかと心配しています。

一方、1行に追加する処理が多いほど、1ページに表示されるロジックが多くなり、読みやすくなります。

私の恐怖は正しいのですか、それとも一般的には商業環境で好まれる何かが欠けていますか?可読性または保守性?

関連するかどうかはわかりませんが、C ++(STL)/ C#で作業しています。


17
@gnat:一般的な質問を参照していますが、ネストされた関数呼び出しの言及されたケースとクラッシュダンプ分析の結果に特に興味がありますが、リンクのおかげで、かなり興味深い情報が含まれています。
ドミニク

9
この例では、C ++(あなたがこれはあなたのプロジェクトで使用されて言及される)に適用されるようにしているかのように、これは、スタイルの唯一の問題ではないことに注意してくださいオーダーの評価HXGX呼び出しのように、ワンライナーで変更される可能性関数の引数の評価の順序は指定されていません。何らかの理由で呼び出しの副作用の順序(故意または無意識のうち)に依存している場合、この「スタイルリファクタリング」は読みやすさ/保守以上の影響をもたらす可能性があります。
dfri

4
変数名は、result_g1実際に使用するものであるか、この値は実際に意味のある名前で何かを表しますか。例えばpercentageIncreasePerSecond。それは実際に2つの間を決定する私のテストになります
リチャードティングル

3
コーディングスタイルに対するあなたの気持ちに関係なく、明確に間違っていない限り、この慣習に従う必要があります(この場合はそうではないようです)。
n00b

4
@ t3chb0t自由に投票できますが、このサイトでトピックに関する質の高い有益な質問を奨励する(そして悪い質問を思いとどまらせる)ため、質問の賛成または反対の目的は質問が有用で明確であるかどうかを示すため、質問のコンテキストを支援するために投稿されたサンプルコードに批判を提供する手段として投票を使用するなど、他の理由で投票することは一般にサイトの品質を維持するのに役立ちません: softwareengineering.stackexchange.com/help/privileges/vote-down
ベンコットレル

回答:


111

次のような1つのライナーを拡張する必要があると感じた場合

 a = F(G1(H1(b1), H2(b2)), G2(c1));

私はあなたを責めません。これは読みにくいだけでなく、デバッグも困難です。

どうして?

  1. 濃い
  2. 一部のデバッガーは、一度にすべてを強調表示するだけです
  3. わかりやすい名前はありません

中間結果で展開すると、

 var result_h1 = H1(b1);
 var result_h2 = H2(b2);
 var result_g1 = G1(result_h1, result_h2);
 var result_g2 = G2(c1);
 var a = F(result_g1, result_g2);

まだ読みにくいです。どうして?2つの問題を解決し、4番目の問題を導入します。

  1. 濃い
  2. 一部のデバッガーは、一度にすべてを強調表示するだけです
  3. わかりやすい名前はありません
  4. わかりにくい名前が散らばっている

新しい、良い、セマンティックな意味を追加する名前でそれを拡張すると、さらに良くなります!良い名前は私が理解するのに役立ちます。

 var temperature = H1(b1);
 var humidity = H2(b2);
 var precipitation = G1(temperature, humidity);
 var dewPoint = G2(c1);
 var forecast = F(precipitation, dewPoint);

今、少なくともこれは物語を語っています。問題を修正し、ここで提供される他のどの製品よりも明らかに優れていますが、名前を考え出す必要があります。

のような無意味な名前でそれを行うならresult_this、そしてresult_thatあなたが単に良い名前を考えることができないので、私はあなたに私たちに無意味な名前の混乱をspareしみ、いくつかの古き良き空白を使用してそれを広げることを本当に望みます:

int a = 
    F(
        G1(
            H1(b1), 
            H2(b2)
        ), 
        G2(c1)
    )
;

意味のない結果名を持つものよりも、そうでないとしても同じくらい読みやすいです(これらの関数名がそれほど素晴らしいというわけではありません)。

  1. 濃い
  2. 一部のデバッガーは、一度にすべてを強調表示するだけです
  3. わかりやすい名前はありません
  4. わかりにくい名前が散らばっている

良い名前を思いつかないときは、それが得られるのと同じくらい良いです。

何らかの理由で、デバッガーは新しい行を好むので、これをデバッグすることは難しくないことがわかるはずです。

ここに画像の説明を入力してください

それだけでは十分でない場合G2()、複数の場所で呼ばれたと想像してください。

Exception in thread "main" java.lang.NullPointerException
    at composition.Example.G2(Example.java:34)
    at composition.Example.main(Example.java:18)

G2()呼び出しはそれぞれの行にあるため、このスタイルではmainの問題のある呼び出しに直接アクセスできるのは良いことだと思います。

そのため、問題1と2を問題4に固執する言い訳として使用しないでください。できないときは無意味な名前を避けてください。

Orbitのコメントの Lightness Racesは、これらの関数は人工的であり、名前自体が死んでいることを正しく指摘しています。そこで、このスタイルを野生のコードに適用する例を次に示します。

var user = db.t_ST_User.Where(_user => string.Compare(domain,  
_user.domainName.Trim(), StringComparison.OrdinalIgnoreCase) == 0)
.Where(_user => string.Compare(samAccountName, _user.samAccountName.Trim(), 
StringComparison.OrdinalIgnoreCase) == 0).Where(_user => _user.deleted == false)
.FirstOrDefault();

単語の折り返しが不要な場合でも、ノイズのストリームを見るのは嫌です。このスタイルでは、次のようになります。

var user = db
    .t_ST_User
    .Where(
        _user => string.Compare(
            domain, 
            _user.domainName.Trim(), 
            StringComparison.OrdinalIgnoreCase
        ) == 0
    )
    .Where(
        _user => string.Compare(
            samAccountName, 
            _user.samAccountName.Trim(), 
            StringComparison.OrdinalIgnoreCase
        ) == 0
    )
    .Where(_user => _user.deleted == false)
    .FirstOrDefault()
;

ご覧のとおり、このスタイルは、オブジェクト指向空間に移行しつつある機能的なコードとうまく機能することがわかりました。中間のスタイルでそれを行うために良い名前を思い付くことができれば、あなたにもっと力があります。それまで私はこれを使用しています。ただし、いずれにしても、意味のない結果名を避ける方法を見つけてください。彼らは私の目を傷つけます。


20
@スティーブと私はあなたに言ってはいけない。意味のある名前をお願いします。多くの場合、私は中間スタイルが無意識に行われるのを見てきました。悪い名前は、1行あたりのスパースコードよりもはるかに脳を燃やします。幅や長さを考慮して、コードを密にしたり、名前を短くしたりする動機はありません。私は彼らにもっと分解するように動機付けさせました。良い名前が発生しない場合は、意味のないノイズを避けるためにこの回避策を検討してください。
candied_orange

6
私はあなたの投稿に追加します:私は少し経験則を持っています:あなたがそれを命名することができないならば、それはそれがよく定義されていないというサインであるかもしれません。 エンティティ、プロパティ、変数、モジュール、メニュー、ヘルパークラス、メソッドなどで使用します。多くの状況で、この小さなルールが設計上の重大な欠陥を明らかにしました。ある意味で、適切な命名は読みやすさと保守性に貢献するだけでなく、設計の検証にも役立ちます。もちろん、すべての単純なルールには例外があります。
アリレザ

4
拡張バージョンは見苦しいです。そこには 余白が多すぎて、それで有効化されたすべての項目を効果的に削減します。
Mateen Ulhaq

5
@MateenUlhaq余分な空白は、改行とインデントがいくつかあるだけで、意味のある境界にすべて注意深く配置されています。代わりに、コメントは意味のない境界に空白を配置します。少し近づき、よりオープンな外観にすることをお勧めします。
jpmc26

3
@MateenUlhaqとは異なり、この特定の例では、そのような関数名を使用して空白を囲んでいますが、実際の関数名(長さが2文字を超えていますか?)
モニカとの軽さレース、

50

一方、1行に追加する処理が多いほど、1ページに表示されるロジックが多くなり、読みやすくなります。

私はこれに全く反対します。あなたの2つのコード例を見るだけで、これは間違っていると呼ばれます:

var a = F(G1(H1(b1), H2(b2)), G2(c1));

読むように聞こえます。「読みやすさ」は情報密度を意味するものではありません。「読みやすく、理解しやすく、維持しやすい」という意味です。

場合によっては、コードは単純で、1行を使用するのが理にかなっています。また、そうすることで読みにくくなり、1行に詰め込む以上の明らかな利点はありません。

ただし、「クラッシュを診断しやすい」とは、コードの保守が容易であることを意味すると主張することもできます。クラッシュしないコードは保守がはるかに簡単です。「保守が容易」は、主に読みやすく理解しやすいコードを介して実現され、自動化された一連のテストでバックアップされています。

したがって、コードが頻繁にクラッシュし、より良いデバッグ情報が必要なために、単一の式を多くの変数を持つ複数行の式に変える場合は、それをやめ、代わりにコードをより堅牢にします。デバッグが簡単なコードよりも、デバッグを必要としないコードを書くことをお勧めします。


37
F(G1(H1(b1), H2(b2)), G2(c1))これは読みにくいと思いますが、これはぎゅうぎゅう詰めになっていることとは何の関係もありません。(それを言うつもりであるかどうかはわかりませんが、このように解釈できます。)特にいくつかの関数が単純な中置演算子である場合、1行に3つまたは4つの関数をネストすることは完全に読みやすくなります。ここで問題となるのは説明のない名前ですが、この問題は複数行バージョンではさらに悪化し、さらに説明のない名前が導入されます。定型文のみを追加しても、読みやすさはほとんど向上しません。
左辺約

23
@leftaroundabout:私にとって、難易度はG13つのパラメーターを取るのか2つだけを取るのかが明らかでG2はなく、のもう1つのパラメーターであるということFです。括弧を細かく数えなければなりません。
マシューM.

4
なつみ これは問題になる可能性がありますが、関数がよく知られている場合、どれだけの引数を取るかは明らかです。具体的には、前に述べたように、中置関数については、2つの引数を取ることがすぐにわかります。(また、ほとんどの言語が使用する括弧付きタプル構文は、この問題を悪化させます。カリー化を好む言語では、それは自動的により明確になりますF (G1 (H1 b1) (H2 b2)) (G2 c1)。)
leftaroundabout

5
個人的には、以前のコメントのようにスタイリングがある限り、よりコンパクトなフォームを好みます。これは、精神的に追跡する状態が少ないことを保証するためです- result_h1存在しない場合は再利用できず、4つの変数間の配管は明らか。
イズカタ

8
一般的にデバッグしやすいコードは、デバッグを必要としないコードであることがわかりました。
ロブK

25

最初の例であるsingle-assignment-formは、選択した名前がまったく意味をなさないため判読できません。それはあなたの側の内部情報を開示しないようにすることの人工物であるかもしれません、本当のコードはその点でうまくいくかもしれません、我々は言うことができません。とにかく、情報密度が非常に低いために時間がかかり、一般に理解しにくい。

2番目の例は、ばかげた程度に凝縮されています。関数に便利な名前が付いていれば、あまり多くはないので問題なく読みやすいかもしれませんが、そのままでは他の方向では混乱します。

意味のある名前を紹介した後、フォームの1つが自然に見えるかどうか、または撮影する黄金色の中間点があるかどうかを確認します。

読みやすいコードができたので、ほとんどのバグは明白になり、他のバグは少なくともあなたから隠れることが難しくなります。


17

いつものように、読みやすさに関して失敗は極端です。あなたは取ることができます任意の、良いプログラミングのアドバイスを宗教的なルールにそれを回すと、全く読めないコードを生成するためにそれを使用します。(あなたは、これら二つのチェックアウトし、この上で私を信じていない場合はIOCCCの受賞者のborsanyigorenを、彼らは全く読めないコードを描画するための関数を使用する方法異なっを見てみましょうヒント:。Borsanyiはあまりgoren、正確に一つの関数を使用して、はるかに...)

あなたの場合、2つの極端な例は、1)単一の式ステートメントのみを使用すること、2)すべてを大きく、簡潔で複雑なステートメントに結合することです。どちらのアプローチも極端な場合、コードが読めなくなります。

プログラマーとしてのあなたの仕事は、バランスを取ることです。あなたが書くすべてのステートメントについて、「このステートメントは理解しやすいのですか、それは私の関数を読みやすくするのに役立ちますか?」という質問に答えるのがあなたの仕事です。


要点は、単一のステートメントに何を含めるのがよいかを決定できる単一の測定可能なステートメントの複雑さはないということです。たとえば、次の行をご覧ください。

double d = sqrt(square(x1 - x0) + square(y1 - y0));

これは非常に複雑なステートメントですが、彼らの塩に値するプログラマーは、これが何をするかをすぐに把握できるはずです。これは非常によく知られているパターンです。そのため、同等のものよりもはるかに読みやすい

double dx = x1 - x0;
double dy = y1 - y0;
double dxSquare = square(dx);
double dySquare = square(dy);
double dSquare = dxSquare + dySquare;
double d = sqrt(dSquare);

これは、よく知られているパターンを、一見無意味な数の単純なステップに分割します。しかし、あなたの質問からの声明

var a = F(G1(H1(b1), H2(b2)), G2(c1));

距離の計算よりも1回少ない操作であるにもかかわらず、私には過度に複雑に思えます。もちろん、それはについて何も知らない私の直接の結果だF()G1()G2()H1()、またはH2()。それらについてもっと知っていれば、私は異なる決定をするかもしれません。しかし、まさにそれが問題です。ステートメントの望ましい複雑さは、コンテキストと関連する操作に大きく依存します。そして、プログラマーとして、あなたはこのコンテキストを見て、単一のステートメントに何を含めるかを決定しなければなりません。読みやすさを重視する場合、この責任を静的ルールにオフロードすることはできません。


14

@ドミニク、あなたの質問の分析では、あなたは「読みやすさ」と「維持性」が2つの別々のことであるという間違いを犯していると思います。

保守可能であるが読み取り不可能なコードを使用することは可能ですか?逆に、コードが非常に読みやすい場合、なぜ読みやすいためにメンテナンスできなくなるのでしょうか?私は、これらの要素を互いに戦わせ、どちらかを選択しなければならないプログラマーについて聞いたことがありません!

ネストされた関数呼び出しに中間変数を使用するかどうかの決定に関して、指定された3つの変数、5つの別個の関数の呼び出し、ネストされた3つの深さの呼び出しの場合、少なくともいくつかの中間変数を使用してそれを分解する傾向がありますが、あなたがやったように。

しかし、私は確かに、関数呼び出しをまったくネストしてはいけないとは言いません。それは状況における判断の問題です。

以下の点が判決に関係していると思います。

  1. 呼び出された関数が標準の数学演算を表す場合、結果が予測不可能であり、読者が必ずしも精神的に評価できない不明瞭なドメインロジックを表す関数よりも、ネストすることができます。

  2. 単一のパラメーターを持つ関数は、複数のパラメーターを持つ関数よりも(内部関数または外部関数として)ネストに参加できます。異なるネストレベルで異なるアリティの関数を混合すると、コードが豚の耳のように見える傾向があります。

  3. プログラマーが特定の方法で表現されるのに慣れている関数のネスト-おそらく標準的な実装を持つ標準的な数学的手法または方程式を表すため-中間変数に分解されているかどうかを確認および検証するのがより困難な場合があります

  4. 単純な機能を実行し、読み取ることが既に明らかであり、関数呼び出しの小さな巣次いで過剰分解および噴霧され、可能な全く破壊されなかったものより読みにくく。


3
+1「保守可能であるが読み取り不可能なコードを使用することは可能ですか?」それも私の最初の考えでした。
ロンジョン

4

両方とも準最適です。コメントを検討してください。

// Calculating torque according to Newton/Dominique, 4th ed. pg 235
var a = F(G1(H1(b1), H2(b2)), G2(c1));

または、一般的な機能ではなく特定の機能:

var a = Torque_NewtonDominique(b1,b2,c1);

どの結果を綴るかを決定するときは、ステートメントごとにコスト(コピーと参照、l値とr値)、可読性、およびリスクを個別に念頭に置いてください。

たとえば、単純なユニット/タイプの変換を独自の行に移動することによる付加価値はありません。これらは読みやすく、失敗する可能性が非常に低いためです。

var radians = ExtractAngle(c1.Normalize())
var a = Torque(b1.ToNewton(),b2.ToMeters(),radians);

クラッシュダンプを分析する心配については、通常、入力の検証がはるかに重要です。実際のクラッシュは、これらの関数を呼び出す行ではなく、これらの関数内で発生する可能性が非常に高いです。物事は爆発しました。物事がどこでバラバラになり始めたかを知ることは、それらが最終的にどこで爆発したかを知ることよりもはるかに重要です。


引数を渡すコストについて:最適化には2つのルールがあります。1)しないでください。2)(専門家のみ)まだしないでください。
ラバーダック

1

可読性は保守性の主要な部分です。私を疑いますか?知らない言語(おそらくプログラミング言語とプログラマーの両方の言語)で大規模なプロジェクトを選択し、それをリファクタリングする方法を確認してください...

可読性は、保守性の80から90の間のどこかに置きます。他の10〜20パーセントは、リファクタリングの影響を受けやすいかどうかです。

つまり、2つの変数を最終関数に効率的に渡します(F)。これらの2つの変数は、他の3つの変数を使用して作成されます。Fが既に存在する場合は、b1、b2、c1をFに渡して、Fの合成を実行して結果を返すDを作成した方がよいでしょう。その時点で、Dに良い名前を付けるだけで、どのスタイルを使用するかは関係ありません。

関連するnotでは、ページ上のロジックが増えると読みやすくなります。それは誤りです。メトリックはページではなく、メソッドであり、メソッドに含まれるLESSロジックは、読みやすくなります。

読み取り可能とは、プログラマが頭の中でロジック(入力、出力、アルゴリズム)を保持できることを意味します。それが多ければ多いほど、プログラマはそれを理解できます。循環的複雑さについて読んでください。


1
あなたが読みやすさについて言うすべてに同意します。しかし、私は別の方法に論理演算を割れ、ことを同意しない必ずしも別々の行にそれをクラックよりそれがより読みやすくなります(両方の技術ができ、使い古された、シンプルなロジックが少なく読みやすくして、プログラム全体がもっと雑然とします) -メソッドに深く入り込みすぎると、アセンブリ言語マクロをエミュレートすることになり、それらが全体としてどのように統合されるかを見失います。また、この別の方法では、同じ問題に直面することになります。呼び出しをネストするか、中間変数に分解します。
スティーブ

@Steve:常にそれを行うとは言いませんでしたが、単一の値を取得するために5行を使用することを考えている場合、関数がより良い可能性があります。複数行と複雑な行については、それが適切な名前の関数である場合、両方とも同等に機能します。
-jmoreno

1

C#またはC ++を使用しているかどうかにかかわらず、デバッグビルドを使用している限り、可能な解決策は関数をラップすることです。

var a = F(G1(H1(b1), H2(b2)), G2(c1));

1行の式を記述しても、スタックトレースを調べるだけで問題の場所を指摘できます。

returnType F( params)
{
    returnType RealF( params);
}

もちろん、同じ行で同じ関数を複数回呼び出した場合、どの関数かはわかりませんが、それでも識別できます:

  • 関数パラメーターを見る
  • パラメーターが同一で、関数に副作用がない場合、2つの同一の呼び出しは2つの同一の呼び出しなどになります。

これは特効薬ではありませんが、それほど悪くはありません。

関数のラッピンググループは、コードを読みやすくするためにさらに有益になることは言うまでもありません。

type CallingGBecauseFTheorem( T b1, C b2)
{
     return G1( H1( b1), H2( b2));
}

var a = F( CallingGBecauseFTheorem( b1,b2), G2( c1));

1

私の意見では、言語に関係なく、自己文書化コードは保守性と可読性の両方に優れています。

上記のステートメントは密ですが、「自己文書化」:

double d = sqrt(square(x1 - x0) + square(y1 - y0));

ステージに分割すると(テストが容易になります)、上記のすべてのコンテキストが失われます:

double dx = x1 - x0;
double dy = y1 - y0;
double dxSquare = square(dx);
double dySquare = square(dy);
double dSquare = dxSquare + dySquare;
double d = sqrt(dSquare);

そして明らかに、目的を明確に示す変数名と関数名を使用することは非常に貴重です。

「if」ブロックでさえ、自己文書化では良い場合も悪い場合もあります。最初の2つの条件で3番目の条件を簡単にテストすることは簡単にできないため、これは悪いことです...すべては無関係です。

if (Bill is the boss) && (i == 3) && (the carnival is next weekend)

これは、より「集合的」な意味を持ち、テスト条件を作成するのが簡単です。

if (iRowCount == 2) || (iRowCount == 50) || (iRowCount > 100)

そして、このステートメントは、自己文書化の観点から見た、単なるランダムな文字列です。

var a = F(G1(H1(b1), H2(b2)), G2(c1));

上記のステートメントを見ると、関数H1とH2の両方が単一の「H」関数に統合されるのではなく同じ「システム状態変数」を変更する場合、保守性は依然として大きな課題です。 H2を見るための関数で、H2を壊す可能性があります。

体系的に検出および実施できる厳格なルールはないため、優れたコード設計は非常に難しいと思います。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.