medium

Убираем спагетти-код

Два подхода к упорядочиванию хаоса.

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

У такого подхода к программированию есть несколько проблем:

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

Единственное, когда можно использовать такой подход — если нужно что-то быстро протестировать и понять, работает оно или нет. Если не работает — значит, мы сэкономили время и не стали тратить его на неудачное решение. А если всё работает как нужно, значит, настало время переписать код в нормальном виде. Для этого нам понадобится некоторое подобие объекта, но не на основе класса, а созданное вручную.

Мы хотели ООП, но не смогли

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

Поэтому мы будем использовать объекты, но без классов. А силу классов мы показали в игре на Питоне — посмотрите, вам понравится.

Новый тип переменной: объект

У переменной типа «объект» есть одна отличительная особенность: она состоит как бы из двух частей — свойства и значения. Мы про это говорили в статье про объекты в ООП, и здесь всё то же самое — у объекта меняются только значения свойств. Поясним на примере языка JavaScript.

Сделаем переменную-объект, у которой будет два свойства — имя и возраст:

var person = {
    name: "Миша",
    age: 35
}

Чтобы поменять имя, нужно написать название переменной и через точку указать название свойства, которое хотим изменить:

person.name = "Илья";
person.age = 31;

Свойств у объекта может быть сколько угодно:

var person = {
    name1: "Миша",
    name2: "Саша",
    name3: "Лена",
    age1: 35,
    age2: 21,
    age3: 19
}

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

var person = {
  // Массив names, который состоит из объектов с одинаковым названием свойств
  names: [
    { name: "Миша" },
    { name: "Саша" },
    { name: "Лена" },
  ],
  // Массив ages, который состоит из объектов с одинаковым названием свойств
  ages: [
    { age: 35 },
    { age: 21 },
    { age: 19 },
  ],
}
person.names[0].name = "Наташа";
person.ages[0].age = 31;

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

Собираем дубли

Пройдёмся по нашему старому коду из прошлой статьи и соберём все одинаковые переменные, которые отличаются только цифрами:

  • List(1,2,3,4)
  • Mask(1,2,3,4)
  • element_Id_(1,2,3,4)
  • number_Id_(1,2,3,4)

Первые две переменные задают ссылку на объект и маску для сравнения элементов, а вторые две отвечают за количество задач в колонках.

Свернём эти 16 переменных в один большой объект:

var big_object = {
  lists: [
    {list: $('#tdl1App ul')},
    {list: $('#tdl2App ul')},
    {list: $('#tdl3App ul')},
    {list: $('#tdl4App ul')},
  ],
  masks: [
    {mask: 'tdl1_'},
    {mask: 'tdl2_'},
    {mask: 'tdl3_'},
    {mask: 'tdl4_'},
  ],
  element_id: [
    {el: 0},
    {el: 0},
    {el: 0},
    {el: 0},
    ],
  number_id: [
    {num: 0},
    {num: 0},
    {num: 0},
    {num: 0},
  ] 
}

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

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

Используем цикл

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

// Функция, которая берёт из памяти наши задачи и делает из них список function showTasks(){   // Узнаём размер хранилища   var Storage_size = localStorage.length;   // Если в хранилище что-то есть…   if (Storage_size > 0){     // то берём и добавляем это в задачи       for (var i = 0; i < Storage_size; i++){       var key = localStorage.key(i);       // Обрабатываем первый список       if(key.indexOf(<strong>Mask1</strong>) == 0){         // и делаем это элементами списка         $('<li></li>').addClass('tdItem')           .attr('data-itemid', key)           .text(localStorage.getItem(key))           .appendTo(<strong>List1</strong>);       }       // Обрабатываем второй список       if(key.indexOf(<strong>Mask2</strong>) == 0){         // и делаем это элементами списка         $('<li></li>').addClass('tdItem')           .attr('data-itemid', key)           .text(localStorage.getItem(key))           .appendTo(<strong>List2</strong>);       }       // Обрабатываем третий список       if(key.indexOf(<strong>Mask3</strong>) == 0){         // и делаем это элементами списка         $('<li></li>').addClass('tdItem')           .attr('data-itemid', key)           .text(localStorage.getItem(key))           .appendTo(<strong><b>List3</b></strong>);       }       // Обрабатываем четвёртый список       if(key.indexOf(<strong>Mask4</strong>) == 0){         // и делаем это элементами списка         $('<li></li>').addClass('tdItem')           .attr('data-itemid', key)           .text(localStorage.getItem(key))           .appendTo(<strong>List4</strong>);       }     }   } }

Сделаем то же самое в цикле, используя нашу большую переменную-объект. Для этого мы организуем цикл от 0 до 3 (потому что нумерация элементов массива начинается с нуля) и по очереди проверяем все значения:

// Функция, которая берёт из памяти наши задачи и делает из них список
function showTasks() {
  // Узнаём размер хранилища
  var Storage_size = localStorage.length;
  // Если в хранилище что-то есть…
  if (Storage_size > 0) {
    // то берём и добавляем это в задачи  
    for (var i = 0; i < Storage_size; i++) {
      // Берём очередной элемент из хранилища
      var key = localStorage.key(i);
      // и в новом цикле сразу проверяем, в какую колонку его отправить
      for (var j = 0; j <= 3; j++) {
        // Если объект попадает под фильтр любой из колонок…
        if (key.indexOf(big_object.masks[j].mask) == 0) {
          // отправляем его в соответствующую колонку, за этим следит переменная j
          $('<li></li>').addClass('tdItem')
            .attr('data-itemid', key)
            .text(localStorage.getItem(key))
            .appendTo(big_object.lists[j].list);
        }
      }
    }
  }
}

Код сократился в 4 раза, читать стало проще, и всё меняется в одном месте. Красота.

Делаем отдельную функцию

У нас есть вот такой огромный кусок кода, который делает одно и то же, только с разными элементами.

// Следим, когда пользователь напишет новую задачу в первое поле ввода и нажмёт Enter
$('#tdl1App input').on('keydown', function (e) {
  if (e.keyCode != 13) return;
  var str = e.target.value;
  e.target.value = "";
  // Если в поле ввода было что-то написано — начинаем обрабатывать
  if (str.length > 0) {
    var number_Id_1 = 0;
    List1.children().each(function (index, el) {
      var element_Id_1 = $(el).attr('data-itemid').slice(5);
      if (element_Id_1 > number_Id_1)
        number_Id_1 = element_Id_1;
    })
    number_Id_1++;
    // Отправляем новую задачу сразу в память
    localStorage.setItem(Mask1 + number_Id_1, str);
    // и добавляем её в конец списка
    $('<li></li>').addClass('tdItem')
      .attr('data-itemid', Mask1 + number_Id_1)
      .text(str).appendTo(List1);
  }
});
// Следим, когда пользователь напишет новую задачу во второе поле ввода и нажмёт Enter
$('#tdl2App input').on('keydown', function (e) {
  if (e.keyCode != 13) return;
  var str = e.target.value;
  e.target.value = "";
  // Если в поле ввода было что-то написано — начинаем обрабатывать
  if (str.length > 0) {
    var number_Id_2 = 0;
    List2.children().each(function (index, el) {
      var element_Id_2 = $(el).attr('data-itemid').slice(5);
      if (element_Id_2 > number_Id_2)
        number_Id_2 = element_Id_2;
    })
    number_Id_2++;
    // Отправляем новую задачу сразу в память
    localStorage.setItem(Mask2 + number_Id_2, str);
    // и добавляем её в конец списка
    $('<li></li>').addClass('tdItem')
      .attr('data-itemid', Mask2 + number_Id_2)
      .text(str).appendTo(List2);
  }
});
// Следим, когда пользователь напишет новую задачу в третье поле ввода и нажмёт Enter
$('#tdl3App input').on('keydown', function (e) {
  if (e.keyCode != 13) return;
  var str = e.target.value;
  e.target.value = "";
  // Если в поле ввода было что-то написано — начинаем обрабатывать
  if (str.length > 0) {
    var number_Id_3 = 0;
    List3.children().each(function (index, el) {
      var element_Id_3 = $(el).attr('data-itemid').slice(5);
      if (element_Id_3 > number_Id_3)
        number_Id_3 = element_Id_3;
    })
    number_Id_3++;
    // Отправляем новую задачу сразу в память
    localStorage.setItem(Mask3 + number_Id_3, str);
    // и добавляем её в конец списка
    $('<li></li>').addClass('tdItem')
      .attr('data-itemid', Mask3 + number_Id_3)
      .text(str).appendTo(List3);
  }
});
// Следим, когда пользователь напишет новую задачу в четвёртое поле ввода и нажмёт Enter
$('#tdl4App input').on('keydown', function (e) {
  if (e.keyCode != 13) return;
  var str = e.target.value;
  e.target.value = "";
  // Если в поле ввода было что-то написано — начинаем обрабатывать
  if (str.length > 0) {
    var number_Id_4 = 0;
    List4.children().each(function (index, el) {
      var element_Id_4 = $(el).attr('data-itemid').slice(5);
      if (element_Id_4 > number_Id_4)
        number_Id_4 = element_Id_4;
    })
    number_Id_4++;
    // Отправляем новую задачу сразу в память
    localStorage.setItem(Mask4 + number_Id_4, str);
    // и добавляем её в конец списка
    $('<li></li>').addClass('tdItem')
      .attr('data-itemid', Mask4 + number_Id_4)
      .text(str).appendTo(List4);
  }
});

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

function pressed(e, col) {
  if (e.keyCode != 13) return;
  var str = e.target.value;
  e.target.value = "";
  // Если в поле ввода было что-то написано — начинаем обрабатывать
  if (str.length > 0) {
    // Принудительно обнуляем переменные
    big_object.number_id[col].num = 0;
    big_object.element_id[col].el = 0;
    // Смотрите — вот тут мы вместо объекта List используем элемент массива, который содержит название этого объекта
    big_object.lists[col].list.children().each(function (index, el) {
      big_object.element_id[col].el = $(el).attr('data-itemid').slice(5);
      if (big_object.element_id[col].el > big_object.number_id[col].num)
        big_object.number_id[col].num = big_object.element_id[col].el;
    })
    big_object.number_id[col].num++;
    // Отправляем новую задачу сразу в память
    localStorage.setItem(big_object.masks[col].mask + big_object.number_id[col].num, str);
    // и добавляем её в конец списка
    $('<li></li>').addClass('tdItem')
      .attr('data-itemid', big_object.masks[col].mask + big_object.number_id[col].num)
      .text(str).appendTo(big_object.lists[col].list);
  }
}
// Следим, когда пользователь напишет новую задачу в первое поле ввода и нажмёт Enter
$('#tdl1App input').on('keydown', function (e) {
  pressed(e, 0)
});
// Следим, когда пользователь напишет новую задачу во второе поле ввода и нажмёт Enter
$('#tdl2App input').on('keydown', function (e) {
  pressed(e, 1)
});
// Следим, когда пользователь напишет новую задачу в третье поле ввода и нажмёт Enter
$('#tdl3App input').on('keydown', function (e) {
  pressed(e, 2)
});
// Следим, когда пользователь напишет новую задачу в четвёртое поле ввода и нажмёт Enter
$('#tdl4App input').on('keydown', function (e) {
  pressed(e, 3)
});

var big_object = {
  lists: [
    { list: $('#tdl1App ul') },
    { list: $('#tdl2App ul') },
    { list: $('#tdl3App ul') },
    { list: $('#tdl4App ul') },
  ],
  masks: [
    { mask: 'tdl1_' },
    { mask: 'tdl2_' },
    { mask: 'tdl3_' },
    { mask: 'tdl4_' },
  ],
  element_id: [
    { el: 0 },
    { el: 0 },
    { el: 0 },
    { el: 0 },
  ],
  number_id: [
    { num: 0 },
    { num: 0 },
    { num: 0 },
    { num: 0 },
  ]
}
// Функция, которая берёт из памяти наши задачи и делает из них список
function showTasks() {
  // Узнаём размер хранилища
  var Storage_size = localStorage.length;
  // Если в хранилище что-то есть…
  if (Storage_size > 0) {
    // то берём и добавляем это в задачи  
    for (var i = 0; i < Storage_size; i++) {
      // Берём очередной элемент из хранилища
      var key = localStorage.key(i);
      // и в новом цикле сразу проверяем, в какую колонку его отправить
      for (var j = 0; j <= 3; j++) {
        // Если объект попадает под фильтр любой из колонок…
        if (key.indexOf(big_object.masks[j].mask) == 0) {
          // отправляем его в соответствующую колонку, за этим следит переменная j
          $('<li></li>').addClass('tdItem')
            .attr('data-itemid', key)
            .text(localStorage.getItem(key))
            .appendTo(big_object.lists[j].list);
        }
      }
    }
  }
}
// Сразу вызываем эту функцию, вдруг в памяти уже остались задачи с прошлого раза
showTasks();
function pressed(e, col) {
  if (e.keyCode != 13) return;
  var str = e.target.value;
  e.target.value = "";
  // Если в поле ввода было что-то написано — начинаем обрабатывать
  if (str.length > 0) {
    // Принудительно обнуляем переменные
    big_object.number_id[col].num = 0;
    big_object.element_id[col].el = 0;
    // Смотрите — вот тут мы вместо объекта List используем элемент массива, который содержит название этого объекта
    big_object.lists[col].list.children().each(function (index, el) {
      big_object.element_id[col].el = $(el).attr('data-itemid').slice(5);
      if (big_object.element_id[col].el > big_object.number_id[col].num)
        big_object.number_id[col].num = big_object.element_id[col].el;
    })
    big_object.number_id[col].num++;
    // Отправляем новую задачу сразу в память
    localStorage.setItem(big_object.masks[col].mask + big_object.number_id[col].num, str);
    // и добавляем её в конец списка
    $('<li></li>').addClass('tdItem')
      .attr('data-itemid', big_object.masks[col].mask + big_object.number_id[col].num)
      .text(str).appendTo(big_object.lists[col].list);
  }
}
// Следим, когда пользователь напишет новую задачу в первое поле ввода и нажмёт Enter
$('#tdl1App input').on('keydown', function (e) {
  pressed(e, 0)
});
// Следим, когда пользователь напишет новую задачу во второе поле ввода и нажмёт Enter
$('#tdl2App input').on('keydown', function (e) {
  pressed(e, 1)
});
// Следим, когда пользователь напишет новую задачу в третье поле ввода и нажмёт Enter
$('#tdl3App input').on('keydown', function (e) {
  pressed(e, 2)
});
// Следим, когда пользователь напишет новую задачу в четвёртое поле ввода и нажмёт Enter
$('#tdl4App input').on('keydown', function (e) {
  pressed(e, 3)
});
// По клику на задаче — убираем её из списка
$(document).on('click', '.tdItem', function (e) {
  // Находим задачу, по которой кликнули
  var jet = $(e.target);
  // Убираем её из памяти
  localStorage.removeItem(jet.attr('data-itemid'));
  // и убираем её из списка
  jet.remove();
})

Что дальше

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

  1. Когда мы 4 раза подряд объявляем колонки, которые отличаются только номером и названием.
  2. У нас остались 4 обработчика нажатий, которые делают одинаковые вещи — вызывают одну и ту же функцию с разными параметрами.

Оба фрагмента можно оптимизировать, например, с помощью jQuery.

Попробуйте сделать это сами, пока это не сделали мы :-)

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