Статьи

Защитное программирование заслуживает такого плохого имени?

На днях я пошел на часовую беседу об эрланге, просто как наблюдатель; Я ничего не знаю об эрланге, кроме того, что это звучит интересно и что синтаксис … ну … необычный. Лекция была прочитана для некоторых Java-программистов, которые недавно выучили эрланг и были справедливым критиком своего первого эрлангского проекта, который они только что завершили. Докладчик сказал, что этим программистам нужно перестать думать, как программисты на Java, и начать думать, как программисты на эрланге 1, и, в частности, перестать защищаться от программирования и позволить процессам быстро отказывать и решать проблему.

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

перезапуск их при необходимости. Идея быстрого отказа не является чем-то новым и определяется как метод, который нужно использовать, когда ваш код сталкивается с недопустимым вводом. Когда это происходит, ваш код просто падает и прерывается, потому что вы фиксируете поставщика этого ввода, а не вашего кода. Подтекст того, что сказал докладчик, состоит в том, что Java и защитное программирование — это плохо, и если они хороши, быстрое восстановление, что действительно требует более тщательного изучения.

Первое, что нужно сделать, это определить защитное программирование, и первое определение, с которым я столкнулся, было в том, что сейчас, возможно, является легендарной книгой: « Написание твердого кода» Стива Магуайра, изданной Microsoft Press . Я читал эту книгу много лет назад, когда я был программистом на Си, который тогда был языком де-факто. В книге Стив демонстрирует использование макроса _Assert :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/* Borrowed from Complete Code by Steve Maguire */
#ifdef DEBUG
void _Assert(char *,unsigned) /* prototype */
#define ASSERT(f) \
if(f) \
{ } \
else
_Assert(__FILE__,__LINE__)
#else
#define ASSERT(f)
#endif
 
// ...and later on..
 
void _Assert(char *strFile,unsigned uLine) {
 
fflush(NULL);
fprintf(stderr, '\nAssertion failed: %s, line %u\n',strFile,uLine);
fflush(stderr);
abort();
}
 
/////// ...and then in your code
 
void my_func(int a). {
 
ASSERT(a != 0);
 
// do something...
}

… как его определение защитного программирования. Идея заключается в том, что мы определяем макрос C, который при включении DEBUG my_func(…) будет проверять его ввод с помощью ASSERT(f) и будет вызывать _Assert(…) случае сбоя условия. Следовательно, в режиме DEBUG в этом примере my_func(int a) может прервать выполнение, если arg a равно нулю. Когда DEBUG выключен, проверенные не выполняются, но код становится меньше и быстрее; что-то, что, вероятно, было больше внимания еще в 1993 году.

Глядя на это определение, приходит на ум несколько вещей. Во-первых, эта книга была опубликована в 1993 году, так ли это до сих пор? Не было бы хорошей идеей убить Tomcat с помощью System.exit(-1) если один из ваших пользователей ввел неправильный ввод! Во-вторых, более свежая версия Java также более сложна, имеет исключения и обработчики исключений, поэтому вместо того, чтобы прервать программу, мы сгенерируем исключение, которое, например, отобразит страницу ошибки с выделением неверных входных данных. Однако главное, что приходит мне в голову, это то, что это определение защитного программирования для меня звучит как безотказное , на самом деле оно идентично.

Я не впервые слышу, как программисты жалуются на защитное программирование, так почему же у него такая плохая репутация? Почему ведущий разговора elrang так сильно клеветал на это? Я предполагаю, что есть хорошее использование защитного программирования и плохое использование защитного программирования. Позвольте мне объяснить с некоторым кодом …

В этом сценарии я пишу калькулятор индекса массы тела (BMI) для программы, которая сообщает пользователям, имеют ли они избыточный вес. Значение ИМТ от 18,5 до 25, по-видимому, приемлемо, в то время как все, что превышает 25, варьируется от избыточного веса до тяжелого ожирения с большим количеством проблем, ограничивающих жизнь. Для расчета ИМТ используется следующая простая формула:

1
BMI = weight (kg) / (height(m)2)

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class BodyMassIndex {
 
  /**
   * Calculate the BMI using Weight(kg) / height(m)2
   *
   * @return Returns the BMI to four significant figures eg nn.nn
   */
  public Double calculate(Double weight, Double height) {
 
    Validate.notNull(weight, "Your weight cannot be null");
    Validate.notNull(height, "Your height cannot be null");
 
    Validate.validState(weight.doubleValue() > 0, "Your weight cannot be zero");
    Validate.validState(height.doubleValue() > 0, "Your height cannot be zero");
 
    Double tmp = weight / (height * height);
 
    BigDecimal result = new BigDecimal(tmp);
    MathContext mathContext = new MathContext(4);
    result = result.round(mathContext);
 
    return result.doubleValue();
  }
}

Приведенный выше код использует идею, выдвинутую в определении защитного программирования Стивом 1993 года. Когда программа вызывает метод Calculate calculate(Double weight,Double height) , выполняется четыре проверки: проверяется состояние каждого входного аргумента и выдается соответствующее исключение при сбое. Поскольку это 21- й век, мне не нужно было определять собственные процедуры проверки, я просто использовал те, что были предоставлены библиотекой Apache commons-lang3, и импортировал:

1
import org.apache.commons.lang3.Validate;

…и добавил:

1
2
3
4
5
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.1</version>
</dependency>

… К моему pom.xml.

Библиотека Apache commons lang содержит класс Validate , который обеспечивает некоторую базовую проверку. Если вам нужны более сложные алгоритмы проверки, взгляните на библиотеку Apache commons validator.

После проверки метод calculate(…) вычисляет ИМТ и округляет его до четырех значащих цифр (например, nn.nn). Затем он возвращает результат вызывающей стороне. Использование Validate позволяет мне писать множество тестов JUnit, чтобы гарантировать, что в случае проблем все идет хорошо, и различать каждый тип ошибки:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class BodyMassIndexTest {
 
  private BodyMassIndex instance;
 
  @Before
  public void setUp() throws Exception {
    instance = new BodyMassIndex();
  }
 
  @Test
  public void test_valid_inputs() {
 
    final Double expectedResult = 26.23;
 
    Double result = instance.calculate(85.0, 1.8);
    assertEquals(expectedResult, result);
  }
 
  @Test(expected = NullPointerException.class)
  public void test_null_weight_input() {
 
    instance.calculate(null, 1.8);
  }
 
  @Test(expected = NullPointerException.class)
  public void test_null_height_input() {
 
    instance.calculate(75.0, null);
  }
 
  @Test(expected = IllegalStateException.class)
  public void test_zero_height_input() {
 
    instance.calculate(75.0, 0.0);
  }
 
  @Test(expected = IllegalStateException.class)
  public void test_zero_weight_input() {
 
    instance.calculate(0.0, 1.8);
  }
}

Одним из «преимуществ» кода на C является то, что вы можете включить или выключить ASSERT(f) используя переключатель компилятора. Если вам нужно сделать это в Java, посмотрите на использование ключевого слова assert в Java.

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class BodyMassIndex {
 
  /**
   * Calculate the BMI using Weight(kg) / height(m)2
   *
   * @return Returns the BMI to four significant figures eg nn.nn
   */
  public Double calculate(Double weight, Double height) {
 
    Double result = null;
 
    if ((weight != null) && (height != null) && (weight > 0.0) && (height > 0.0)) {
 
      Double tmp = weight / (height * height);
 
      BigDecimal bd = new BigDecimal(tmp);
      MathContext mathContext = new MathContext(4);
      bd = bd.round(mathContext);
      result = bd.doubleValue();
    }
 
    return result;
  }
}

Приведенный выше код также проверяет как null и нулевой аргументы, но делает это с помощью следующего оператора if:

1
if ((weight != null) && (height != null) && (weight > 0.0) && (height > 0.0)) {

С другой стороны, код не будет зависать, если введены неверные данные, но он не сообщит вызывающей стороне, что случилось, он просто скрывает ошибку и возвращает null . Хотя он не потерпел крах, вы должны спросить, что будет делать вызывающая сторона, будет ли возвращено null значение? Придется либо игнорировать проблему, либо обработать ошибку и использовать что-то вроде этого:

01
02
03
04
05
06
07
08
09
10
11
@Test
  public void test_zero_weight_input_forces_additional_checks() {
 
    Double result = instance.calculate(0.0, 1.8);
    if (result == null) {
      System.out.println("Incorrect input to BMI calculation");
      // process the error
    } else {
      System.out.println("Your BMI is: " + result.doubleValue());
    }
  }

Если этот «плохой» метод кодирования используется во всей кодовой базе, то для проверки каждого возвращаемого значения потребуется большой объем дополнительного кода. Это хорошая идея, чтобы НИКОГДА не возвращать нулевые значения из метода. Для получения дополнительной информации взгляните на этот набор блогов . В заключение, я действительно думаю, что нет никакой разницы между защитным программированием и отказоустойчивым программированием, поскольку они действительно одно и то же. Разве нет, как всегда, просто хорошего кодирования и плохого кодирования? Я позволю тебе решить. Этот пример кода доступен на Github .

1 При изучении нового языка всегда происходит смена парадигмы мышления. Там будет точка, где пенни падает, и вы «получите», что бы это ни было .