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

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

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

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

Вместо этого мы должны установить «границу доверия» между различными наборами классов и методов. Не существует строгого набора правил относительно того, где устанавливать «границу доверия», но вот несколько рекомендаций:

  • код в конкретном классе, скорее всего, должен доверять коду в том же классе (например, нет необходимости в тяжелой проверке при вызове одного метода класса из другого);
  • если набор классов представляет собой связную функциональность, тогда эти классы должны доверять друг другу. Примером может служить набор сервисов и вспомогательных классов, реализующих определенный алгоритм - они, скорее всего, разрабатываются и тестируются вместе, а предварительные условия должны проверяться в начале алгоритма;
  • один уровень, скорее всего, не должен доверять другому слою; например, когда вы вызываете уровень REST из внешнего интерфейса, вы обязательно должны проверять вводимые данные; аналогично, когда вы вызываете уровень обслуживания из уровня REST;
  • общие и служебные классы и службы никогда не должны доверять вызывающему объекту в предоставлении правильных входных значений;
  • один микросервис определенно не должен доверять входным данным другого микросервиса; один глючный микросервис не должен тащить за собой всю систему.