web-dev-qa-db-ja.com

矢じりのアンチパターンを防ぐ方法

コードをより読みやすいものにリファクタリングする方法について少し混乱しています。

このコードについて考えてみましょう。

var foo = getfoo();
if(foo!=null)
{
    var bar = getbar(foo);
    if(bar!=null)
    {
        var moo = getmoo(bar);
        if(moo!=null)
        {
            var cow = getcow(moo);
            ...
        }
    }
}
return;

ご覧のとおり、ネストされたifブロックが多数必要です。これは、ネストされたifがそれぞれ前の値に依存しているためです。

今、私はこの点で私のコードを少しきれいにする方法を考えていました。

私が自分で考えたいくつかのオプションは次のとおりです。

  • 各if句の後に戻る、つまり、メソッドを離れる場所が複数あることを意味します
  • ArgumentNullExceptionsをスローします。その後、最後にそれらをキャッチし、returnステートメントをfinally句に(またはtry/catchブロックの外に)配置します。
  • ラベルとgoto:

これらのオプションのほとんどは私には少し「汚い」ように見えるので、私が作成したこの混乱をクリーンアップするための良い方法があるかどうか疑問に思いました。

51
Kippie

複数のreturnステートメントを使用します。これにより、コードが読みやすく、理解しやすくなります。

明らかな理由でgotoを使用しないでください。

実行しているチェックは例外ではないため、例外を使用しないでください。これは期待できることなので、それを考慮に入れる必要があります。例外に対するプログラミングもアンチパターンです。

45
Gerrie Schenck

Nullチェックを次のように反転することを検討してください。

var foo = getfoo();
if (foo == null)
{
    return;
}
var bar = getbar(foo);
if (bar == null)
{
    return;
}
...etc
44
rexcfnghk

式を連鎖させることができます。割り当ては割り当てられた値を返すため、その結果を確認できます。また、割り当てられた変数を次の式で使用できます。

式がfalseを返すとすぐに、式全体がすでにfalseを返すため(and操作のため)、他の式は実行されなくなります。

したがって、次のようなものが機能するはずです。

Foo foo; Bar bar; Moo moo; Cow cow;

if( (foo = getfoo()) != null &&
    (bar = getbar(foo)) != null &&
    (moo = getmoo(bar)) != null &&
    (cow = getcow(moo)) != null )
{
  ..
}
27
GolezTrol

これは、gotoを使用することが完全に許容される(望ましくない場合)数少ないシナリオの1つです。

このような関数では、多くの場合、割り当てられたリソースや途中で行われた状態の変更があり、関数が終了する前に元に戻す必要があります。

リターンベースのソリューション(rexcfnghkやGerrie Schenckなど)の通常の問題は、every return。これはコードの重複につながり、特に大きな関数では微妙なエラーへの扉を開きます。 これをしないでください。


CERT 実際に推奨gotoに基づく構造的アプローチ。

特に、Linuxカーネルのcopy_processkernel/fork.cから取得したサンプルコードに注意してください。概念の簡略版は次のとおりです。

    if (!modify_state1(true))
        goto cleanup_none;
    if (!modify_state2(true))
        goto cleanup_state1;
    if (!modify_state3(true))
        goto cleanup_state2;

    // ...

cleanup_state3:
    modify_state3(false);
cleanup_state2:
    modify_state2(false);
cleanup_state1:
    modify_state1(false);
cleanup_none:
    return;

基本的に、これは「矢印」コードのより読みやすいバージョンであり、不要なインデントや重複コードを使用していません。この概念は、状況に最も適したものに簡単に拡張できます。


最後に、特にCERTの最初の準拠例に関して、クリーンアップを一度に処理できるように、可能な限りコードを設計する方が簡単であることを追加したいと思います。そうすれば、次のようなコードを書くことができます。

    FILE *f1 = null;
    FILE *f2 = null;
    void *mem = null;

    if ((f1 = fopen(FILE1, "r")) == null)
        goto cleanup;
    if ((f2 = fopen(FILE2, "r")) == null)
        goto cleanup;
    if ((mem = malloc(OBJSIZE)) == null)
        goto cleanup;

    // ...

cleanup:
    free(mem); // These functions gracefully exit given null input
    close(f2);
    close(f1);
    return;
25
user1354557

まず、あなたの提案(各if句の後に戻る)は非常に良い方法です:

  // Contract (first check all the input)
  var foo = getfoo();

  if (Object.ReferenceEquals(null, foo))
    return; // <- Or throw exception, put assert etc.

  var bar = getbar(foo);

  if (Object.ReferenceEquals(null, bar))
    return; // <- Or throw exception, put assert etc.

  var moo = getmoo(bar);

  if (Object.ReferenceEquals(null, moo))
    return; // <- Or throw exception, put assert etc.

  // Routine: all instances (foo, bar, moo) are correct (not null) and we can work with them
  ...

2番目の可能性(あなたの場合)は、getbar()関数とgetmoo()関数をわずかに変更して、null入力でnullを返すようにすることです。

  var foo = getfoo();
  var bar = getbar(foo); // return null if foo is null
  var moo = getmoo(bar); // return null if bar is null

  if ((foo == null) || (bar == null) || (moo == null))
    return; // <- Or throw exception, put assert(s) etc.    

  // Routine: all instances (foo, bar, moo) are correct (not null)
  ...

3番目の可能性は、複雑なケースではNull Object DesingPatterenを使用できることです。

http://en.wikipedia.org/wiki/Null_Object_pattern

16
Dmitry Bychenko

昔ながらのやり方:

var foo;
var bar;
var moo;
var cow;
var failed = false;
failed = failed || (foo = getfoo()) == null;
failed = failed || (bar = getbar(foo)) == null;
failed = failed || (moo = getmoo(bar)) == null;
failed = failed || (cow = getcow(moo)) == null;

はるかに明確で、矢印がなく、永久に拡張可能です。

Dark Sideに移動してgotoまたはreturnを使用しないでください。

11
OldCurmudgeon
var foo =                        getFoo();
var bar = (foo == null) ? null : getBar(foo);
var moo = (bar == null) ? null : getMoo(bar);
var cow = (moo == null) ? null : getCow(moo);
if (cow != null) {
  ...
}
5
Rex Kerr

呼び出しているものを変更できる場合は、nullを返さないように変更できますが、代わりに NULL-Object です。

これにより、すべてのifを完全に失うことができます。

3
Jens Schauder

レックス・カーの答えは確かにとてもいいです。
ただし、コードを変更できる場合は、Jens Schauderの答えの方がおそらく良いでしょう(Nullオブジェクトパターン)

例をより具体的にすることができれば、おそらくさらに多くの答えを得ることができます
たとえば、メソッドの「場所」に応じて、次のようになります。

namespace ConsoleApplication8
{
    using MyLibrary;
    using static MyLibrary.MyHelpers;

    class Foo { }
    class Bar { }
    class Moo { }
    class Cow { }


    internal class Program
    {
        private static void Main(string[] args)
        {
            var cow = getfoo()?.getbar()?.getmoo()?.getcow();
        }
    }
}

namespace MyLibrary
{
    using ConsoleApplication8;
    static class MyExtensions
    {
        public static Cow getcow(this Moo moo) => null;
        public static Moo getmoo(this Bar bar) => null;
        public static Bar getbar(this Foo foo) => null;
    }

    static class MyHelpers
    {
        public static Foo getfoo() => null;
    }
}
1
George Vovos

別の方法は、プログラムフローを制御するために「偽の」単一ループを使用することです。私はそれをお勧めするとは言えませんが、矢じりよりも見た目が良く、読みやすいです。

その変数のような「ステージ」、「フェーズ」、またはsthを追加すると、デバッグやエラー処理が簡素化される場合があります。

int stage = 0;
do { // for break only, possibly with no indent

var foo = getfoo();
if(foo==null) break;

stage = 1;
var bar = getbar(foo);
if(bar==null) break;

stage = 2;
var moo = getmoo(bar);
if(moo==null) break;

stage = 3;
var cow = getcow(moo);

return 0; // end of non-erroreous program flow
}  while (0); // make sure to leave an appropriate comment about the "fake" while

// free resources if necessary
// leave an error message
ERR("error during stage %d", stage);

//return a proper error (based on stage?)
return ERROR;
1
Dariusz
try
{
  if (getcow(getmoo(getbar(getfoo()))) == null)
  {
    throw new NullPointerException();
  }
catch(NullPointerException ex)
{
  return; //or whatever you want to do when something is null
}

//... rest of the method

これにより、メソッドのメインロジックが整理され、例外的なリターンが1つだけ発生します。その欠点は、get *メソッドが遅いと遅くなる可能性があることと、どのメソッドがnull値を返したかをデバッガーで判断するのが難しいことです。

1
MikeFHay

奇妙なことに、メソッドチェーンについては誰も言及していません。

メソッドチェーンクラスを一度作成した場合

Public Class Chainer(Of R)

    Public ReadOnly Result As R

    Private Sub New(Result As R)
        Me.Result = Result
    End Sub

    Public Shared Function Create() As Chainer(Of R)
        Return New Chainer(Of R)(Nothing)
    End Function

    Public Function Chain(Of S)(Method As Func(Of S)) As Chainer(Of S)
        Return New Chainer(Of S)(Method())
    End Function

    Public Function Chain(Of S)(Method As Func(Of R, S)) As Chainer(Of S)
        Return New Chainer(Of S)(If(Result Is Nothing, Nothing, Method(Result)))
    End Function
End Class

どこでも使用して、任意の数の関数を実行シーケンスに構成し、結果または何もない(Null)を生成できます。

Dim Cow = Chainer(Of Object).Create.
    Chain(Function() GetFoo()).
    Chain(Function(Foo) GetBar(Foo)).
    Chain(Function(Bar) GetMoo(Bar)).
    Chain(Function(Moo) GetCow(Moo)).
    Result
0
Lightman