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

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

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

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

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

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

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

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

У пере­мен­ной типа «объ­ект» есть одна отли­чи­тель­ная осо­бен­ность: она состо­ит как бы из двух частей — свой­ства и зна­че­ния. Мы про это гово­ри­ли в ста­тье про объ­ек­ты в ООП, и здесь всё то же самое — у объ­ек­та меня­ют­ся толь­ко зна­че­ния свойств. Пояс­ним на при­ме­ре язы­ка 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;

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

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

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

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

Свер­нём эти 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.

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