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

Ошибки будут обнаружены.

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

Список

Есть так много типов ошибок, с которыми я сталкивался во время работы над проектами. Вот контрольный список, который мне пригодился, когда я столкнулся с проблемой, которую не могу решить:

  • Типы: задокументируйте входные данные и подтвердите их там, где это необходимо.
  • Возвращаемые значения: документируйте выходные данные и всегда возвращайте обещания.
  • Побочные эффекты: помните о них и/или предотвращайте их, не изменяя входные данные.
  • Файловая система: позаботьтесь о том, чтобы быть в правильном каталоге.
  • Импорт/экспорт: не забывайте деструктурировать нестандартные модули и не деструктурировать связанные компоненты.
  • Объем проблемы слишком велик: разделяй и властвуй — используй технику бинарного поиска и отладчик.
  • Поврежденные/устаревшие проекты: иногда требуется переустановка или переустановка.
  • Отсутствующие знания: отладка резиновой утки. Найдите «хорошие» части документации. Используйте целевые методы исследования.
  • «этот» контекст: стрелочные функции (ES6) сохраняют контекст выполнения.
  • Сложность: когда многие части зависят друг от друга, и их взаимодействием трудно управлять.
  • Плохой дизайн/ошибки копирования и вставки: убедитесь, что ваш код не противоречит S.O.L.I.D, D.R.Y и другим принципам дизайна. 5 принципов, которые сделают вас НАДЕЖНЫМ разработчиком JavaScript
  • Лихо ужасный: если на карту поставлены жизни, будьте внимательны.
  • Плохо переданные спецификации: убедитесь, что вы работаете над полезными и востребованными функциями.
  • Отсутствие тестов: все мы пишем уязвимый код, если постоянно не спрашиваем: «Как я могу это протестировать?»
  • Oblique Strategies: Брайан Ино крут. Косые стратегии и Случайные косвенные стратегии онлайн

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

Типы (такие как String, Int, Array и т. д.)

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

// without validation
function add(x,y) {
return x + y;
}
add(1,2);// -> 3
add('1','2');// -> '12'
add('hi ','everyone');// -> 'hi everyone'

(Приношу свои извинения за форматирование этих примеров кода. Примеры кода на моем сайте намного читабельнее: исходный пост)

Javascript не волнует, что мы вставляем типы «число» в первый вызов add и типы «строка» во второй и третий вызовы add. Это кажется очень простым, но мы можем захотеть избежать случая, когда мы случайно вводим «1» и «2» (обе строки) в качестве параметров и получаем «12» в качестве вывода. Чтобы обойти эту ошибку, мы могли бы принудить, а затем проверить входные данные, чтобы они соответствовали типам, которые мы ищем.

// with input validation / coercion
function add(x,y) {
 x = Number(x);
 y = Number(y);
 if (!isNaN(x) && !isNaN(y)) {
  return x + y;
}
 throw new TypeError('Function arguments expected type number');
}
add(1,2);// -> 3
add('1','2');// -> 3
add('hi ','everyone');// -> throws error

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

Дальнейшее использование этого подхода будет заключаться в абстрагировании проверки и принуждения в отдельную функцию:

function validateNum(input) {
 input = Number(input);
 if (isNaN(input)) {
  throw new TypeError('Expected type number');
}
 return input;
}

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

add(validateNum('1'), validateNum('2'));

Хотите полностью избежать этой проблемы в Javascript? Вот несколько популярных решений:

Забыл вернуть значения

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

// this function will log result to the console, but return undefined.
function forgetful(num) {
 const otherNum = 6;
 const result = otherNum + num;
 console.log(result);
}
// this function can be used as expected. Notice the documentation.
function rememberedToReturn(num) {
 /*
  * num: number
  * return: number
 */
 const otherNum = 6;
 return otherNum + num;
}
console.log(rememberedToReturn(11));// logs 17 as expected.

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

function fetchMoviesNoReturn() {   
  fetch('http://example.com/movies.json')
 .then(function(response) {
  return response.json();
 })
} 
fetchMoviesNoReturn().then(function(myJson) {
 console.log(myJson); 
});// throws error

Добавление простого оператора return перед fetch решит нашу проблему.

// ES5 function 
fetchMovies() {
 return fetch('http://example.com/movies.json')
   .then(function(response) {
     return response.json();
 })
} 
// ES6 -> notice the implicit `return`s by omitting the `{}` around the function bodies 
const fetchMovies = () => fetch('http://example.com/movies.json') .then(response => response.json()); 
fetchMovies().then(function(myJson) { console.log(myJson); });
// myJson is logged to console.

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

Допустим, у вас есть массив. По какой-то причине вы хотите отсортировать его во вспомогательной функции, но сохраните доступ к его несортированному порядку. Вы пишете функцию, которая принимает массив в качестве входных данных:

function sortAndOtherStuff(array) {
 const sortedArray = array.sort();//whoops, we modified the input
 // do stuff with sortedArray 
return sortedArray;
}

Теперь, перебирая массив в другой функции, вы обнаружите, что он ведет себя хаотично:

for (let i = 0; i < array.length; i++) {
 if (i === 2) { // do some magic with array[i] and sortAndOtherStuff(array) } 
}

Вы можете видеть, что мы случайно изменили ввод в функции sortAndOtherStuff. Было бы лучше сделать «чистую» функцию, которая не мутирует ввод:

function sortAndOtherStuff(array) {
 const sortedArray = [...array].sort();//that's better. we made a copy with the spread ... operator 
// do stuff with sortedArray 
 return sortedArray; 
}

В React может быть неприятно, когда вы меняете состояние и ожидаете повторного рендеринга компонента. Будьте осторожны, если вы ожидаете, что массив или объект обновят состояние в React. Состояние неизменяемо, поэтому вам нужно вернуть копию (как мы сделали с операцией распространения [...array] выше) тех, чтобы получить состояние для обновления!

Нахождение в неправильной части файловой системы

Вы когда-нибудь ловили себя в неправильном каталоге, когда…

  • пытаюсь установить пакеты из npm
  • пытаюсь импортировать файл
  • попытка инициализировать репозиторий git

Просто всегда полезно проверить работоспособность, спросив: «Я в правильном каталоге?»

Импорт/экспорт

Мне нравится небольшая экономия времени и нажатия клавиш, которая достигается благодаря большому количеству синтаксисов ES6. import и export, которые заменяют require() и module.exports, я регулярно использую при разработке в React. Я также использую деструктурирование при импорте: import - JavaScript | МДН

Одна из опасностей здесь — написать код в одну сторону для import {ComponentName}, а затем добавить функцию connect из «реагировать-редукс». Я совершал эту глупую ошибку несколько раз! Приложения будут «работать», но вести себя странно. Все, что мне нужно было сделать, это изменить указанный выше импорт на import ComponentName. Эти глупые маленькие жучки могут занять некоторое время, чтобы выследить их, и вы заставите вас ударить себя по лбу, когда вы их найдете. Кодирование очень специфично. Одно простое слово или две маленькие фигурные скобки могут нарушить работу всего приложения.

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

module.exports = coolObject;
/*
USAGE: 
import { fetchData } from './coolObject'; 
fetchData.then(res => { etc }) 
*/

Объем проблемы слишком велик

Один из самых мощных алгоритмов поиска называется «Двоичный поиск». Начнем с поиска в середине отсортированного набора данных. Если число слишком велико, мы ищем первую половину набора данных. Если слишком мало, ищем вторую половину. Мы повторяем этот процесс разделения пространства поиска пополам, пока не достигнем нашей цели (или не найдем ее). Думайте об этом как о «разделяй и властвуй».

Нам нужна небольшая вариация, которая будет полезна при отладке. Наше приложение не «отсортировано», но мы можем произвольно назначать «половинки» приложению. Базовый подход заключается в размещении операторов print или console.log в «первой половине» вашего приложения. Если он ведет себя так, как ожидалось, то можете быть уверены, что проблема в вашей «второй половинке».

Мой пример из реальной жизни: я работал с гигантской функцией render в компоненте React. Выдавало ошибку о том, что React не может отобразить объект. В коде было так много объектов, а ошибка не показывала номер строки. Чтобы «разделяй и властвуй» эту проблему, я удалил код из второй половины кода. Это не сработало, поэтому я удалил код во второй половине первой половины. И так далее. Как только код заработает, я смогу внимательно и внимательно изучить код, который я недавно удалил, вместо того, чтобы разбирать гигантскую функцию, щурясь на сотни строк кода своими слабыми человеческими глазами.

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

Отладка в Chrome (javascript.info)

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

Иногда требуется переустановка или переустановка. Возможно, вам потребуется обновить некоторые пакеты. Этот пост представляет собой руководство по перестройке базового модуля узла с некоторой информацией об обновлении: «node.js — как переустановить зависимости приложения с помощью npm? - Переполнение стека"

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

Отсутствующие знания

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

В качестве примера история: чем больше я использовал библиотеку «react-redux», тем больше она мне нравилась. Я использовал Redux внутри React без него, но забыл часть синтаксиса во время работы над проектом (включая методы subscribe и getState в методах монтирования). Я просмотрел документацию Redux и не смог найти простой пример с использованием более многословный синтаксис. Они переходят сразу к использованию библиотеки «react-redux» без явных инструкций по ее использованию без нее. В данном случае не было хорошей документации, кроме примеров проектов, которые у меня были. Поэтому иногда лучшая документация — это ваши собственные заметки. Что я должен был сделать.

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

После этого используйте свои навыки «Google-fu», чтобы получить доступ к бесчисленным ресурсам во всемирной паутине:

Google-fu определяется как «умение пользоваться поисковыми системами (особенно Google) для быстрого поиска полезной информации в Интернете». Это несколько насмешливая ссылка на кунг-фу, которое в западном полушарии обычно воспринимается как требующее высокой степени мастерства.

К этому я бы добавил StackOverflow. Это удивительный ресурс. Не бойтесь задавать вопросы!

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

‘this’ (функции со стрелками и объявления функций в Javascript)

Помните, как специфично кодирование? Иногда простое маленькое слово «функция» может сбить с толку ваш контекст выполнения. В функциях обратного вызова обработчиков событий использование выражения стрелочной функции (ES6 Javascript) будет обеспечивать использование контекста this функции обработки событий. Использование объявления функции (с ключевым словом «функция») создаст новый контекст «это» для этой функции. Этот второстепенный синтаксис будет препятствовать правильному срабатыванию обработчика. Недавно я споткнулся из-за функции обратного вызова внутри обработчика событий, который использовал объявление функции:

class MyClass extends Component {
 constructor(props) { 
 super(props)
 this.state = {
  myData: []
 }
// this binds the context correctly
  this.handleChange = this.handleChange.bind(this)
 }
alterData(newData) {
 return newData + Math.random();
}
handleChange() {
 // since we used .bind above, 'this' is the correct context 'MyClass'
 this.setState({myData: new Cool_Library.Method(function(event) {
// since we used the 'function' keyword in this callback function,
// we'll get an error like: "this.alterData is not a function"
  this.alterData(event.property);
});
}
}
render() {
return (
 <MyInput
  onChange={ this.handleChange }
 />
)
}
}

С небольшим изменением синтаксиса и удалением слова «функция» это было решено:

this.setState({myData: new Cool_Library.Method((event) => {

Я мог бы также использовать .bind(this):

// ... this.alterData(event.property) }).bind(this) // ...

Сложность

Чем больше ваше приложение, тем с большей сложностью вам, вероятно, придется иметь дело. Многие части могут зависеть друг от друга, и их взаимодействием может быть трудно управлять. Здесь необходимо сделать шаг назад от инструментов кодирования и отладки. Я считаю полезным рисовать диаграммы, представляющие общую архитектуру приложения. При этом я задаю вопросы, чтобы проверить свои предположения: «Является ли компонент действительно повторным рендерингом во всем приложении?» «А сервисные работники вообще работают в Электроне?» «Я предполагаю, что этот компонент может делиться состоянием с этим другим компонентом. Это правильно?" Мы склонны сопротивляться этим трудным вопросам. Ответы могут заставить нас изменить структуру нашего приложения, и мы должны это сделать, если это улучшит код в целом.

Краткое описание некоторых ошибок сложности, с которыми я недавно имел дело:

  • Я работал над веб-приложением, которое использовало рабочий поток службы для записи микрофона пользователя. Мы с командой решили сделать его настольным приложением с помощью Electron. Из-за того, что узел является однопоточным, мы в конце концов обнаружили, что наш рабочий поток не будет работать так, как мы изначально планировали.
  • В том же проекте мы были очень озадачены тем, что не все части нашего приложения реагировали на изменения в нашем магазине Redux. Мы предполагали, что наш слушатель доступен на каждой странице приложения, но мы обнаружили, что слушатель не отображается на всех страницах. Мы могли бы избежать многих бесплодных проб и ошибок, бросив вызов нашему предположению об архитектуре ранее.
  • Я работал над приложением, которое отображало данные и включало «модальный» компонент, который отображал данные по клику. Только последнее модальное окно отображалось правильно, остальные были либо тусклыми, либо затемненными. Оказалось, что все модальные окна имеют одно и то же состояние, и мне нужно было поместить каждое модальное окно в отдельный компонент.

Плохой дизайн/ошибки копирования и вставки: проверьте свой код на соответствие S.O.L.I.D, D.R.Y и другим принципам проектирования.

Если во время написания кода вы обнаружите, что решаете одну проблему, но создаете две новые проблемы, вы, вероятно, идете вразрез с хорошими принципами дизайна. Аббревиатура S.O.L.I.D охватывает много вопросов. «S» означает «Принцип единой ответственности». Если вы можете помочь, функция должна делать только одну вещь. Если вы объедините несколько действий внутри одной функции, вы можете создать неожиданное поведение, если что-то изменится в другом месте, которое взаимодействует с этой функцией.

Аббревиатура D.R.Y расшифровывается как «Don’t Repeat Yourself». Это не просто рекомендация — это необходимо для создания поддерживаемого кода. Если вы скопируете и вставите один раз, вам придется многократно копировать и вставлять каждое изменение. Ручное редактирование кода в нескольких местах — верный путь к катастрофе. Лучше потратить время на то, чтобы выяснить, что общего между обоими примерами кода, и абстрагироваться от функциональности. Вот известная ошибка, когда вывод a на консоль «врёт»:

// Initial log is consistent: 
const x = 23.5; 
const value = 3 * 17 / 53 + x; 
console.log(3 * 17 / 53 + x; 
// Later, we modify `value` slightly and forget to change the log: const value = 3 * 17 / (53 + x); 
console.log(3 * 17 / 53 + x);// why isn't it changing ?!?!?

Это очень опасная ошибка — после копирования и вставки ваш журнал точен, но затем, пока вы исправляете код, вы изменяете выражение, не изменяя журнал. Вы заметите скобки вокруг (53 + x) в value, но не в журнале. Я надеюсь, что вы сможете найти лучший способ справиться с этой ситуацией.

Примеры этих и других принципов приведены в статьях ниже:

- 5 принципов, которые сделают вас НАДЕЖНЫМ разработчиком JavaScript

- KISS, DRY, YAGNI и многое другое. 7 сокращений, которые должен знать каждый разработчик

Известные ужасные ошибки

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

Плохо сообщаемые спецификации

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

Отсутствие тестов

Мы все пишем код, уязвимый для ошибок, если постоянно не спрашиваем: «Как я могу это протестировать?» Я работал над набором функций преобразования слов и гордился результатом. Я писал тесты постфактум. То, что произошло, было очень поучительным.

// test: what happens with unexpected input? expect(wordTransform([])).not.toThrowError();//FAILED!!!

wordTransform ожидал массив длины больше 0. Я реорганизовал каждый метод, чтобы гарантировать работу с массивом любой длины и изящно вывести undefined, если ввод не ожидался. Я избежал пули, написав тесты!

Косвенные стратегии

Мы все попадаем в колею, следуя одним и тем же шаблонам мышления снова и снова. Кое-что, на что меня натолкнул Коллин Миллер из Fullstack Academy, называется косвенные стратегии. В двух словах, это короткие текстовые фразы (написанные музыкальным продюсером Брайаном Ино и Питером Шмидтом), которые заставляют вас думать или двигаться не так, как вы обычно думаете или двигаетесь. Косые стратегии и Случайные косвенные стратегии онлайн

Примеры:

  • «Собери некоторые элементы в группу и обработай группу»
  • «Поскольку поиск продолжается, что-то будет найдено»
  • «Наполните каждый бит чем-нибудь»

Это может помочь включить ваше правое полушарие в решение проблем.

Прощальные мысли / Дальнейшее чтение

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

Дополнительная литература

* 250 стратегий отладки.pdf (stanford.edu)

* Контрольный список отладки (depaul.edu)

* flatiron-school/ruby-debugging-checklist: список того, что нужно делать, когда вы сталкиваетесь с ошибкой в ​​вашем коде ruby. (github.com)

* Контрольный список «Глупых: Просто скажи Нет! к эмоциональной отладке…эмоционально! Марк Уитмер на CodePen»

Первоначально опубликовано на scraggo.github.io 8 мая 2018 г.