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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
language: javascript
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(Mask1) == 0){
        // и делаем это элементами списка
        $('<li></li>').addClass('tdItem')
          .attr('data-itemid', key)
          .text(localStorage.getItem(key))
          .appendTo(List1);
      }

      // Обрабатываем второй список
      if(key.indexOf(Mask2) == 0){
        // и делаем это элементами списка
        $('<li></li>').addClass('tdItem')
          .attr('data-itemid', key)
          .text(localStorage.getItem(key))
          .appendTo(List2);
      }

      // Обрабатываем третий список
      if(key.indexOf(Mask3) == 0){
        // и делаем это элементами списка
        $('<li></li>').addClass('tdItem')
          .attr('data-itemid', key)
          .text(localStorage.getItem(key))
          .appendTo(List3);
      }

      // Обрабатываем четвёртый список
      if(key.indexOf(Mask4) == 0){
        // и делаем это элементами списка
        $('<li></li>').addClass('tdItem')
          .attr('data-itemid', key)
          .text(localStorage.getItem(key))
          .appendTo(List4);
      }
    }
  }
}

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

    
language: JavaScript
// Функция, которая берёт из памяти наши задачи и делает из них список

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 раза, читать ста­ло про­ще, и всё меня­ет­ся в одном месте. Кра­со­та.

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

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

    
language: JavaScript
// Следим, когда пользователь напишет новую задачу в первое поле ввода и нажмёт 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 раза зада­ёт­ся обра­бот­чик нажа­тия кла­виш в поле вво­да у каж­дой колон­ки. Оче­вид­но, что про­ще выне­сти повто­ря­ю­щий­ся код в отдель­ную функ­цию и вызы­вать её по мере необ­хо­ди­мо­сти:

    
language: JavaScript
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)

});


Ско­пи­ро­вать код
Код ско­пи­ро­ван
ОПТИМИЗИРОВАННЫЙ КОД СКРИПТА

    
language: JavaScript
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.

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