Как я могу уменьшить цикломатическую сложность?

Всякий раз, когда я нажимаю кусок кода, над которым я работаю, я получаю This function 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();
    }
  }
}

Я хотел бы услышать некоторые советы о том, как я мог бы структурировать свой код таким образом, чтобы избежать таких ситуаций.

Ответ 1

У вас в коде всего два действия, но слишком много условий. Используйте один оператор if-else и логические операторы в условии. Если это невозможно, вы могли бы как минимум

  • удалите пустые строки, чтобы получить полную логику на одной странице экрана
  • добавьте несколько комментариев о том, что делают ветки (и почему)
  • избежать ранних возвратов

Здесь ваша функция упрощена:

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

Ответ 2

Во-первых, есть три результата, которые может иметь ваша функция: ничего не делать, вызвать this.close() или вызвать this.open(). Поэтому в идеале результирующая функция будет иметь только один оператор if, определяющий, какой результат используется.

Следующий шаг - извлечь весь логический код в переменные. Например, 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 истинно, ничего не происходит. Это немедленно избавляет от одного уровня вложенности:

    // 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 истинно, this.open() вызывается только при !direction и !(leftPastCenter && isPulled). Если isPastHalf false, то this.open() вызывается только тогда, когда leftPastCenter и !positiveDelta:

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

Мне сложно делать больше, не зная своей кодовой базы. Следует отметить, что direction и моя новая переменная positiveDelta почти идентичны - вы можете удалить positiveDelta и просто использовать direction. Кроме того, direction не является хорошим именем для логического, что-то вроде movingLeft будет лучше.

Ответ 3

На самом деле все эти операторы 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;
}

Simplify:

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

(Помимо этого: в исходном столбце была оставлена ​​закрывающая фигурная скобка. Если вы (OP) предполагали, что функция продолжается после вашего сообщения, тогда этот ответ неверен (но вы должны были сделать это более ясным))

Результат: Мы устранили два (повторных) решения:

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

Берги уже дал правильный ответ, но он все еще слишком сложный для моего вкуса. Поскольку мы не используя fortran77, я думаю, что нам лучше использовать ранний возврат. Кроме того, код может быть дополнительно уточнен путем введения дополнительных переменных:

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

    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;

    // 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 happening.
    var movedPastBottom = isPastHalf && !direction && !(isFarRight && pulled);
    var movedBeforeTop = isPastHalf && !undirection && isFarRight;

    if (movedPastBottom || movedBeforeTop) {
        this.open();
    else
        this.close();
    }
} 

Ответ 5

Я бы предпочел простой и менее вложенный код, как показано ниже:

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)
    {
        return;
    }
    if (isPastHalf) 
    {
        if (direction) 
        {
            this.close();
            return;
        }
        if (this.content.getBoundingClientRect().left > viewport / 2 && pulled == = true) 
        {
            this.close();
            return;
        }
        this.open();
        return;
    }
    if (this.content.getBoundingClientRect().left > viewport / 2) 
    {
        if (this.isEmpty(delta) || delta.x > 0) 
        {
            this.close();
            return;
        }
        this.open();
        return;
    }
    this.close();
    return;
}