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

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

Безотлагательная и видимая ошибка.

Многие люди рекомендуют разрабатывать устойчивые приложения, посредством автоматической обработки ошибок. В результате, приложение ‘failing slowly’ падает тихо. Программа продолжает работать после ошибки, но ошибка еще даст о себе знать позже при странных обстоятельствах.

Система, написанная по методике Fail Fast ведет себя в точности до наоборот — если возникла проблема, то ошибка возникает немедленно и она абсолютно видна. Эта техника кажется неинтуитивной, так как ‘failing immediately and visibly’ звучит так, будто система будет непрочной, но по факту это сделает системы более устойчивой. Баги легче найти и исправить.

Для примера рассмотрим метод, который читает свойство из конфигурационного файла. Что случиться, если свойства не существует? Как правило это null или значение по-умолчанию.

public int maxConnections() {
    string property = getProperty("maxConnections");
    if (property == null) {
        return 10;
    } else {
        return property.toInt();
    }
}

Тот же метод, написанный по методике fail fast:

public int maxConnections() {
    string property = getProperty("maxConnections");
    if (property == null) {
        throw new NullReferenceException ("maxConnections property not found in " + this.configFilePath);
    } else {
        return property.toInt();
    }
}

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

Результат будет совершенно другим, если использовать методику Fail Fast. В тот момент, когда разработчик опечатается, приложение прекратит выполнение, сообщив об ошибке, причем очень подробно. Разработчик стукнет себя по лбу и исправит проблему за 30 секунд.

Методика Fail-fast

Ключ к методике — assert. Assert’ы — это очень маленькая часть кода, которая проверяет условие и останавливает приложение, если условие не выполнено. Итак, если кто-то начнет делать что-то неправильно, то assert’ы заметит проблему и сделает её видимой. Большинство языков имеет встроенные assert’ы, но они не всегда вызывают исключения. Также, они как правило довольно обобщенны, недостаточно выразительны и вызывают дубликаты. Методика предлагает писать собственные assert’ы.

Как бы то ни было, для собственного assert’а нужно дать знать, когда его добавлять. Самым простым способом сделать это — добавить поясняющий комментарий. Комментарии — множественные документированные допущения о том как код работает или как он должен быть назван. Когда вы пишете подобный комментарии, подумайте, как его можно поместить сам assert.

Когда вы пишете метод, избегайте написания подтверждения проблемы в самом методе (его стоит вынести). Тесты и практика TDD — лучший путь к обеспечению правильности отдельного метода. Красота assert’ов в их способности отлавливать проблемы на швах системы — взаимодействие остальной части с конкретным методом.

Написание assert’ов

Отличным примером, который покажет превосходство использования assert’ов — Assert.notNull(). NullPonterException возникает достаточно часто, поэтому было бы неплохо, чтобы программное обеспечение сообщало, когда пустая ссылка создается неправомерно. С другой стороны, если использовать Assert.notNull() повсеместно, то код превратиться в море из неиспользуемых assert’ов. Так что я ставлю себя на место незадачливого разработчика, отлаживающего систему. Когда происходит данная ошибка, как я могу легко предотвратить её?

Иногда, язык автоматически подсказывает нам, где произошла ошибка. Например, Java и C# вызывает исключение, когда имеет место быть указатель на null. В простом случае stack trace ведет нас к месту, где произошла ошибка.

System.NullReferenceException
    at Example.WriteCenteredLine()
        in example.cs:line 9
    at Example.Main() in
        example.cs:line 3

Здесь мы приходим на строку 3, где обнаруживаем, что null был явно передан в метод. Ответ не всегда так очевиден, но, как правило, он обнаруживается после нескольких минут поиска.

Теперь рассмотрим более сложный случай, такой как:

1 public class Example
2 {
3     public static void Main()
4     {
5         FancyConsole out = new FancyConsole();
6         out.WriteTitle("text");
7     }
8 }
9
10 public class FancyConsole()
11 {
12     private const screenWidth = 80;
13     private string _titleBorder;
14
15     public FancyConsole()
16     {
17         _titleBorder = getProperty("titleBorder");
18     }
19
20     public void WriteTitle(string text)
21     {
22         int borderSize = (screenWidth - text.Length) /(_titleBorder.Length * 2);
23         string border = "";
24         for (int i = 0; i < borderSize; i++)
25         {
26            border += _titleBorder;
27         }
28         Console.WriteLine(border + text + border);
29     }
30 }

    System.NullReferenceException
        at Example.Main()
            in example.cs:line 22
        at FancyConsole.Main()
            in example.cs:line 6

Stack race ведет нас к линиям 6 и 22, тогда как реальная ошибка произошла на 17 строке — getProperty() вернул null. А на 22 строке была попытка использовать свойство Length. Stack trace привел нас в никуда, типичный ситуация для кода, написанного в стиле ‘failing slowly’.

В последнем случае имеет место быть то, что может предотвратить fail fast методика. Необходимо, чтобы программа давала больше информации для поиска ошибки. Итак, для кода можно применить следующее правило — если приложение применяет методику fail fast самостоятельно, то нет необходимости использовать что-либо дополнительное. То есть, если идет речь о вызове метода на объекте, который предположительно является null, то программа сама подскажет где это произошло. А вот если речь идет о присвоении пришедшего из вне значения в переменную, то необходимо проверить, не является ли значение null.

Это правило расстановки меток может помочь вашим программам тоже, но основная цель - чтобы задуманный процесс шел по плану. Добавляя assert’ы в код, необходимо рассудительно их размещать, подобно примеру выше. Думать о том какие ошибки могут возникнуть и почему. Размещать assert’ы так, чтобы приложение сразу же упало при их появлении, так проблемы будет проще найти.

Устранение дебаггинга.

Иногда stack race — это все, что нужно для обнаружения ошибки. В других случаях, нужно знать, что находится в переменной. Но даже если знать содержимое переменной, бывает тяжело воспроизвести ошибку. Не правда ли было бы лучше, если программа сразу же будет говорить, если что-то пошло не так?

Когда вы пишете assert’ы, необходимо думать о том, какая информация понадобиться для исправления ошибки, если та произойдет. Эту информацию необходимо включить в сообщение assert’а. Не надо повторять условие assert’а, stack race сам приведет вас туда. Вместо этого, включите ошибку в контекст.

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

Assert.notNull(result, "result was null");
Assert.notNull(result, "can't find property");

Но лучшим вариантом будет Assert.notNull(result, “can’t find [” + key + “] property in config file [” + file + “]”);. Данный вариант дает всю необходимую информацию.

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

Прочная ошибка.

Fail fast выглядит так, словно приложение хрупко. Конечно, это делает дефект видимыми, что сказать о том, когда приложение окажется у пользователей. Никто не хочет, чтобы приложение сломалось из-за конфигурационного файла.

Типичное желание — не использовать assert’ы. Не делать этого! Запомните, ошибка, произошедшая у пользователя — прошла через тестирование. И скорее всего, у вас возникнут проблемы с её воспроизведением в данном случае. Такие ошибки сложно найти, а хорошо расставленные assert’ы помогут сэкономить дни напряжений.

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

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