Включите исполнение JavaScript в браузере, чтобы запустить приложение.
28 окт 2024

Что такое Guard Expression и примеры применения данного подхода к разработке

Что такое Guard Expression и зачем его нужно применять во время программирования. Основные задачи Guard Expression: цикломатическая сложность функций и отсечение пограничных случаев. Примеры применения Guard Expression — в нашей статье.

Guard Expression как подход в программировании позволяет программе оценивать условия и реагировать соответствующим образом. Если определенное условие не выполняется, оператор Guard гарантирует, что программа выйдет за пределы текущей области видимости. Метод используется в разных языках программирования. Он необходим для снижения уровня вложенности функции. Также Guard помогает экономить ресурсы. В статье отметим основные задачи Guard Expression и приведем примеры кода.

  1. Что такое Guard Expression
  2. Основные задачи и применение Guard Expression
  3. Цикломатическая сложность функций
  4. Отсечение пограничных случаев

Что такое Guard Expression

В программировании Guard, или ограничитель, — это логическое выражение (boolean), которое должно иметь значение true, если выполнение программы будет продолжаться в рассматриваемой ветви.

Независимо от того, какой язык программирования используется, guard code или guard clause представляют собой проверку предварительных условий целостности, используемых для предотвращения ошибок во время выполнения. 

Например, Guard в Nest JavaScript работает так же, как и в общем программировании. Но их реализация тесно интегрирована с инфраструктурой NestJS. Этот метод контроля дает разрешение или запрет доступа к конечным точкам Nest JavaScript. Можно создать защиту для маршрута, обновляющего профиль пользователя, чтобы доступ к нему был только у администратора.

Основные задачи и применение Guard Expression

Основные проблемы, возникающие в коде, в котором не применяется этот подход:

  • чрезмерный отступ — использование вложенной структуры управления означает, что существует высокий уровень отступов. Затрудняет чтение кода;
  • связь между if-else — при наличии большого количества отдельных фрагментов кода между if-else, концептуально связанных друг с другом, необходимо выполнять чтение кода путем перескакивания между разными частями;
  • mental effort — из-за различных переходов в исходном варианте при генерации кода требуются дополнительные условия.

 Цикломатическая сложность функций

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

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

Пример: если исходный код не содержит оператора потока управления, то его цикломатическая сложность будет равна 1. Исходный код содержит единственный путь. Но если исходный код содержит одно условие if, цикломатическая сложность уже будет равна 2. Здесь будет два пути: один для истинного, другой — для ложного.

Метрика рассчитывается путем построения ориентированного графа из исходного кода. Это делается для каждой функции и не считается накопительным числом: сообщаемое значение относится только к этой функции.

Пример кода:

void ef1(void);

void ef2(void);

void ef3(void);

void func(int a, int b)

{

  ef1();

  if(a < 0) {

    ef2();

  }

  ef3();

}

Цикломатическая сложность измеряется с помощью простой формулы, основанной на теории графов:

v(G) = e – n + 2p

Где: 

e = количество ребер графа.

n = количество узлов графа.

p = количество связных компонентов.

Поскольку мы анализируем только функции, а не набор связанных графов, формулу можно упростить до:

v(G) = е – n + 2

как P = 1, представляющий узлы входа и выхода функции.

Один из полезных эффектов метрики — через исходный код программы она находит применение как количественная мера линейно независимых путей. В результате это приводит к правильному количеству модульных тестов, необходимых для достижения покрытия ветвей (например, истинные и ложные пути оператора if).

Пример 1:

func complicatedFunction(a int, b int) int {

  if a > 10 {

    if b < 5 {

      return a * b

    } else {

      if a%2 == 0 {

        return a + b

      }

    }

  }

  return a - b

}

Эта функция имеет несколько вложенных условий, что приводит к высокой цикломатической сложности.

Пример 2:

func simplerFunction(a int, b int) int {

  if a <= 10 {

    return a - b

  }

  if b < 5 {

    return a * b

  }

  if a%2 == 0 {

    return a + b

  }

  return a - b

}

Здесь функция уменьшает цикломатическую сложность, сохраняя при этом ту же логику. 

Отсечение пограничных случаев 

Пограничные случаи еще называют проверками. Они могут сделать код более сложным. При этом программа не будет выполняться, если задать правильные условия.

В простой одномерной задаче всегда следует проверять два пограничных случая вместе хотя бы с одной внутренней точкой. Это гарантирует хороший охват диапазона значений.

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

Пример:

def fib(n):

    if n == 0 or n == 1:

        return n

    else:

        return fib(n - 1) + fib(n - 2)

Эта функция имеет два крайних случая: ноль и единицу. Для этих значений n функция fib() совершает выборочное действие, не применимое ни к каким другим значениям.  Пример проверки на Python:

from mod import fib

def test_fib0():

    # test edge 0

    obs = fib(0)

    assert obs == 0

def test_fib1():

    # test edge 1

    obs = fib(1)

    assert obs == 1

def test_fib6():

    # test internal point

    obs = fib(6)

    assert obs == 8

Guard не заменяет if-else, а скорее дополняет его. Основное различие между ними заключается в обращении с предложением else.

func guardVsIfElse(isValid: Bool) {

    if isValid {

        print("Valid")

    } else {

        print("Invalid")

        return

    }

    guard isValid else {

        print("Invalid")

        return

    }

    print("Valid")

}

В приведенном выше примере с использованием оператора if-else сообщение Valid будет напечатано, если условие выполнено. В противном случае он напечатает Invalid и вернется.