Защитное программирование как стиль написания программы

Обновлено: 21.11.2024

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

В стандарте IEC 60880 [IEC 06], раздел B.3a, рекомендуется «выполнять проверки правдоподобия (защитное программирование)». IEC 61508 [IEC 11], раздел C.2.5, объясняет защитное программирование как цель «создать программы, способные обнаруживать поток заказов или ненормальные данные или ненормальные значения данных, которые выполняются, и реагировать на эти ошибки заранее определенным и приемлемым образом» и методы для этого включают в себя: контроль данных (тип, пределы поля, проверка правдоподобия и т. д.), проверки правдоподобия для входных данных и промежуточных переменных, контроль выходных данных посредством наблюдения, мониторинг целостности программного обеспечения и методов реализации.

Критически важные для безопасности приложения OpenVX

Фрэнк Брилл, . Стивен Рамм, в Руководстве по программированию OpenVX, 2020 г.

11.4.2 Защитное программирование

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

Примером API, поддерживающего наступательное программирование, является графическая библиотека Vulkan TM, разработанная так, чтобы быть быстрой, легкой и использоваться с осторожностью. Реализациям рекомендуется не тратить циклы на проверку параметров, а требовать надлежащего использования. OpenVX не делает конкретных условий о том, что должны делать реализации, но в спецификации перечислены требования к значениям параметров.

Излишне защитное программирование может привести к недостижимости кода и стать кошмаром тестирования, требующим обширного внедрения ошибок, особенно на более высоких уровнях безопасности, когда аудиторам может потребоваться доказательство 100% охвата mc/dc.

Рекомендации по отладке встроенного программного обеспечения

Защитный код

Я нашел в Интернете эту лекцию [12], которая, как мне показалось, очень хорошо изложила философию защитного программирования . Автор определяет защитное программирование как метод, при котором вы всегда предполагаете худшее из всех входных данных. Он выделяет три правила защитного программирования:

Никогда ничего не предполагайте. Все входные данные должны быть проверены на соответствие набору всех допустимых входных данных. Затем определите, что вы предпримете, если введенные данные неверны.

Используйте стандарты кодирования (это уже знакомо).

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

Безопасность программного приложения

5.4.2.2 Правила программирования

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

Правила программирования состоят из:

правила именования объектов для повышения удобочитаемости;

правила, относящиеся к типизации, стандарты, рекомендующие использование строгого языка типизации и выбор такого языка, как C, который требует добавления защитного программирования для ограничения объектов функциональным типом, как определено в спецификациях;

правила, запрещающие определенные конструкции (например, не использовать указатели) или рекомендующие определенные формы для защиты определенных конструкций (в случае использования указателя проверять до и после использования);

правила, экспортированные повторно используемыми элементами и/или уже существующими компонентами;

правила, экспортируемые платформой выполнения (SRAC — ограничения приложений, связанные с безопасностью) и/или операционной системой.

CCR обычно формализуется как:

форма (чек-лист), требующая идентификации компонента, его версии, уровня безопасности и учитываемого репозитория документов;

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

Обнаружение вторжений

Эрик Книпп, . Эдгар Даниелян, технический редактор, Управление сетевой безопасностью Cisco (второе издание), 2002 г.

Слабые стороны приложения и операционной системы

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

Ошибки программного обеспечения

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

Получение паролей — простые способы взлома программ

Большинство людей в тот или иной момент создавали простой пароль на основе объектов, которые легко запомнить, таких как имя или любимый цвет. В 10 из 15 компаний, в которых я работал, хорошие методы управления паролями редко применялись.

"Сёрфинг через плечо", наблюдение через плечо.

Получение доступа к файлам паролей.

Использование сниффера или троянского ПО для поиска паролей в открытом виде.

Воспроизведение трафика входа в систему, записанного сниффером, который содержит зашифрованный пароль.

Атаки на основе словаря, когда программа просматривает каждое слово в словарной базе данных.

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

Распространенные проблемы, причины и решения

Шейн Кук, программирование CUDA, 2013 г.

Утверждения и защитное программирование

Защитное программирование — это программирование, предполагающее, что вызывающий абонент сделает что-то не так. Например, что не так со следующим кодом?

char * ptr = malloc(1024);

Код предполагает, что malloc вернет действительный указатель на 1024 байта памяти. Учитывая небольшой объем памяти, который мы запрашиваем, маловероятно, что в действительности произойдет сбой. В случае неудачи malloc возвращает нулевой указатель. Чтобы код работал правильно, функция free() также должна обрабатывать нулевые указатели. Таким образом, начало свободной функции может быть

… найти список выделенных областей памяти для ptr и освободить память.

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

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

C предоставляет очень полезную конструкцию, которая редко используется, за исключением тех программистов, которые знакомы с передовой практикой разработки программного обеспечения — директива assert. Когда программа дает сбой, молчаливая ошибка — это плохая практика. Это позволяет ошибкам оставаться в коде и оставаться незамеченными. Идея утверждения противоположна. Если есть ошибка с параметрами, переданными вызывающей стороной, это ошибка программирования. Вызванная функция должна кричать о проблеме, пока она не будет устранена. Таким образом, если нулевой указатель не допускается в качестве одного из входных параметров функции, то замените if ptr =! Проверка NULL со следующим:

// Нулевые указатели не поддерживаются

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

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

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

char * ptr = malloc (1024);

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

Одной из проблем с использованием защитного программирования и утверждений является то, что процессор проводит условия проверки времени, которые, по большей части, всегда будут действительными. Он может сделать это на каждой и каждую функцию вызова, петель и т. Д., В зависимости от того, насколько широко распространено использование утверждений. Решение этой проблемы - это простое - для генерации двух наборов программного обеспечения, версии отладки и версия выпуска. Если вы уже используете такую ​​пакет, как Visual Studio, это присуще на настройке проекта по умолчанию. Старые системы, особенно системы, не являющиеся IDE, могут понадобиться, чтобы это было установлено.

После выполнения вы можете просто генерировать версию Assert Macro, Assert.

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

Как от выпуска CUDA 4.1, теперь также возможно разместить утверждения в код устройства для вычисления 2.x устройства. Это было не то, что было возможным ранее из-за неспособности графического процессора для повышения такого исключения.

Смотреть тенденции на языках (особенно Go), методологии (Agile), проектирование программного обеспечения, тестирование и т. Д.

Среда, 16 мая 2012 года

Оборонительное программирование

Я немного говорил о оборонительных программировании в моем феврале. Я также намеревался добавить его как один из моих выходов в своей статье в Code Project, поэтому я сделал немного исследований о том, что другие написали об этом. Я был очень удивлен некоторыми неточностями, которые я нашел, особенно в статье Википедии.

Обратите внимание, что этот пост не о том, как использовать защитное программирование. Это было охвачено глубиной (например, в коде, завершенном Стивом Макконнеллом). Я просто пытаюсь более точно описать то, почему это было изобретено и проблемы с этим.

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

Первый раз, когда я когда-либо сталкивался с термином «оборонительное программирование», был в K & R (язык программирования C, 1-е издание, по Кернгигану и Ritchie). После обширного поиска я не могу найти более ранние ссылки на термин. Термин, вероятно, вытекает из термина «оборонительное вождение», которое вступило в общее использование в начале 1970-х годов за несколько лет до написания K & R.

Он упоминается дважды в индексе K & R. На стр. 53 Это ясно относится к созданию кода устойчивых к ошибкам, но на странице 56 он говорит о написании кода таким образом, что снижает вероятность изменений будущего кода, представляя ошибки. В любом случае многие книги с тех пор использовали термин «оборонительное программирование», чтобы означать создание устойчивости кода в присутствии ошибок, например, прагматичный программист, а Эндрю Хант и Дейв Томас (который разговаривает о «обосновании) в главе« Прагматичная паранойя ") и другие. Еще до того, как многие профессионалы программного обеспечения, я включал в себя, использовал этот термин таким же, как минимум в середине 1980-х годов.

Разногласия о определении

Несмотря на то, что срок довольно четко понимается более 20 лет, точное определение термина в последнее время затуманется после нескольких (в целом безрецептурных) статей и блогах появилось на различных веб-сайтах. Например, текущая статья Wikipedia и несколько сайтов, которые ее цитируют, производит «оборонительное программирование», как подход к обработке ошибок. Обработка ошибок может быть связана с защитным программированием, но они определенно не одно и то же; и один не является подмножеством другого (см. Ниже).

Еще одна хорошо известная и часто упоминаемая статья, озаглавленная просто «Защитное программирование», имеет очень высокий рейтинг в Code Project. Это отличная и достойная статья сама по себе, но она не только о защитном программировании. По собственному признанию, речь идет о "методах, полезных для обнаружения ошибок программирования". Как мы увидим ниже, защитное программирование имеет противоположный эффект — оно имеет тенденцию скрывать ошибки, а не отлавливать их. В этой статье обсуждается множество вещей, и ее правильнее было бы назвать чем-то вроде "Надлежащая практика написания кода".

Обработка ошибок и защитное программирование

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

Обработка ошибок выявляет и обрабатывает ситуации, когда что-то идет не так, как вы знаете, возможно, но маловероятно. Напротив, защитное программирование пытается решить проблемы, которые считаются «невозможными». Есть две проблемы с этим различием, которые могут вызвать путаницу.

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

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

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

  1. Программа принимает данные непосредственно от пользователя, и пользователь может ввести неверные данные.
  2. Программы принимают данные из текстового файла, введенного пользователем.
  3. Программа принимает данные из XML-файла (автоматизированного или созданного вручную).
  4. Программа читает двоичный файл данных, созданный другой программой.
  5. Программа читает двоичный файл, написанный ею самой.
  6. Программа считывает двоичный файл, содержащий CRC, чтобы проверить, не поврежден ли он.
  7. Программа считывает временный двоичный файл, созданный за несколько минут до этого.
  8. Программа считывает созданный ею файл отображения памяти.
  9. Программа читает из локальной переменной (т. е. в памяти), в которую она только что записала.

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

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

Поэтому не всегда ясно, когда вам нужен явный код для обработки ошибок, а когда вы должны просто программировать в обороне.

Архетипический пример защитного программирования встречается практически в каждой когда-либо написанной программе на C, где условие завершения написано как проверка на неравенство (культура C

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

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

Из-за такого акцента на эффективности защитное программирование используется только тогда, когда оно практически не снижает производительности. Это характерно для приведенного выше примера, поскольку операция «меньше чем» обычно выполняется так же быстро, как и операция «не равно».

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

Таким образом, если "маловероятные" ошибки обычно не обрабатываются, нет смысла обрабатывать "невозможные" условия как ошибки, поскольку это усложнит существующую нагрузку по обработке ошибок. (Более подробно это описано в пункте 10 моей статьи Code Project по адресу http://www.codeproject.com/Articles/357065/Ten-Fallacies-of-Good-C-Code.) Конечно, на языках, за исключением обработки, многие такие "невозможные" условия можно легко обработать, создав "программное исключение".

Большая путаница в отношении защитного программирования возникает из-за того, что область контроля не всегда четко определена. Например, если у вас есть функция, которая принимает строковый (const char *) параметр, вы можете предположить, что вам никогда не передается указатель NULL, если в этом нет смысла. Если это частная функция, вы всегда сможете ее обеспечить; но если его использование выходит за рамки вашего контроля, вы не можете предположить это, если вы четко не задокументируете, что указатель NULL не может использоваться.

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

Поэтому любое обсуждение защитного программирования должно четко определять объем рассматриваемого кода. Это одна из проблем со статьей Википедии о защитном программировании.

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

Иногда это восстановление может быть успешным, но обычно это приводит к тому, что программа хромает. В худшем случае это может незаметно вызвать серьезные проблемы, такие как потеря или повреждение данных. (Увидев что-то подобное, я обычно сохраняю свои данные и перезапускаю программу, чтобы убедиться, что она не находится в каком-то странном состоянии.)

Проблемы с защитным программированием

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

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

Хуже всего то, что защитное кодирование, как известно, скрывает ошибки во время разработки и тестирования. Никто не спорит, что это хорошо. Альтернативой является использование того, что было названо «наступательным программированием», а иногда и «быстрым отказом». Это означает, что кто-то должен знать о проблемах, а не скрывать их.

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

Стандартная библиотека C

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

В отчетах часто требуются данные, красиво отформатированные в столбцы. Это обычно достигается в C, используя минимальную ширину поля спецификаторов формата printf(). Например, чтобы напечатать числа в столбце шириной пять символов, вы должны использовать спецификатор "%5d". Если целое число слишком велико для поля, то C все равно печатает лишние символы, даже если это испортит ваши столбцы. (Сравните это с другими языками, такими как Fortran, где переполнение поля приводит к скрытому усечению чисел, что вызывает некоторые очень неприятные проблемы.) Это пример защитного программирования, поскольку в неожиданной ситуации код пытается сделать что-то разумное.< /p>

Наконец, вам есть над чем подумать. Стандартная библиотека C включает функцию, которая принимает строку цифр и возвращает целое число с именем atoi.

Если вы не знакомы с atoi(), она не возвращает никакого кода ошибки, но останавливается, когда встречает первый непредвиденный символ. Например, atoi("two") просто возвращает ноль.

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

Защитное программирование: предварительные условия

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

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

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

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

Или это может быть связано с каким-то сложным бизнес-правилом, которое вы, возможно, еще даже не в состоянии выразить словами.

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

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

С правильно определенными концепциями предметной области нет необходимости дублировать предварительные условия.

Защитное программирование: пустые значения

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

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

Для этого вам понадобятся две вещи. Во-первых, определите специальную структуру Maybe, которая позволит вам различать ссылочные типы, допускающие и не допускающие значение NULL. А во-вторых, используйте библиотеку Fody.NullGuard, чтобы внедрить автоматические проверки всех входных параметров, не отмеченных структурой Maybe.

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

Обратите внимание на отсутствие проверок на null. Null Guard сделает за вас всю необходимую работу.

Защитное программирование: утверждения

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

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

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

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

В нашем случае это будет выглядеть так:

Обратите внимание, что вместе с утверждением мы также преобразовываем объект типа FacebookResponse, который является встроенным классом из официального SDK, в наш собственный тип UserInfo. Таким образом, мы можем быть уверены, что информация о пользователе всегда находится в допустимом состоянии, поскольку мы сами проверили и преобразовали ее.

Обзор

Несмотря на то, что защитное программирование является полезным приемом, убедитесь, что вы используете его правильно.

Если вы видите повторяющиеся предварительные условия, подумайте о том, чтобы выделить их в отдельный тип.

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

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

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

Мусор на входе, ничего на выходе

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

Как правило, эти проверки предназначены для таких предположений, как:

  • Входные параметры находятся в ожидаемом диапазоне.
  • Указатель не нулевой
  • Логика функции не изменяет переменные, которые не должны изменяться
  • Функция корректно освободила обработчик внешнего ресурса (например, обработчик файла)
  • Результат вычисления проходит некоторую числовую проверку, например CRC.
  • Контейнеры, такие как списки или стеки, содержат необходимое количество элементов

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

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

В исключительных случаях используйте исключения

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

Есть несколько идей, как максимально эффективно использовать исключения независимо от того, какой язык вы используете:

Обработайте ошибку, не игнорируйте ее

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

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

Подход, который вы выберете для обработки ошибки, зависит от ваших потребностей в надежности и правильности.

Является ли ваш код надежным или правильным?

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

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

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

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

Немного внимания к деталям решает все

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

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

Что делать дальше:

  • Поделитесь этой статьей с друзьями и коллегами. Благодарю вас за то, что помогаете мне обращаться к людям, которым эта информация может оказаться полезной.
  • Карл Витулло написал прекрасную статью о проблемах чрезмерного использования защитного программирования. Вы можете найти ее здесь.
  • Хорошие ребята из Jobtensor написали удобный инструмент для изучения основ Python и некоторых популярных библиотек, таких как Pandas и Numpy. Вы можете найти его здесь.
  • Дополнительную информацию об обработке ошибок можно найти в главе 7 книги "Чистый код", а о защитном программировании - в главе 8 книги "Завершенный код". Эта и другие очень полезные книги находятся в списке рекомендуемой литературы.
  • Отправьте мне электронное письмо с вопросами, комментариями или предложениями (это находится на странице «Обо мне»). Давай, не стесняйся!

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

Читайте также: