web-dev-qa-db-ja.com

ループのリファクタリング(クリーンなコードの側面)

「for」ループをリファクタリングして、配列を反復処理し、配列要素で2つの独立した処理を実行する必要があります。例えば:

doThisOrOtherStuff(array) {
 for(int i=0; i<array.length; i++) {
  element = array[i];
  id = element.getId();
  if(id < 0) {
   doThisStuff(element);
  } else {
   doOtherStuff(element);
  }
 }
}

私の問題は、たった1つのことしかできない機能が欲しいということです。現在、私の機能は「if」での決定に基づいて2つのことを行います。 2つの関数に分割すると、配列を2回反復処理する必要があります。というのは:

doThisStuff(array) {
 for(int i=0; i<array.length; i++) {
  element = array[i];
  id = element.getId();
  if(id < 0) {
   doThisStuff(element);
  } 
 }
}

doOtherStuff(array) {
 for(int i=0; i<array.length; i++) {
  element = array[i];
  id = element.getId();
  if(id >= 0) {
   doOtherStuff(element);
  } 
 }
}

これは、パフォーマンスの観点からは最適ではありません。誰かが私に助言を与えることができますか、そのような場合に異なる機能の異なる責任をどのように分けるか?

3
tenkmilan

この関数を想像してください:

function updateOrCreateAll(array) {
  for (var i = 0; i < array.length; i++) {
    var element = array[i];
    var id = element.getId();
    if (id < 0) {
      create(element);
    }
    else {
      update(element);
    }
  }
}

この関数には2つの役割があります。1つ目は配列を反復処理すること、2つ目は作成するか更新するかを選択することです。この関数をリファクタリングして、責任を1つだけにする:

function updateOrCreateAll(array) {
  for (var i = 0; i < array.length; i++) {
    updateOrCreate(array[i]);
  }
}

function updateOrCreate(element) {
  return element.getId() < 0 ? create(element) : update(element);
}

これでupdateOrCreateAllは、配列のすべての要素にupdateOrCreateを適用するという1つの責任を負います。条件はupdateOrCreateの責任であり、create/updateの責任も1つだけです。

3

もちろん、パフォーマンスの観点からは最適ではありません。非常に少数の最適なソリューションがあり、たとえそれらがたとえそうであっても、それを証明するのは難しいでしょう。

しかし、クリーンなコードは、単一の次元でのoptimalの評価ではありません。それは常にトレードオフについてです:たとえば、このコードの実行に費やすことができる時間と書き込みにかかる時間対(これは非常に重要です!)コードを変更する必要がある場合、コードを理解するのにどのくらいの時間がかかりますか?

データセットに対する2回目の反復を排除することによる最小限の利益が、このコードを処理するときに同時に2つの異なることに注意を払わなければならないコストを上回ることはほとんどありません(そして、パフォーマンスの向上は常に測定されるべきであり、決して想定されません! )。正しいことは、責任を分離することです。余分な速度が必要であることが判明した場合、1か所で速度を簡単にリファクタリングできますが、時期尚早の最適化の結果を簡単に取り消すことはできません。

6
Kilian Foth

呼び出している関数がelementで何かをしているので、関数を要素に移動できますか?

次に、ifステートメントを削除してelement->doSomething()を呼び出すだけで、コードを簡略化できます

これでできることはたくさんあります。ソース: https://refactoring.guru/refactoring/techniques/simplifying-conditional-expressions

2

おそらく、いくつかの条件に基づいて最初に配列を分割できるため、2つの配列があります。次に、それらのそれぞれを反復します。ifは必要ありません。条件はすでにそれぞれに当てはまるためです。

PHPではそれはそのようになるでしょう:

function split(array $a, Closure $c)
{
    $arrayWhereConditionHoldsTrue = [];
    $arrayWhereConditionDoesNotHoldTrue = [];

    foreach ($a as $e) {
        if ($c($e)) {
            $arrayWhereConditionHoldsTrue[] = $e;
        } else {
            $arrayWhereConditionDoesNotHoldTrue[] = $e;
        }
    }

    return [$arrayWhereConditionHoldsTrue, $arrayWhereConditionDoesNotHoldTrue];
}

$isPositive =
    function ($n) {
        return $n > 0;
    };

$arrays =
    split(
        [1, -2, 3, -4, 5, -6],
        $isPositive
    )
;

$positive = $arrays[0];
$negative = $arrays[1];

print_r($positive);
print_r($negative);

function doStuffWithPositiveNumbers(array $positive)
{

}

function doStuffWithNegativeNumbers(array $negative)
{

}

しかし、オブジェクトについて話しているのであれば、私はRobert Andrzejukと一緒です。配列内のすべての要素に準拠するインターフェースを導入し、その多態性メソッドを使用できます。

interface Number
{
    public function doSmth();
}

class Positive implements Number
{
    public function doSmth()
    {
    }
}

class Negative implements Number
{
    public function doSmth()
    {
    }
}

function doStuff(array $numbers) {
    foreach ($numbers as $number) {
        $number->doSmth();
    }
}
2
Zapadlo

ポリモーフィズムを使用して条件を削除し、ループするだけです。

class DoThis : Base 
{
     public void Process() {} // DoThis logic 
}

class DoThat : Base 
{
     public void Process() {} // DoThat logic 
}


void ProcessItems(List<Base> items) 
{
     foreach(var i in items)
     {
          i.Process();
     } 
}

あなたのコードはよりクリーンで効率的です

1
Ewan

個人的には、最初のベスト、肩をすくめるような、ループ内のidで単純な分岐を行うだけです。通常、ポリモーフィズムはif/switchを使用した条件付き分岐の直接的な代替手段ですが、要素IDが動的で変化する場合は扱いにくい場合があります。

ちょっと奇抜なことに、この質問は言語にとらわれないようなので、ここに行きます:

// Applies 'true_func' to elements in the the random-access sequence
// that satisfies 'pred' or 'false_func' otherwise.
applyIf(array, pred, true_func, false_func)
{
    for (int i=0; i < array.length; i++) {
        if (pred(array[i]))
            true_func(array[i]);
        else
            false_func(array[i]);
    }
}

// Returns true if the element has a negative ID.
hasNegativeId(element) {
    return element.getId() < 0;
}

そして、呼び出し元で:

applyIf(array, hasNegativeId, doThisStuff, DoOtherStuff);

または単に:

applyIf(array, function(element) {element.getId() < 0;}, doThisStuff, DoOtherStuff);

私は実際にはこのソリューションが好きではありません。私はそれさえ嫌いかもしれません。ただし、単純な関数をさらに2つの単純な関数に分割します。要素に負のIDがある場合にtrueを返す1つの超単純な単項述語と、1つの関数を呼び出す非常に一般的に適用可能なアルゴリズム(applyIf)または述語に基づくランダムアクセスシーケンス(配列は1つのタイプに過ぎません)の各要素の別の要素。

もちろん、クライアントはこの要素を一緒に作成する必要がありますが、個々の機能はすべて、今ではもっと簡単なことをしています。ランダムアクセスシーケンスの代わりに順方向反復子/列挙可能型に依存することで、さらに一般化することができます。これにより、applyIfは、単方向リンクリスト、二重リンクなど、順方向反復可能な任意のデータ構造で機能します。リスト、二分探索木、配列、ハッシュテーブル、試行、四分木など、名前を付けることができます(連続して反復できる限り)。

そのapplyIf関数は、適用する述語と関数の任意の組み合わせを使用してほぼ何でも機能させることができるため、非常に一般的でコードを再利用する考え方になっているため、再利用可能性がはるかに高くなります。 IDをチェックするようにハードコーディングされた関数(標準ライブラリに入る可能性のあるもの)。しかし、私はまだそれが好きではなく、あなたの昔ながらの最初の解決策がより好きです。最近、私はこれらの超柔軟なソリューションについてあまり気にしません。あちこちで負のIDを冗長にチェックする必要がない限り、ifが内部にある単純な古いループが好きです(その後、他のことを検討します)。さらに一般化できます。

// Calls 'true_func' if pred(element) is true,
// 'false_func' otherwise.
iif(pred, true_func, false_func) {
    return function(element) {
         return pred(element) ? true_func(element): false_func(element);
    }
}

// Calls 'func' for each element in the container.
forEach(container, func) {
    for (it = container.begin(); it != container.end(); ++it)
        func(it.element())
}

その時点でこれを行うことができます:

forEach(array, iif(hasNegativeId, doThisStuff, doOtherStuff));

さらに進んで物事を細かく分類してさらに一般化する方法はいくつかありますが、ここでは止めます。繰り返しますが、これはあまり好きではありません。コードを10代の最も再利用可能な関数(最近は楽しんでいたものの、それほど多くはないもの)にダイシングするのが好きな人のために、ちょっと出してみました。

このソリューションが気に入らない主な理由は、関数型プログラミングを使用して副作用を引き起こすと、その優雅さが大幅に失われるためです。 doThisStuffdoOtherStuffが副作用を引き起こす場合、私はそれらが副作用を引き起こすと想定していますが、戻り値を使用しないため、複雑な制御フローの関数型プログラミングに関連する困難が組み合わされます。副作用あり。これら2つの組み合わせにより、事情を判断するのが非常に困難になる可能性があります。したがって、副作用を発生させたい場合は単純な制御フローが必要であり、複雑な制御フローが必要な場合は(2つの組み合わせではなく)副作用は不要です。だからこそ、最高のifステートメントを使用した単純なループが好きです。制御フローに関しては、デバッグと追跡が非常に簡単です。

0
user204677

言語を指定していませんが、マルチキャストデリゲートをサポートしている場合は、それらを活用して配列を「一度に」処理し、それでも素晴らしいコード分離を実現できます。

いくつかの疑似コード:

public class ListenerA
{
    public void Register(Foo foo)
    {
        foo.ProcessRequest += Process
    }

    public void Process(ProcessRequestData data)
    {
        if (data.getId < 0)
        {
            doStuff();
        }
    }
}

public class ListenerB
{
    public void Register(Foo foo)
    {
        foo.ProcessRequest += Process
    }

    public void Process(ProcessRequestData data)
    {
        if (data.getId >= 0)
        {
            doStuff();
        }
    }
}

public class Foo
{
    public Event ProcessRequest;
    public void Process()
    {
        for(int i=0; i<array.length; i++) 
        {
            if (null != ProcessRequest)
            {
                ProcessRequest(element);
            }
    }
}

// in some controller like class

listenerA.Register(foo);
listenerB.Register(foo);

foo.Process();

このアプローチの優れている点は、変更が完全に閉じられていることです。将来的に3番目のプロセッサを追加する必要がある場合は、新しいプロセッサを作成して登録するだけです。

0
RubberDuck

11行のコードから18行のコードに変更しました。さらに、1つの関数を呼び出す場所がどこにあっても、2つ必要になります。おめでとう。しかし、あなたは十分に行きません。サパディオの答えを見てください。今私はあなたに尋ねます:このコードを維持する(不幸な)運命、11行、18行、またはZapadioの250行(概算)がある場合、どのバージョンが好きですか?

あなたの関数にはoneの責任があります:すべての配列要素を反復処理し、各要素に対してさらに2つの関数の適切な1つを呼び出します。あなたの関数する確かに1つのこと。

11行を250行に変更した場合、実際に重要な問題が発生した場合はどうしますか? 「単一責任」の原則は、おそらく最も誤解されている原則の1つです。タクシーを雇うとき、1ダースの運転手、1つのギア、1つのギア、1つのブレーキ、1つのガスペダル、1つのフロントガラスのワイプ、1つの左のインジケータ、1つの右のインジケータ、 1つはドアを開け、もう1つは荷物をブーツに入れますか?一人のタクシー運転手には、人々とその荷物を安全かつ効率的に目的地まで運ぶという単一の責任があります。

PS。次に、これらすべての関数呼び出しを正しい順序で行う必要があると仮定します。これは珍しいことではなく、多くのことは順序に依存しています。今あなたは行き​​詰まっています。

0
gnasher729