Как я могу уменьшить цикломатическую сложность?
Всякий раз, когда я нажимаю кусок кода, над которым я работаю, я получаю 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;
}