web-dev-qa-db-ja.com

サイクロマティックの複雑さを軽減するにはどうすればよいですか?

私が取り組んでいるコードをlintするたびに、This function's cyclomatic complexity is too high. (7)を取得します。しかし、私はそれがうまくいくようにそれをどのように書き直すことができるかについて少し混乱しています。

これは、そのメッセージをスローし続ける関数です。

function () {
  var duration = +new Date() - start.time,
    isPastHalf = Number(duration) < 250 && Math.abs(delta.x) > 20 || Math.abs(delta.x) > viewport / 2,
    direction = delta.x < 0;

  if (!isScrolling) {
    if (isPastHalf) {
      if (direction) {
        this.close();
      } else {
        if (this.content.getBoundingClientRect().left > viewport / 2 && pulled === true) {
          this.close();
          return;
        }
        this.open();
      }
    } else {
      if (this.content.getBoundingClientRect().left > viewport / 2) {
        if (this.isEmpty(delta) || delta.x > 0) {
          this.close();
          return;
        }
        this.open();
        return;
      }
      this.close();
    }
  }
}

このような状況を回避するために、コードをこのように構造化する方法についてアドバイスをお願いします。

16
Roland

コードには2つのアクションしかありませんが、条件が多すぎます。単一のif-else-statementとブール演算子を条件で使用します。それが不可能な場合は、少なくとも

  • 空の行を削除して、1つの画面ページで完全なロジックを取得します
  • ブランチが何をしているか(そしてその理由)についてコメントを追加します
  • 早期の返品を避ける

簡略化した関数は次のとおりです。

var duration = +new Date() - start.time,
    isPastHalf = Number(duration) < 250 && Math.abs(delta.x) > 20 || Math.abs(delta.x) > viewport / 2,
    isFarRight = this.content.getBoundingClientRect().left > viewport / 2, 
    direction = delta.x < 0;

if (!isScrolling) {
    if (isPastHalf) {
        if (direction)
            this.close();
        else {
            if (isFarRight && pulled)
                this.close();
            else
                this.open();
        }
    } else {
        if (isFarRight) {
            // Looks like the opposite of `direction`, is it?
            if (this.isEmpty(delta) || delta.x > 0)
                this.close();
            else
                this.open();
        } else
            this.close();
    }
}

および短縮:

var duration = +new Date() - start.time,
    isPastHalf = Number(duration) < 250 && Math.abs(delta.x) > 20 || Math.abs(delta.x) > viewport / 2,
    isFarRight = this.content.getBoundingClientRect().left > viewport / 2, 
    direction = delta.x < 0,
    undirection = this.isEmpty(delta) || delta.x > 0;

if (!isScrolling) {
    if ( isPastHalf && !  direction && !(isFarRight && pulled)
     || !isPastHalf && !undirection &&  isFarRight )
        this.open();
    else
        this.close();
}
25
Bergi

まず、関数の結果は3つあります。何もしない、this.close()を呼び出す、またはthis.open()を呼び出します。したがって、理想的には、結果の関数には、使用する結果を決定するifステートメントが1つだけ含まれます。

次のステップは、すべてのブールコードを変数に抽出することです。例:var leftPastCenter = this.content.getBoundingClientRect().left > viewport / 2

最後に、ブール論理を使用して、段階的に単純化します。

ここに私がそれをした方法があります:

まず、すべてのブール変数を抽出します。

_function () {
    var duration = +new Date() - start.time,
      isPastHalf = Number(duration) < 250 && Math.abs(delta.x) > 20 || Math.abs(delta.x) > viewport / 2,
      direction = delta.x < 0,
      leftPastCenter = this.content.getBoundingClientRect().left > viewport / 2,
      positiveDelta = this.isEmpty(delta) || delta.x > 0,
      isPulled = pulled === true; // I'll assume the test is needed rather than just using pulled.

    if (!isScrolling) {
        if (isPastHalf) {
            if (direction) {
                this.close();
            } else {
                if (leftPastCenter && isPulled) {
                    this.close();
                    return;
                }
                this.open();
            }
        } else {
            if (leftPastCenter) {
                if (positiveDelta) {
                    this.close();
                    return;
                }
                this.open();
                return;
            }
            this.close();
        }
    }
}
_

引き出す最も簡単な部分は、isScrollingがtrueの場合、何も起こらないことを認識することです。これにより、1レベルの入れ子がすぐに取り除かれます。

_    // above same
    if (isScrolling) { return; }

    if (isPastHalf) {
        if (direction) {
            this.close();
        } else {
            if (leftPastCenter && isPulled) {
                this.close();
                return;
            }
            this.open();
        }
    } else {
        if (leftPastCenter) {
            if (positiveDelta) {
                this.close();
                return;
            }
            this.open();
            return;
        }
        this.close();
    }
}
_

次に、this.open()が呼び出されたケースを確認します。 isPastHalfがtrueの場合、this.open()は、_!direction_および!(leftPastCenter && isPulled)の場合にのみ呼び出されます。 isPastHalfがfalseの場合、leftPastCenterおよび_!positiveDelta_の場合にのみthis.open()が呼び出されます。

_    // above same
    if (isScrolling) { return; }

    if (isPastHalf) {
        if (!direction && !(leftPastCenter && isPulled)) {
            this.open();
        } else {
            this.close();
        }
    } else {
        if (leftPastCenter && !positiveDelta) {
            this.open();
        } else {
            this.close();
        }
    }
_

Ifsを反転する(つまりthis.close()が最初に来る)と、コードが少しすっきりして、私の最終バージョンが得られます。

_    function () {

    var duration = +new Date() - start.time,
      isPastHalf = Number(duration) < 250 && Math.abs(delta.x) > 20 || Math.abs(delta.x) > viewport / 2,
      direction = delta.x < 0,
      leftPastCenter = this.content.getBoundingClientRect().left > viewport / 2,
      positiveDelta = this.isEmpty(delta) || delta.x > 0,
      isPulled = pulled === true; // I'll assume the test is needed rather than just using pulled.

    if (isScrolling) { return; }

    if (isPastHalf) {
        if (direction || (leftPastCenter && isPulled)) {
            this.close();
        } else {
            this.open();
        }
    } else {
        if (!leftPastCenter || positiveDelta) {
            this.close();
        } else {
            this.open();
        }
    }
}
_

あなたのコードベースを知らずに、私がもっと多くをするのは難しいです。注意すべきことの1つは、directionと私の新しい変数positiveDeltaがほぼ同じであることです。positiveDeltaを削除して、directionを使用することもできます。また、directionはブール値に適した名前ではありません。movingLeftのような名前の方が適しています。

5
David Miani

実際、これらすべてのreturnステートメントは問題を混乱させますが、解決策のヒントを提供します。

if (direction) {
  this.close();
} else {
  if (this.content.getBoundingClientRect().left > viewport / 2 && pulled === true) {
    this.close();
    return; // We'll never `this.open()` if this is true anyway, so combine the booleans.
  }
  this.open();
}

どうですか:

if (direction || (this.content.getBoundingClientRect().left > viewport / 2 && pulled === true)) {
  this.close();
} else {
  this.open();
}

そして:

if (this.content.getBoundingClientRect().left > viewport / 2) {
  if (this.isEmpty(delta) || delta.x > 0) {
    this.close();
    return; // Combine the booleans!
  }
  this.open();
  return;
}

簡素化する:

if ((this.isEmpty(delta) || delta.x > 0) || !this.content.getBoundingClientRect().left > viewport / 2) {
  this.close();
} else {
  this.open();
}

(脇に:元の投稿は閉じ括弧を省略しました。関数(OP)が投稿を超えて継続することを意図している場合、この答えは間違っています(ただし、それをより明確にする必要があります))

結果:2つの(繰り返される)決定を排除しました。

function () {
  var duration = +new Date() - start.time,
    isPastHalf = Number(duration) < 250 && Math.abs(delta.x) > 20 || Math.abs(delta.x) > viewport / 2,
    direction = delta.x < 0;

  if (!isScrolling) {
    if (isPastHalf) {
      if (direction || (this.content.getBoundingClientRect().left > viewport / 2 && pulled === true)) {
        this.close();
      } else {
        this.open();
      }
    } else {
      if ((this.isEmpty(delta) || delta.x > 0) || !this.content.getBoundingClientRect().left > viewport / 2) {
        this.close();
      } else {
        this.open();
      }
    }
  }
}
4
kojiro

ベルギはすでに正しい答えを出していますが、それでも私の好みには複雑すぎます。 fortran77を使用 ではないため、 early return を使用した方がよいと思います。また、追加の変数を導入することにより、コードをさらに明確にすることができます。

function doSomething(isScrolling, start, delta, viewport) {
    if (isScrolling) return;

    var duration = +new Date() - start.time;
    var isPastHalf = Number(duration) < 250 && Math.abs(delta.x) > 20 || Math.abs(delta.x) > viewport / 2;
    var isFarRight = this.content.getBoundingClientRect().left > viewport / 2;

    // I'm not sure if my variable names reflect the actual case, but that's
    // exactly the point. By choosing the correct variable names for this,
    // anybody reading the code can immediatly comprehend what's happening.
    var isMovingToLeft = delta.x < 0;
    var isMovedPastEnd = isPastHalf && !isMovingToLeft && !(isFarRight && pulled);
    var isMovedBeforeStart = !isPastHalf && isMovingToLeft && isFarRight;

    if (isMovedPastEnd || isMovedBeforeStart) {
        this.open();
    else
        this.close();
    }
} 
2
realbart