Когда мы делали свой Трелло-планировщик из Бутстрапа и нашего списка задач, у нас появился спагетти-код. Это значит, что для простоты и скорости решения мы скопировали одинаковые куски кода, которые делают почти одно и то же. У них внутри одинаковая логика, но разные входные данные, и мы их просто скопировали-вставили.
У такого подхода к программированию есть несколько проблем:
- поддерживать такой код сложно, потому что нужно постоянно проверять, во всех ли местах мы внесли нужные изменения;
- расширять функциональность сложно, так как легко запутаться, какую именно часть кода мы расширяем или оптимизируем;
- читать такой код трудно, потому что один и тот же код встречается много раз.
Единственное, когда можно использовать такой подход — если нужно что-то быстро протестировать и понять, работает оно или нет. Если не работает — значит, мы сэкономили время и не стали тратить его на неудачное решение. А если всё работает как нужно, значит, настало время переписать код в нормальном виде. Для этого нам понадобится некоторое подобие объекта, но не на основе класса, а созданное вручную.
Мы хотели ООП, но не смогли
Изначально мы хотели на этом примере показать силу классов и объектов в объектно-ориентированном программировании. Но, разобравшись, поняли, что классы только всё усложнят, не принеся нам существенной пользы.
Поэтому мы будем использовать объекты, но без классов. А силу классов мы показали в игре на Питоне — посмотрите, вам понравится.
Новый тип переменной: объект
У переменной типа «объект» есть одна отличительная особенность: она состоит как бы из двух частей — свойства и значения. Мы про это говорили в статье про объекты в ООП, и здесь всё то же самое — у объекта меняются только значения свойств. Поясним на примере языка 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();
})
Что дальше
Мы только что убрали почти весь спагетти-код из нашего проекта. Почти — потому что у нас осталось два неопрятных фрагмента:
- Когда мы 4 раза подряд объявляем колонки, которые отличаются только номером и названием.
- У нас остались 4 обработчика нажатий, которые делают одинаковые вещи — вызывают одну и ту же функцию с разными параметрами.
Оба фрагмента можно оптимизировать, например, с помощью jQuery.
Попробуйте сделать это сами, пока это не сделали мы :-)