Хотим мы того или нет, но современное программирование, а точнее современная разработка программного обеспечения, — это в первую очередь бизнес, а не искусство. Это бизнес, который построен на производстве программного продукта. В том или ином виде. И как в любом производстве, в нём существуют свои процессы, управленческие хитрости, критерии качества, конкуренция и истории успехов и поражений. Я — руководитель платформенной команды в компании, одним из основных продуктов которой являются iOS, Android и веб-фреймворки. Также по совместительству я один из авторов курса по iOS-разработке в Яндекс Практикуме. В этой статье хочу поделиться одним подходом или стилем написания программного кода, который помог лично мне в трудной ситуации, описать набор инструментов, которые могут его обеспечить, поговорить о результатах, которые можно с помощью него достичь, и, разумеется, о цене, которую приходится за это заплатить. Одно из распространённых названий этого стиля — defensive programming (англ. защищённое программирование). В статье привожу примеры кода на Swift, но это не означает, что такой же подход не может быть применим к другому языку и стеку технологий. Просто автору этой статьи удобнее приводить примеры именно из мира iOS-разработки. Ну, поехали. Автоматизация и диджитализация с помощью программных продуктов проникла уже практически во все сферы нашей жизни, поэтому область, в которой будет использоваться ваш программный код, может быть любой — от приложений для конференций до систем управления автомобилем или самолётом, от обработчика фотографий до биржевых роботов, которые торгуют миллиардами рублей. И для каждого из таких проектов существует цена ошибки программиста. Если программист допустит ошибку в приложении для редактирования фотографий и приложение начнёт падать или некорректно работать, то по большей части никто особо не пострадает. Пользователи смогут пожаловаться на проблему, ошибка будет исправлена, опубликована новая версия приложения, и всё закончится небольшим падением трафика пользователей и дырами в аналитических отчётах. Другое дело, когда такая ошибка может привести к непоправимым последствиям — некорректной работе автопилота самолёта или утечке секретных данных. В таких случаях ошибка программиста становится практически непоправимой. К сожалению, современная история знает такие случаи, компания Boeing не даст соврать. Как же бороться и подойти к написанию кода, если ваш проект именно такой — проект, в котором цена ошибки программиста велика? Одним из подходов является как раз defensive programming, концепция, при которой команда разработки предполагает, что если в коде есть что-то, что гипотетически может пойти не так, то оно обязательно пойдёт не так, и в самый неподходящий момент. Звучит как Закон Мёрфи, не правда ли? Соответственно, команда заранее обрабатывает все такие места в коде, которые гипотетически могут привести к нежелательному результату, и тем самым обеспечивает стабильность кода. Но разве при обычной разработке программисты не должны этим же и заниматься? Учитывать все возможные крайние случаи, обрабатывать ошибки и писать безопасный и расширяемый код? Во-первых, ситуации бывают разные и проекты тоже бывают разные. На этапе прототипирования чего только не бывает, а потом этот код может стать основой будущего большого бизнеса. Во-вторых, вся суть в глубине погружения в то, что гипотетически может пойти не так. И тут нам уже не обойтись без примеров. 1. Самая распространённая проблема, которая случается с программой, — это NullPointerException, состояние, когда мы обращаемся к объекту, а его значения уже нет в памяти. Рискну предположить, что это самая распространённая ошибка в современной истории программирования. Благо практически все языки, которые активно развиваются сейчас, предоставляют инструменты, как бороться с этой напастью. Давайте посмотрим на вот эти примеры на Swift: Возможность указания того, что в переменной может быть значение, а может и не быть, поддерживается в большом количестве языков, например, в TypeScript, Dart и Kotlin, так что это не что-то Swift-специфичное. Но также разработчики языков дали возможность указать компилятору свою «уверенность» в том, что значение всё-таки не может быть нулевым и именно эта конструкция может вызвать тот самый NullPointerException: И вот именно такие так называемые force unwrapp’ы и force cast’ы — наш первый кандидат на потенциальную проблему и, соответственно, на исключение из кодовой базы. 2. Следующей очевидной ошибкой может стать IndexOutOfBoundsException при работе с коллекциями, состояние, когда вы пытаетесь получить элемент, например, массива по индексу, который больше, чем длина этого массива. Если так сделать, то программа в большинстве языков просто упадёт: Благо в Swift можно создать свой безопасный оператор, чтобы взять элемент по индексу: 3. Еще одна распространённая ошибка, которая может привести к падению программы, — это использование небезопасных атрибутов для работы с памятью. Swift реализует концепцию «автоматического счётчика ссылок». Для того чтобы не создавать циклических ссылок на объекты, в языке есть два атрибута — weak и unknown. Первый всегда обнуляет ссылку, если объект был удалён из памяти, второй этого не делает, и именно тут кроется та самая потенциальная ошибка. Ведь как удобно не заниматься работой с опциональными значениями и просто написать что-то такое: Но если объект, на который указывает unknown ссылка, пропадёт из памяти, а мы попытаемся к нему обратиться, то программа также упадёт. При работе с weak ссылками компилятор попросит вас сделать такую ссылку опциональной, и её можно будет корректно обработать: Кстати, сами циклические ссылки тоже могут создавать нежелательные сторонние эффекты, и их также лучше исключить. Эти примеры достаточно очевидны. Часто правила, как безопасно работать с такими конструкциями, уже существуют в серьёзных проектах. Давайте пойдём дальше в поиске потенциально опасных конструкций. 4. И первым пунктом будет принципиальное сокращение всех возможных участков кода, которые полагаются на рантайм языка. Чем больше всего может быть проверено и отсечено в процессе компиляции — тем лучше. В случае со Swift под этот принцип попадает достаточно много языковых конструкций. Некоторые из них существуют из-за совместимости с Objective-C. Например, Key-Value Observing или сокращённо KVO. Эта языковая конструкция позволяет отслеживать изменения значения поля объекта и присылает оповещения, если значение изменилось. Для этого мы должны передать имя поля в виде строки: Но что будет, если кто-то изменит поле age, не изменив имя константы? Ничего фатального не произойдёт, но мы не узнаем, что наш обсервер перестал работать, без тестирования этого участка кода. Поэтому в Swift придумали новый синтаксис, который позволит вашей программе всегда проверять, что мы следим за изменениями существующих полей: Но в новом синтаксисе появляется новая проблема: разработчику надо обязательно отписываться от наблюдения за изменениями свойства, иначе оно так и останется в памяти. Но отписываться надо всего один раз, иначе программа упадёт. То есть в такой конструкции о безопасности кода не может идти и речи. 5. Мы хотим, чтобы наш код не содержал потенциальных ошибок, поэтому должны быть уверены в каждой строчке, которую предоставляем конечным пользователем. Но как быть, если ваш проект использует код, который написан не вами? Ведь проект может содержать сторонние зависимости, и код этих зависимостей не всегда отвечает правилам безопасности, которые нужны вам в проекте. В таких случаях есть несколько вариантов: Полный отказ от сторонних зависимостей. В проекте останется только ваш собственный код, и вы сможете гарантировать его безопасность. Копирование исходного кода зависимостей в проект и исправление этого кода. Код зависимости становится уже вашим, и вы можете гарантировать его безопасность. Важно не забывать смотреть на лицензию этой зависимости, не с каждым кодом такое можно сделать легально. Также обновлять зависимость будет проблематично. Исправить код сторонней зависимости в отдельном форке репозитория. Код будет отвечать вашим требованиям. Также будет возможность обновлять эту зависимость и предлагать изменения разработчикам самой зависимости, чтобы не пользоваться форком. 6. Большое количество скрытых проблем возникает в приложениях, которые написаны на нескольких языках, в случае iOS это могут быть Swift, Objective-C, C и C++, если мы говорим о нативных языках. Проблемы могут возникать на стыке языков, то есть когда мы используем типы и функции одного языка в другом. Например, указатели из C в Swift-коде. Самый очевидный ответ — мигрирование кодовой базы на один язык. Если по каким-то причинам это невозможно, то целью должна стать максимальная изоляция кодовых баз, написанных на разных языках, друг от друга и взаимодействие между ними через безопасный и тщательно оттестированный протокол. Эти примеры уже достаточно необычные и радикальные, но давайте пойдём еще дальше и попробуем найти откровенно безумные претензии к конструкциям в коде, которые мы используем каждый день, но которые тем не менее могут привести к проблемам в программах. 7. В Swift есть 4 типа объектов: классы, структуры, перечисления и акторы. Акторы и классы являются ссылочными типами данных, структуры и перечисления передаются по значению. Обычно, если в программе есть какой-то стейт — значение, которое надо сохранять и изменять в течение работы программы, то оно сохраняется в каком-то классе или акторе. Наступает потенциальная проблема, если программа работает в многопоточной среде (а теоретически любая программа работает в многопоточной среде), то хранить значения в классе может быть не очень безопасно, потому что сразу несколько потоков могут работать с этим классом и асинхронно переписывать значения переменной: Обычно такую проблему решают просто — все операции с переменными, которые могут быть изменены из разных потоков, должны проходить синхронно, для этого во всех языках есть инструменты. Но цель defensive programming — исключить даже гипотетическую возможность ошибки. Ведь разработчик может просто забыть обратиться к переменной синхронно. Эту проблему как раз решают акторы — программа просто не соберётся, если не написать работу с переменной корректным образом: Получается, само наличие классов в коде программы является потенциальной проблемой? Кажется, что да. Но ведь акторы не поддерживают наследование! И это ещё одна потенциальная проблема классов. Разработчик может переписать поведение какого-то метода и забыть вызвать метод предка. Это можно решить, используя атрибут final для классов, который не даст возможность наследования от класса и заставит разработчиков использовать, например, композицию объектов. Приведённые выше случаи — только примеры, и далеко не всё, что можно выделить. Как в iOS-разработке, так и на других платформах и языках можно найти много таких конструкций, которые потенциально могут привести к проблемам. Но ведь это всё нужно, только когда потенциальная ошибка разработчика стоит очень дорого, правда? По умолчанию да, но есть и другие случаи, когда defensive programming или какие-то его элементы могут помочь. Хочется выделить ещё несколько сценариев использования defensive programming, которые чаще могут встретиться в реальной жизни. Разработка продукта, для которого получить сообщения и отчёт об ошибках мы, как разработчики, физически не можем, в частности — crash-логи. Пример такого проекта — разработка фреймворков.  Если ваша команда разрабатывает iOS-фреймворк, который потом будут использовать разработчики в своих продуктах, то не существует физической возможности получить отчёты о падениях кода фреймворка. Единственный способ — ждать, когда о проблемах вам сообщат пользователи. В таких случаях приходится действовать на опережение и быть проактивным — просто убрать теоретическую возможность возникновения таких проблем. Еще один сценарий — это проекты с большим техническим долгом и проблемами со стабильностью. Если ваш проект такой, то, возможно, defensive programming сможет помочь в короткие сроки навести какой-то порядок.  Вместо того чтобы разбираться с каждой проблемой по отдельности, можно запретить к использованию в коде проекта потенциально опасные конструкции. Также модуль за модулем, файл за файлом исправить все потенциально опасные участки в текущем коде. Это не решит проблему технического долга, зато заметно повысит стабильность продукта и обеспечит эту стабильность на время исправлений, которые нужно внести, чтобы исправить технический долг. А теперь давайте поговорим об инструментах, которые помогут найти проблемы в коде и не позволят возникать такому коду в дальнейшем. Основными инструментами для поиска и исправления проблем в коде являются линтеры, форматтеры кода и непосредственно компилятор. Форматтеры приводят код к единообразному виду, а также исправляют очевидные простые проблемы в коде. Линтеры находят большие и комплексные проблемы в коде и могут заблокировать сборку программы, если эти проблемы критичны. Компилятор содержит большое количество как стандартных, так и дополнительных проверок, которые не позволят вам собрать программу, если она не отвечает определенным требованиям. Для iOS самым распространённым форматтером является SwiftFormat, а линтером — SwiftLint. Тут можно прочитать про дополнительные настройки компилятора. Важно, чтобы нарушение правил, которые вы определили для вашего форматтера, линтера и компилятора, не давало возможности собрать ваш проект. Тогда это исключит потенциальную возможность попадания кода, нарушающего эти правила, в общий репозиторий и тем более в продакшен. В моём опыте работы случились сразу все три аспекта, при которых стоит воспользоваться defensive programming, — iOS-фреймворк с большим техническим долгом и достаточно дорогой стоимостью ошибки, а также не очень большим временем на исправление текущей ситуации в связи с реалиями рынка. Решением было вводить элементы defensive programming как можно скорее. В результате за два месяца получилось кардинально изменить ситуацию в проекте и тем самым улучшить показатели самого бизнеса. Технический долг остался, но перестал негативно влиять на сам продукт. Его исправлением мы успешно занимаемся параллельно с разработкой новой функциональности. Когда вы пишете код — никогда не забывайте, что в первую очередь он должен решать бизнес-задачи, а уже потом быть красивым и выразительным. Может показаться, что defensive programming — это «серебряная пуля» от большинства проблем, которые могут случиться с проектом. Но, разумеется, за всё приходится платить, и плата за defensive programming достаточно высока. В первую очередь на обработку даже самых невероятных сценариев работы программы нужно тратить много времени, а объём кодовой базы может значительно увеличиваться.  Во-вторых, при обработке всех возможных сценариев может начать страдать быстродействие самой программы и другие метрики, например размер приложения, — для некоторых проектов это критично. Многие удобные и понятные языковые конструкции, которые разработчики привыкли использовать каждый день, попадают «под нож», и приходится заново привыкать к языку программирования и платформе, под которую ведётся разработка. Это может негативно отразиться на настроении команды, и важно заранее объяснить, зачем нужны такие радикальные правила в данный момент. В любом случае технические лидеры, которые будут принимать решение о внедрении такого подхода, должны взвесить все за и против, оценить стоимость ошибки программиста, понять настроения команды и цену, которая будет заплачена за внедрение такого подхода. И только потом принимать решение. В этой статье мы посмотрели на один спорный, но временами эффективный стиль разработки программного обеспечения — defensive programming. Основным контраргументом обычно выступает посыл, что проблема не в коде, а в неумении программистов нормально пользоваться инструментами, мол, «нормально делай — нормально будет». Но как показывает моя практика, далеко не во всех ситуациях такой посыл применим, ведь не бывает идеальных людей и идеальных проектов. Я надеюсь, что для начинающих программистов эта статья даст понимание  того, что в программировании всегда есть место для разных производственных концепций и что всегда есть куда углубляться в безопасность кода. А для опытных разработчиков статья покажет образ мышления, который поможет с вашим проектом в критических ситуациях. Буду рад услышать обратную связь, обсудить подход в комментариях и ответить на вопросы.