web-dev-qa-db-ja.com

読みやすさと保守性、ネストされた関数呼び出しを作成する特殊なケース

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

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#で作業しています。

57
Dominique

ワンライナーを拡張せざるを得ない場合

_ 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_のような意味のない名前でそれを行う場合、単に良い名前を考えることができないため、意味のない名前の混乱を避け、古き良き空白を使用して拡張することを私は本当に望んでいます:

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

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

  1. 濃い
  2. 一部のデバッガーは一度にすべてを強調表示するだけです
  3. 説明的な名前はありません
  4. わかりにくい名前でいっぱいです

あなたが良い名前を考えることができないとき、それはそれが得られるのと同じくらい良いです。

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

enter image description here

それだけでは不十分な場合は、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の comment の明度競争は、これらの関数が人工的なものであり、それ自体が非常に貧弱な名前であることを正しく指摘しています。そこで、このスタイルを実際のコードに適用する例を次に示します。

_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();
_

Wordの折り返しが必要ない場合でも、そのノイズのストリームを見るのは嫌いです。このスタイルでの表示は次のとおりです。

_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()
;
_

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

111
candied_orange

一方、1行に多くの処理を行うほど、1ページに表示されるロジックが多くなり、読みやすさが向上します。

私はこれに全く同意しません。 2つのコード例を見るだけで、これは正しくないことがわかります。

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

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

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

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

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

50
David Arno

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

2番目の例は、ばかげた程度に凝縮されています。関数に有用な名前が付いている場合は、多すぎるわけではないため問題なく読みやすくなる可能性がありますが、そのままでは、他の方向。

意味のある名前を導入した後、フォームの1つが自然に見えるかどうか、または黄金の真ん中であるかどうかを確認します。

読み取り可能なコードができたので、ほとんどのバグは明らかになり、他のバグは少なくともあなたから隠すのが難しくなります。

25
Deduplicator

いつものように、読みやすさに関しては失敗は極端です。あなたはany良いプログラミングのアドバイスを取り入れ、それを宗教的なルールに変え、それを使ってまったく読めないコードを生成することができます。 (私がこれを信じていない場合は、この2つをチェックしてください [〜#〜] ioccc [〜#〜] 勝者 borsanyi および goren =そして、コードをまったく読めなくするために関数をどのように使用するかを見てみましょう。ヒント:Borsanyiは1つの関数だけを使用します。

あなたのケースでは、2つの極端な方法は、1)単一の式ステートメントのみを使用すること、および2)すべてを大きく簡潔で複雑なステートメントに結合することです。どちらの方法を使用しても、コードが読みにくくなります。

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


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

_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()。それらについてもっと知っていれば、私は違う決断をするかもしれません。しかし、それがまさに問題です。ステートメントの推奨される複雑さは、コンテキストと関連する操作に強く依存します。そして、あなたはプログラマーとして、このコンテキストを見て、単一のステートメントに何を含めるかを決定する必要があります。読みやすさを重視する場合は、この責任を静的ルールに任せることはできません。

@Dominique、私はあなたの質問の分析で、「読みやすさ」と「保守性」が2つの別個のものであることを間違えていると思います。

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

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

しかし、関数呼び出しは決して入れ子にしてはならない、と言うまでも行きません。それは状況における判断の問題です。

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

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

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

  3. プログラマが持っている関数のネスト慣れ特定の方法で表現されているのを見るのは-おそらくそれが標準の実装を備えた標準の数学的手法または方程式を表しているため-読み、それを確認するのがより難しいかもしれません中間変数に分解されます。

  4. 単純な機能を実行し、既に読み取りが明確で、過度に分解されて霧化されている関数呼び出しの小さなネストできることまったく分解されていないものよりも読み取りが難しい。

14
Steve

どちらも最適ではありません。コメントを検討してください。

// 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);

クラッシュダンプの分析に関する懸念に関しては、通常、入力の検証がはるかに重要です。実際のクラッシュは、関数を呼び出す行ではなく、これらの関数内で発生する可能性が高く、そうでない場合でも、通常、正確な場所を通知する必要はありません。物事が爆発した。物事がバラバラになり始めた場所を知ることの方が、最終的にどこが壊れたかを知ることよりも重要です。これは、入力検証がキャッチするものです。

4
Peter

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

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

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を調べて破壊する可能性があります。

体系的に検出して適用できる厳格な規則がないため、優れたコード設計は非常に難しいと思います。

1
Ozymandias

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

私は可読性を80から90の間の保守性のどこかに置くと思います。残りの10〜20%は、リファクタリングにどれほど適しているかです。

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

関連するnotでは、ページ上のより多くのロジックは読みやすさを支援すると言います。不正解です。指標はページではなく、メソッドであり、メソッドに含まれるLESSロジックは読みやすくなります。

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

1
jmoreno

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
CoffeDeveloper