ブロック内の追加行とクリーンコード内の追加パラメーター


33

コンテキスト

きれいなコード、35ページ、それは言います

これは、ifステートメント、elseステートメント、whileステートメントなど内のブロックが1行の長さであることを意味します。おそらくその行は関数呼び出しであるべきです。これにより、囲んでいる関数が小さく保たれるだけでなく、ブロック内で呼び出される関数にわかりやすい名前を付けることができるため、ドキュメントの価値も高まります。

私は完全に同意します、それはとても理にかなっています。

後で40ページで、関数の引数について説明します

関数の理想的な引数の数はゼロ(niladic)です。次に1つ(単項)、2つ(2項)が続きます。可能な場合は、3つの引数(3項)を避ける必要があります。3つ以上(ポリアディック)には非常に特別な正当化が必要です。引数は難しいです。彼らは多くの概念的な力を取ります。

私は完全に同意します、それはとても理にかなっています。

問題

しかし、しばしば私は別のリストからリストを作成していることに気づき、2つの悪の1つと一緒に暮らさなければなりません。

ブロック内で2つの行使用します。1つは物を作成するため、もう1つは結果に追加するためです。

    public List<Flurp> CreateFlurps(List<BadaBoom> badaBooms)
    {
        List<Flurp> flurps = new List<Flurp>();
        foreach (BadaBoom badaBoom in badaBooms)
        {
            Flurp flurp = CreateFlurp(badaBoom);
            flurps.Add(flurp);
        }
        return flurps;
    }

または、事物が追加されるリストの関数に引数を追加し、「1つの引数が悪化する」ようにします。

    public List<Flurp> CreateFlurps(List<BadaBoom> badaBooms)
    {
        List<Flurp> flurps = new List<Flurp>();
        foreach (BadaBoom badaBoom in badaBooms)
        {
            CreateFlurpInList(badaBoom, flurps);
        }
        return flurps;
    }

質問

私が見ていない(不利な)利点はありますか?または、特定の状況でそのような利点があります。その場合、決定を下すときに何を探すべきですか?


58
何が問題なのflurps.Add(CreateFlurp(badaBoom));ですか?
cmaster

47
いいえ、それはただ一つの声明です。これは、単純にネストされた(単一のネストされたレベル)です。そして、もしf(g(x))あなたのスタイルガイドに対して単純なものがあれば、私はあなたのスタイルガイドを修正することはできません。つまり、sqrt(x*x + y*y)4行に分割しないでください。そして、それは、2つの(!)内側のネストレベル上の3つの(!)ネストされた部分式です(gasp!)。単一の演算子ステートメントではなく、読みやすさを目標にする必要があります。後で欲しい場合は、アセンブラーという完璧な言語があります。
cmaster

6
@cmaster x86アセンブリでさえ、厳密に単一演算子ステートメントを持ちません。メモリアドレス指定モードには多くの複雑な操作が含まれており、算術演算に使用できます。実際、x86 mov命令とjmp toStart最後に1つだけを使用してチューリング完全なコンピューターを作成できます。誰かが実際にそれを行うコンパイラを実際に作成しました:D
Luaan

5
@Luaan rlwimiPPC での悪名高い命令について語らないこと。(これはRotate Left Word Immediate Mask Insertの略です。)このコマンドは5つ以上のオペランド(2つのレジスターと3つのイミディエート値)を取り、次の操作を実行しました。他の2つの即値オペランドによって制御される1ビットの1回の実行で作成され、他のレジスタオペランドのそのマスクの1ビットに対応するビットは、ローテーションされたレジスタの対応するビットで置き換えられました。非常にクールな指示:-)
cmaster

7
@ R.Schmitz「私は汎用プログラミングを行っています」-実際、いや、そうではない、あなたは特定の目的のためにプログラミングをしている(私は何の目的わからないが、あなたがやると仮定しいる;-)。プログラミングには文字通り何千もの目的があり、それらの最適なコーディングスタイルはさまざまです。そのため、あなたに適したものは他の人には適さないかもしれませんし、逆もまた同様です:ここでのアドバイスは絶対的です(「常に Xを行い、Yは悪いです」"など)一部のドメインでは、これに固執することは完全に非現実的です。そのため、Clean Codeのような本のアドバイスは、常に(実用的な)塩分を
少々

回答:


104

これらのガイドラインは地図ではなくコンパスです。彼らはあなたを賢明な方向に向けます。しかし、どのソリューションが「最良」であるかを絶対的な言葉で説明することはできません。ある時点で、目的地に到着したため、コンパスが指している方向への歩行を停止する必要があります。

Clean Codeは、コードを非常に小さな明白なブロックに分割することをお勧めします。それは一般的に良い方向です。しかし(引用されたアドバイスの文字通りの解釈が示唆するように)極端に取られたとき、あなたはあなたのコードを無駄な小さな断片に細分化するでしょう。実際には何もしません。すべてが委任されます。これは、本質的に別の種類のコード難読化です。

「小さければ小さいほど良い」と「小さすぎるなら役に立たない」のバランスを取ることがあなたの仕事です。どのソリューションがより簡単かを自問してください。私にとって、それは明らかにリストを組み立てるため、明らかに最初のソリューションです。これはよく知られているイディオムです。さらに別の関数を見なくても、そのコードを理解できます。

もっとうまくやれるなら、「すべての要素をリストから別のリストに変換する」ことが一般的なパターンであり、機能的なmap()操作を使用して抽象化できることが多いことに注意してください。C#では、と呼ばれると思いますSelect。このようなもの:

public List<Flurp> CreateFlurps(List<BadaBoom> badaBooms)
{
    return badaBooms.Select(BadaBoom => CreateFlurp(badaBoom)).ToList();
}

7
コードはまだ間違っており、無意味に車輪を再発明します。CreateFlurps(someList)BCLがすでに提供しているのに、なぜ呼び出すのsomeList.ConvertAll(CreateFlurp)ですか?
ベンフォークト

44
@BenVoigtこれは設計レベルの質問です。特にホワイトボードにはコンパイラがないので正確な構文には関係ありません(そして、最後にC#を書いたのは2009年です)。私のポイントは「可能な限り最高のコードを見せた」ということではなく、「余談ですが、これはすでに解決済みの一般的なパターンです」。Linqはそれを行う1つの方法であり、別の方法で言及したConvertAllです。その代替案を提案していただきありがとうございます。
アモン

1
あなたの答えは賢明ですが、LINQがロジックを抽象化し、結局1行に文を減らすという事実は、あなたのアドバイスに矛盾しているようです。補足として、BadaBoom => CreateFlurp(badaBoom)冗長です。CreateFlurp関数として直接渡すことができます(Select(CreateFlurp))。(私の知る限り、これは常にそうでした。)
jpmc26

2
これにより、メソッドの必要性が完全になくなることに注意してください。名前CreateFlurpsは実際に見るだけではなく、より誤解を招きやすく理解しにくいbadaBooms.Select(CreateFlurp)です。後者は完全に宣言的です-分解する問題はないため、メソッドはまったく必要ありません。
カール・Leth

1
@ R.Schmitz 理解するのは難しくありませんが、理解するのは簡単ですbadaBooms.Select(CreateFlurp)。メソッドを作成して、その名前(高レベル)が実装(低レベル)を表すようにします。この場合、それらは同じレベルにあるため、何が起こっているのかを正確に調べるには、メソッドを(インラインで表示するのではなく)見なければなりません。 CreateFlurps(badaBooms)驚きを抱くことがbadaBooms.Select(CreateFlurp)できますが、できません。また、のList代わりにを誤って要求しているため、誤解を招きIEnumerableます。
カール・Leth

61

関数の理想的な引数の数はゼロ(niladic)です

いや!関数の理想的な引数の数は1です。ゼロの場合、アクションを実行するために関数が外部情報にアクセスする必要があることを保証しています。「おじ」ボブはこれを非常に間違っていました。

コードに関しては、最初の行にローカル変数を作成しているため、最初の例のブロックには2行しかありません。その割り当てを削除すると、次のクリーンコードガイドラインに準拠しています。

public List<Flurp> CreateFlurps(List<BadaBoom> badaBooms)
{
    List<Flurp> flurps = new List<Flurp>();
    foreach (BadaBoom badaBoom in badaBooms)
    {
        flurps.Add(CreateFlurp(badaBoom));
    }
    return flurps;
}

しかし、それは非常に長い(C#)コードです。次のようにしてください:

IEnumerable<Flurp> CreateFlurps(IEnumerable<BadaBoom> badaBooms) =>
    from badaBoom in babaBooms select CreateFlurp(badaBoom);

14
引数がゼロの関数は、オブジェクトが必要なデータをカプセル化することを意味するものであり、オブジェクトの外部のグローバルな状態に存在するものではありません。
リャサル

19
@ Ryathal、2つのポイント:(1)メソッドを話している場合、ほとんどの(すべて?)OO言語では、そのオブジェクトが最初のパラメーターとして推論されます(Pythonの場合は明示的に指定されます)。Java、C#などでは、すべてのメソッドは少なくとも1つのパラメーターを持つ関数です。コンパイラーは、その詳細をユーザーから隠すだけです。(2)「グローバル」とは決して言及しませんでした。オブジェクトの状態は、たとえばメソッドの外部にあります。
デビッドアルノ

17
ボブおじさんが「ゼロ」と書いたとき、彼は「ゼロ(これは数えない)」を意味したと確信しています。
Doc Brown

26
@DocBrownは、おそらく彼がオブジェクトの状態と機能を混在させることの大ファンなので、「関数」とは、おそらくメソッドを指します。そして、私はまだ彼に反対します。必要なものだけをメソッドに渡す方がはるかに良いのです。オブジェクトを探し回って、必要なものを取得するのではありません(つまり、古典的な「教えて、聞かないでください」)。
デビッドアルノ

8
@AlessandroTeruzzi、理想は1つのパラメーターです。ゼロは少なすぎます。これが、たとえば、関数型言語がカリー化の目的でパラメーターの数として1つを採用する理由です(実際、関数型言語によっては、すべての関数に1つのパラメーターがあります。パラメータがゼロのカレーは無意味です。「理想はできるだけ少ない、エルゴゼロが最善」と述べることは、不合理な不合理な例です。
デビッドアルノ

19

「クリーンコード」アドバイスは完全に間違っています。

ループで2行以上を使用します。関数内で同じ2行を非表示にするのは、説明が必要なランダムな数学の場合に意味がありますが、行がすでに記述的である場合は何もしません。「作成」および「追加」

2行目を避けるために2番目の引数を追加する必要はないため、2番目の方法は実際には意味がありません。

public List<Flurp> CreateFlurps(List<BadaBoom> badaBooms)
    {
        List<Flurp> flurps = new List<Flurp>();
        foreach (BadaBoom badaBoom in badaBooms)
        {
            flurps.Add(badaBoom .CreateFlurp());
            //or
            badaBoom.AddToListAsFlurp(flurps);
            //or
            flurps.Add(new Flurp(badaBoom));
            //or
            //make flurps a member of the class
            //use linq.Select()
            //etc
        }
        return flurps;
    }

または

foreach(var flurp in ConvertToFlurps(badaBooms))...

他の人が指摘しているように、最良の関数は引数のない関数であるというアドバイスは、せいぜいOOPに偏っており、最悪の場合には単純な悪いアドバイスです。


この答えを編集して、より明確にすることもできますか?私の質問は、Clean Codeのもとで、あるものが別のものを上回るかどうかでした。あなたは全体が間違っていると言い、それから私が与えた選択肢の一つを説明し続けます。現時点では、この答えは、実際に質問に答えようとするのではなく、アンチクリーンコードのアジェンダに従っているように見えます。
R.シュミッツ

申し訳ありませんが、最初の質問は「通常の」方法であると示唆していると解釈しましたが、2番目の質問にプッシュされていました。私は一般的にアンチクリーンコードではありませんが、この引用は明らかに間違っています
ユアン

19
@ R.Schmitz私は自分で "Clean Code"を読みましたが、その本が言っていることのほとんどに従います。ただし、完全な関数のサイズがほとんど1つのステートメントであるという点については、まったく間違っています。唯一の効果は、スパゲッティコードをライスコードに変換することです。読者は、一緒に見た場合にのみ意味のある意味を生成する些細な機能の多くで失われます。人間の作業メモリ容量は限られているため、ステートメントまたは関数でオーバーロードできます。読みやすくするには、この2つのバランスを取る必要があります。極端を避けてください!
cmaster

@cmasterそのコメントを書いたとき、答えは最初の2段落だけでした。今ではもっと良い答えです。
R.シュミッツ

7
率直に言って、私は短い答えを好みました。これらの答えのほとんどには外交的な話が多すぎます。引用されたアドバイスは明らかに間違っており、「それが実際に何を意味するのか」を推測したり、適切な解釈を試みるためにひねったりする必要はありません。
ユアン

15

CreateFlurpInListリストを受け入れ、そのリストを変更し、関数を純粋でなく推論するのが難しくなるため、2番目は間違いなく悪いです。メソッド名に、メソッドがリストに追加するだけであることを示唆するものはありません。

そして、私は3番目の、最高のオプションを提供します:

public List<Flurp> CreateFlurps(List<BadaBoom> badaBooms)
{
    return badaBooms.Select(CreateFlurp).ToList();
}

地獄、それが使用される場所が1つしかない場合、そのメソッドはすぐにインライン化できます。1ライナーはそれ自体で明確であるため、意味を与えるためにメソッドでカプセル化する必要はありません。


私はその方法について「純粋で推論するのが難しくない」(本当ではあるが)そんなに文句を言うつもりはないが、特別なケースを扱うための完全に不必要な方法であることについては文句を言わないだろう。スタンドアロンのFlurp、配列に追加されたFlurp、辞書、辞書で検索され、一致するFlurpが削除されたFlurpなどを作成する場合はどうなりますか?同じ引数を使用すると、Flurpコードにはこれらのメソッドもすべて必要になります。
gnasher729

10

引数が1つのバージョンの方が優れていますが、主に引数の数が原因ではありません。

それが優れている最も重要な理由は、カップリング低いことです。これにより、より便利で、推論が容易になり、テストが容易になり、コピー+貼り付けられたクローンになりにくくなります。

あなたが私を提供した場合はCreateFlurp(BadaBoom)シンプル:、私は、回収容器のいずれかのタイプのものを使用することができFlurp[]List<Flurp>LinkedList<Flurp>Dictionary<Key, Flurp>、など。しかし、CreateFlurpInList(BadaBoom, List<Flurp>)CreateFlurpInBindingList(BadaBoom, BindingList<Flurp>)使用して、リストが変更されたという通知を私のビューモデルが取得できるように、明日あなたに戻ってきます。うん!

追加の利点として、より単純な署名が既存のAPIに適合する可能性が高くなります。あなたは繰り返し問題があると言います

かなり頻繁に、私は自分が別のリストからリストを作成していることに気付く

使用可能なツールを使用するだけです。最も短く、最も効率的で、最高のバージョンは次のとおりです。

var Flurps = badaBooms.ConvertAll(CreateFlurp);

これにより、コードの記述とテストが減るだけでなくList<T>.ConvertAll()、結果が入力と同じ数のアイテムを持つことを認識し、結果リストを正しいサイズに事前に割り当てることができるため、高速です。コード(両方のバージョン)でリストを増やす必要がありました。


使用しないでくださいList.ConvertAll。オブジェクトの列挙可能オブジェクトをC#の異なるオブジェクトにマップする慣用的な方法はと呼ばれSelectます。ConvertAllここで利用できる唯一の理由は、OPがListメソッドで誤ってa を要求しているためIEnumerableです。
カール・Leth

6

全体的な目標を念頭に置いてください:コードを読みやすく、保守しやすくします。

多くの場合、複数の行を単一の意味のある関数にグループ化できます。これらの場合にそうしてください。場合によっては、一般的なアプローチを再検討する必要があります。

たとえば、あなたの場合、実装全体をvarに置き換えます

flups = badaBooms.Select(bb => new Flurp(bb));

可能性があります。または、あなたは次のようなことをするかもしれません

flups.Add(new Flurp(badaBoom))

場合によっては、最もクリーンで読みやすいソリューションが1行に収まらないことがあります。したがって、2行になります。何らかのarbitrary意的なルールを満たすためだけに、コードを理解しにくくすることはしないでください。

2番目の例は(私の意見では)最初の例よりも理解するのがかなり難しいです。2番目のパラメーターがあるだけでなく、パラメーターが関数によって変更されるということです。Clean Codeがそれについて言っていることを調べてください。(現在、本を手元に持っていませんが、基本的には「避けることができるなら、それをしないでください」と確信しています)。

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