Как человек, который любит потреблять бесконечный контент, связанный с программированием на YouTube, я недавно наткнулся на видео, которое действительно привлекло мое внимание. Видео озаглавлено Почему вы не должны вкладывать свой код от CodeAesthetic. В этой статье я подытожу то, что узнал из видео, и поделюсь несколькими примерами, связанными с темой, из моей профессиональной карьеры.
Что такое вложенность кода
Вложение кода является обычной практикой при написании кода. Каждый оператор if, цикл for, оператор switch или функция обратного вызова добавляют глубину, что в конечном итоге приводит к глубоко вложенным блокам кода.
Вот пример:
function processData(data: any[]) { for (let i = 0; i < data.length; i++) { if (data[i].type === "A") { for (let j = 0; j < data[i].values.length; j++) { if (data[i].values[j].isValid) { for (let k = 0; k < data[i].values[j].details.length; k++) { if (data[i].values[j].details[k].isImportant) { console.log(data[i].values[j].details[k].info); } } } } } else if (data[i].type === "B") { // do something else } } }
Этот код имеет две основные проблемы:
1- Он глубоко вложен, с тремя уровнями циклов for, что затрудняет чтение и понимание.
2- если изменится структура массива данных, этот код может перестать работать как положено.
Недостатки вложения кода
Вложенный код намного сложнее читать и понимать. По мере усложнения кода становится все труднее отслеживать, что происходит, что позволяет легко упустить важные детали. Это может привести к ошибкам и другим проблемам, которые гораздо сложнее диагностировать и исправить.
Кроме того, вложенный код может быть сложно поддерживать. По мере роста кодовой базы становится все сложнее добавлять новые функции, что может привести к увеличению времени разработки и разочарованию коллег.
Будь никогда Нестером
Как говорится в видео, никогда не несущий — это программист, который допускает только три уровня глубины. Все, что выходит за рамки этого, не допускается и требует рефакторинга.
Вот пример трех слоев глубины:
function processData(data: any[]) // 1 { if(data.length > 0) // 2 { for (let k = 0; k < 3; k++) // 3 { console.log(`i: ${i}, j: ${j}, k: ${k}`); } } }
Чтобы соблюдать философию «Никогда не складывать» при написании сложных алгоритмов, есть несколько полезных методов: извлечение, ранний возврат, структуры данных и внешние библиотеки.
Добыча
Как следует из названия, извлечение просто означает реорганизацию частей кода в отдельные функции, каждая из которых имеет свою собственную задачу.
Пример:
function processData(data: any[]) { data.forEach((item) => { if (item.type === "A") { item.values.forEach((value) => { if (value.isValid) { console.log(`${value.id}, ${value.name}, ${value.description}`); } }); } else if (item.type === "B") { // do something else } }); } // The previous deeply nested function can be extracted as such: function processData(data: any[]) { data.forEach((item) => { processItem(item); }); } function processItem(item: any) { if (item.type === "A") { processValues(item.values); } else if (item.type === "B") { // do something else } } function processValues(values: any[]) { values.forEach((value) => { if (value.isValid) { console.log(`${value.id}, ${value.name}, ${value.description}`); } }); }
Раннее возвращение
Шаблон раннего возврата можно использовать для возврата из функции, как только будет выполнено определенное условие. Это может помочь избежать глубоко вложенных блоков кода за счет раннего выхода из функции.
Например:
function processData(data: any[]) { data.forEach((item) => { if(item.type === "A") { if (item.values.length > 0) { item.values.forEach((value) => { if (value.isValid) { console.log(value.detail.info); } }); } } }); } // The previous deeply nested function can be refactored using early return as such: function processData(data: any[]) { data.forEach((item) => { if (item.type !== "A") { return; } if (item.values.length == 0) { return; } item.values.forEach((value) => { if (!value.isValid) { return; } console.log(value.detail.info); }); }); }
Структуры данных
Структуры данных, такие как массивы, карты или наборы, могут использоваться для хранения данных, а простые циклы могут использоваться для итерации данных, а не глубоко вложенных блоков кода.
Вот очень простой пример:
for (let i = 0; i < 5; i++) { for (let j = 0; j < 5; j++) { for (let k = 0; k < 5; k++) { console.log(`k: ${k}`); } } } // instead of nesting loops, the following array can be used: const data = [0,1,0,1,0,1,0,1]; data.forEach((value) => { console.log(value); });
Внешние библиотеки
Иногда лучший способ избежать глубоко вложенного кода — вообще не писать никакого кода, лучший способ сделать это — использовать внешние библиотеки для эффективного получения необходимых результатов.
Вот пример:
const data = [3, 2, 5, 1, 4]; for (let i = 0; i < data.length - 1; i++) { for (let j = 0; j < data.length - i - 1; j++) { if (data[j] > data[j + 1]) { [data[j], data[j + 1]] = [data[j + 1], data[j]]; } } } console.log(data); // this unnecessarily nested block of code does the same as the following const data = [3, 2, 5, 1, 4]; const sortedData = _.sortBy(data); // https://lodash.com/ console.log(sortedData);
Наблюдаемое вложение в RXJS
Из личного опыта, наиболее частое использование глубокой вложенности в Angular происходит при работе с observables.
При реализации сложных алгоритмов в RxJS легко создать несколько слоев вложенных наблюдаемых и операций, каждый из которых зависит от предыдущего. Это может привести к сложному и непонятному коду.
Никогда не вкладывать каналы и подписки
При работе с наблюдаемыми крайне важно избегать вложенных каналов и подписок. Это явный признак ошибочной наблюдаемой логики. В большинстве случаев это можно легко исправить, используя соответствующие операторы RxJS или разбивая исходную наблюдаемую на более мелкие наблюдаемые, каждая из которых предназначена для определенной цели.
Пример:
this.courseService .getCourse(lesson.id) .pipe( switchMap((course) => combineLatest([ of(course), from( this.getAdditionalDetails(course.id) ), this.progressService.getProgress( course.id, lesson.id ) ]).pipe( take(1), tap(([course, details, progress]) => { //...do some business logic with the results }) ) ), take(1) ).subscribe(); // This Observable can be refactored into the following const course$ = this.courseService.getCourse(lesson.id); const additionalDetails$ = this.course$.pipe( switchMap((course) => from(this.getAdditionalDetails(course.id))) ) const progress$ = this.course$.pipe( switchMap((course) => this.progressService.getProgress(course.id,lesson.id)) ) combineLatest([course$, additionalDetails$, progress$]).pipe( tap(([course, additionalDetails, progress]) => //...do some business logic with the results ) ).subscribe()
Соблюдая правило максимальной глубины и используя методы, упомянутые выше, сложные цепочки операторов RXJS можно упростить до кода, который легко понять.
Еда на вынос
Хотя вложенный код может показаться удобным решением в краткосрочной перспективе, важно учитывать долгосрочные последствия. Я надеюсь, что методы, упомянутые в этой статье, и видео помогут сделать ваш код легким для чтения, эффективным и удобным для сопровождения.
Будь Нестер Нестер!
Первоначально опубликовано на https://blog.mbcubeconsulting.ca 17 февраля 2023 г.