IlmHamroh
JavaScript Full-stack/12-qism. JavaScript: ilg'or mavzular va kod sifati37/44-dars22 daqiqa
Mundarija (32)

Legacy kod va texnik qarz: testsiz kodni buzmasdan o'zgartirish

Qisqacha: Legacy kod — testi yo'q kod: uni o'zgartirsangiz, nima buzilganini bilmaysiz. Xavfsiz yo'l — avval kodning hozirgi xulqini testlar bilan qotirish, test qo'yib bo'lmasa — kodga kichik tikuv (seam) ochish, keyin o'zgartirish. Texnik qarz — "keyin tuzataman" deb qoldirilgan ish; u ko'rinadigan ro'yxatda yashashi kerak. Katta tizim noldan qayta yozilmaydi — qism-qism almashtiriladi.

Bu darsda

  • "Legacy kod" va "texnik qarz" nimani anglatishini va qarz turlarini tushuntira olasiz.
  • Xulqni qotiruvchi (characterization) test yozasiz — kod "to'g'ri" emas, hozir qanday ishlashini qotiradigan test.
  • Testga xalaqit beradigan bog'liqlikni tikuv (seam) bilan ajratasiz.
  • Katta o'zgarish uchun strangler naqshini va skaut qoidasini qo'llay olasiz, "noldan qayta yozamiz" taklifiga asosli javob berasiz.
  • vazifalar ga TEXNIK-QARZ.md ro'yxati va fetch tikuvini qo'shasiz.

Oldin bilishingiz kerak: Code smell va refactoring, Birinchi avtomatik test: node:test va assert, Asinxron naqshlar.

1. Nega bu kerak?

Sardor «Bahor»da uch oydan beri ishlaydi. Bugun Jasur aka yangi ish berdi: "Buyurtma hisobiga yangi chegirma qo'sh — 10 tadan ko'p taom olgan mehmonga choy bepul. Faqat hech narsani buzma, kassa shu kod bilan ishlaydi".

Sardor hisob.js faylini ochdi. Kodni ikki yil oldin boshqa dasturchi yozgan, u allaqachon ketgan. Izoh yo'q, test yo'q, nomlar bir harfli. Har qatorda savol tug'iladi: "Bu || 1 nega kerak? Buni olib tashlasam, nima bo'ladi?".

Sardor ikki yo'lni ko'radi. Birinchisi — ehtiyotkorlik bilan bitta qator qo'shib, "ishlasa kerak" deb umid qilish. Ikkinchisi — "bu kod dahshat, hammasini qayta yozaman". Ikkalasi ham xavfli. Birinchisida u kassa hisobini sezmasdan buzishi mumkin. Ikkinchisida haftalar ketadi va eski kod "biladigan" mayda qoidalar yo'qoladi.

Bu dars — uchinchi yo'l haqida. Real ishda siz yangi kod yozishdan ko'ra mavjud kodni o'zgartirishga ko'proq vaqt sarflaysiz. Shuning uchun begona, eski, testsiz kod bilan ishlay olish — kasbning asosiy ko'nikmalaridan biri.

Bizning vazifalar ilovamizda ham shunday joylar bor. U endigina ikki oylik, lekin asosiy.js ning birorta testi yo'q. Darsning oxirida shu qarzni ko'rinadigan qilamiz va api.js ga birinchi tikuvni ochamiz.

2. Legacy kod nima

2.1 Eski emas — testsiz

Legacy kod (legacy code) — so'zma-so'z "meros qolgan kod". Kundalik nutqda "eski, chalkash kod" ma'nosida ishlatiladi. Lekin dasturchilar orasida eng foydali ta'rifni Michael Feathers bergan ("Working Effectively with Legacy Code" kitobi, 2004): legacy kod — testi yo'q kod.

Nega yoshi emas, test? Chunki muammo kodning yoshida emas, uni o'zgartirish qo'rqinchli ekanida. Testi bor o'n yillik kodni bemalol o'zgartirasiz: biror narsa buzilsa, test darhol aytadi. Testi yo'q kechagi kodni o'zgartirganda esa natijani faqat qo'lda tekshirasiz — va hamma holatni tekshirishga ulgurmaysiz.

Bu ta'rif bo'yicha vazifalar ning bir qismi ham legacy:

Modul Testi Holati
royxat.js, paket.js, saqlash.js bor (node:test) xavfsiz o'zgartiriladi
api.js bor, lekin global fetch ni almashtiradi qisman
asosiy.js, render.js, sinxron.js yo'q legacy

2.2 Legacy kodning tuzog'i

Legacy kod bilan ishlashda bir "tovuq va tuxum" muammosi bor:

  • kodni xavfsiz o'zgartirish uchun test kerak;
  • test yozish uchun ko'pincha kodni o'zgartirish kerak — chunki u test qo'yishga xalaqit beradigan qilib yozilgan.

Masalan, render.js ni Node'da testga ulab ko'ramiz. Bitta qator — import:

js
// sinov-render.mjs (vaqtinchalik fayl)
import { render } from "./assets/js/render.js";
console.log(typeof render);

Konsolda:

text
Xotirani o'qib bo'lmadi: ReferenceError
file:///…/assets/js/render.js:12
export const forma = document.getElementById("yangi-forma");
                     ^

ReferenceError: document is not defined

Biz hali hech qanday funksiyani chaqirmadik. Lekin modul import paytidayoq document dan element qidiradi, Node'da esa document yo'q. Birinchi qator — holat.js orqali yuklangan saqlash.js ning ogohlantirishi: u ham import paytida localStorage ni o'qishga urindi.

sinxron.js bilan boshqacha bo'ladi — xato yo'q, lekin jarayon tugamaydi:

text
$ timeout 8 node sinov-sinxron.mjs
import tugadi: function
$ echo $?
124

Modul import paytida new BroadcastChannel("vazifalar") ochadi. Ochiq kanal Node jarayonini tirik ushlab turadi. 8 soniyadan keyin timeout buyrug'i jarayonni majburan to'xtatdi — 124 kodi shuni bildiradi (Exit code). Test ham xuddi shunday "osilib" qoladi.

Ikkala holatda ham kod ishlaydi — brauzerda hech qanday muammo yo'q. U shunchaki testga moslab yozilmagan. Legacy kod bilan ishlashning asosiy mahorati — shu tuzoqdan chiqish.

Tekshirib ko'ring: Uch yil oldin yozilgan, lekin 200 ta testi bor modul legacy kodmi? Kecha yozilgan, testsiz modul-chi?

Javob

Feathers ta'rifi bo'yicha birinchisi legacy emas: uni o'zgartirsangiz, testlar buzilgan joyni darhol ko'rsatadi. Kechagi testsiz modul esa — legacy: har o'zgarishdan keyin nima buzilganini faqat qo'lda tekshirasiz. Muhimi yosh emas, o'zgartirish qanchalik xavfsiz ekani.

3. Texnik qarz

3.1 Qarz va uning "foizi"

Texnik qarz (technical debt) — tezroq natija uchun ataylab yoki bilmasdan qoldirilgan ish, keyin u "foiz" bilan qaytadi. Metaforani Ward Cunningham 1992-yilda taklif qilgan.

Hayotiy o'xshatish. Jasur aka bayram oldidan do'kondan guruchni nasiyaga oldi — oshxona ishlab turdi, bu to'g'ri qaror. Lekin har oy qarzni to'lamasa, ustiga jarima qo'shiladi. Bir kun kelib daromadning yarmi jarimaga ketadi.

Kodda "foiz" — har o'zgarish sekinlashishi. Testsiz modulga har tegishda qo'lda tekshirish uchun yarim soat ketadi. Bir xil bilim uch joyda yozilgan bo'lsa — uchalasini topish kerak, bittasini unutsangiz, xato chiqadi. Qarz qancha uzoq tursa, foiz shuncha ko'payadi.

Qarz — o'z-o'zidan yomon narsa emas. Ko'rgazmaga ertaga ulgurish uchun soddalashtirilgan kod yozish — oqilona qarz. Yomoni — qarzni unutish.

3.2 Qarz turlari

Martin Fowler qarzni ikki savol bilan ajratadi: "ataylab olindimi?" va "o'ylab olindimi?".

O'ylab (ehtiyotkor) O'ylamay (beparvo)
Ataylab "Hozir chiqaramiz, keyin tuzatamiz" — ro'yxatga yozildi "Dizaynga vaqtimiz yo'q"
Bilmasdan "Endi tushundik, qanday yozish kerak ekan" "Modul nima o'zi?"

Eng yaxshisi — chap yuqori katak: qarz ongli olingan va yozib qo'yilgan. O'ng pastki katak — bilim yetishmasligidan, uni o'rganish bilan kamaytirasiz. Chap pastki katak ham normal: har qanday jamoa ish davomida "endi bilsak, boshqacha yozardik" deydi.

Qarzning qayerda to'planishiga qarab ham ajratish foydali:

  • kod qarzi — takror, uzun funksiya, noaniq nom (Code smell va refactoring darsidagi hidlar);
  • test qarzi — testsiz modullar, faqat qo'lda tekshiriladigan ssenariylar;
  • hujjat qarzi — README eskirgan, "nega shunday" hech qayerda yozilmagan;
  • bog'liqlik qarzi — yangilanmagan npm paketlar, eskirgan brauzer API'lari;
  • arxitektura qarzi — modullar noto'g'ri bo'lingan, bir o'zgarish o'nta faylga tegadi.

3.3 Qarzni ko'rinadigan qilish

Ko'rinmaydigan qarz to'lanmaydi. Kodda // TODO: keyin tuzatish izohlari tarqalib yotsa, ularni hech kim yig'ib ko'rmaydi. Shuning uchun qarz bitta joyda yoziladi: loyiha ildizidagi faylda yoki GitHub Issues'da (Issues va Projects).

Har qatorda to'rt narsa bo'lsin:

  1. Nima — aniq joy va muammo.
  2. Nega qoldi — qarz olingan sabab. Bu eng muhim ustun: keyingi dasturchi "nega shunday?" deb vaqt yo'qotmaydi.
  3. Ustuvorlik — qanchalik og'ir.
  4. Qachon — qaysi shart bajarilganda to'lanadi.

vazifalar ning TEXNIK-QARZ.md faylidan bitta qator:

# Nima Nega qoldi Ustuvorlik
3 sinxron.js BroadcastChannel ni import paytida ochadi — Node'da jarayon tugamay qoladi (o'lchandi: import dan keyin 8 s kutib, timeout bilan to'xtatildi) Kanal bitta va doim kerak; test yozilmagan O

Telefonda jadval tor bo'lgani uchun "Qachon" ustunini alohida yozamiz: "Kanalni birinchi tarqat/tingla da ochish yoki kanalni tashqaridan berish (tikuv)". E'tibor bering: "o'lchandi" degan so'z bor — yuqorida ko'rgan timeout 8 tajribasi. Qarz ro'yxatiga taxmin emas, tekshirilgan fakt yoziladi.

Tekshirib ko'ring: Nega "Nega qoldi" ustuni bo'lmasa, ro'yxat foydasi kamayadi?

Javob

Sababsiz qator "bu kod yomon" degan shikoyatga aylanadi. Keyingi dasturchi uni ko'rib, darhol tuzatmoqchi bo'ladi va qarz olinishiga sabab bo'lgan cheklovga (masalan, xulq o'zgarib ketishiga) duch keladi. Sabab yozilgan bo'lsa, u qachon va qanday tuzatish xavfsiz ekanini biladi.

4. Testsiz kodni xavfsiz o'zgartirish

4.1 Besh qadam

Feathers legacy kodni o'zgartirish uchun bir algoritm taklif qiladi:

flowchart TD
  A["1. O'zgarish joyini toping"] --> B["2. Test qo'yiladigan<br/>joyni toping"]
  B --> C{"Bog'liqlik testga<br/>xalaqit beradimi?"}
  C -- "ha" --> D["3. Tikuv oching"]
  C -- "yo'q" --> E["4. Xulqni qotiruvchi<br/>testlar yozing"]
  D --> E
  E --> F["5. O'zgartiring:<br/>testlar yashil qolsin"]

Diagrammaga qarang: o'zgartirish — oxirgi qadam. Undan oldingi hamma ish xavfsizlik to'ri tortishga ketadi. Sirk artisti ham avval to'rni tortadi, keyin arqonga chiqadi.

4.2 Xulqni qotiruvchi test

Sardorning hisob.js fayli mana bu (eski kod: nomlari bir harfli, maydonlari inglizcha):

js
// hisob.js — 2024-yilda yozilgan, muallifi boshqa ishga o'tgan
export function calculate(b) {
  let s = 0;
  for (let i = 0; i < b.length; i++) {
    s += b[i].price * (b[i].qty || 1);
  }
  if (s > 200000) s = s - s * 0.1;
  if (b.length > 5) s -= 5000;
  return Math.round(s);
}

Avval kod hozir nima qilishini bilib olamiz. Taxmin qilmaymiz — chaqirib ko'ramiz:

js
function calculate(b) {
  let s = 0;
  for (let i = 0; i < b.length; i++) {
    s += b[i].price * (b[i].qty || 1);
  }
  if (s > 200000) s = s - s * 0.1;
  if (b.length > 5) s -= 5000;
  return Math.round(s);
}

console.log(calculate([])); // 0
console.log(calculate([{ price: 35000, qty: 2 }])); // 70000
console.log(calculate([{ price: 35000, qty: 0 }])); // 35000
console.log(calculate([{ price: 50000, qty: 4 }])); // 200000
console.log(calculate([{ price: 35000, qty: 6 }])); // 189000
console.log(calculate(Array(6).fill({ price: 5000 }))); // 25000

Uchinchi qator kutilmagan: qty: 0 (soni nol) bo'lsa ham, osh bir porsiya deb hisoblandi. Sabab — b[i].qty || 1: 0 "yolg'onsimon" qiymat, shuning uchun || o'ng tomondagi 1 ni oladi (Mantiqiy operatorlar va qisqa tutashuv darsidagi tuzoq). To'rtinchi qator ham qiziq: aynan 200 000 so'mda chegirma yo'q, chunki shart > — >= emas.

Bular xatomi yoki ataylabmi? Bilmaymiz. Balki kassir "0" kiritib, bir porsiyani shunday belgilashga o'rgangandir. Shuning uchun hozircha "tuzatmaymiz" — faqat yozib qo'yamiz.

Xulqni qotiruvchi test (characterization test) — kodning hozirgi xulqini, to'g'rimi-noto'g'rimi, aynan qotiradigan test. U savolga javob beradi: "o'zgarishimdan keyin kod avvalgidek ishlayaptimi?". Birinchi avtomatik test darsida vazifalar uchun shunday testlarni yozgan edik. Bu safar ularni noldan, notanish kodga yozamiz.

Yozish usuli g'alati ko'rinadi: kutilgan qiymatni ataylab noto'g'ri yozasiz va testning o'zi to'g'ri qiymatni aytadi.

js
test("qty: 0 bo'lsa", () => {
  assert.equal(calculate([{ price: 35000, qty: 0 }]), 0);
});

node --test chiqishidan parcha:

text
✖ qty: 0 bo'lsa (5.1676ms)
  AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:

  35000 !== 0

Tarjimasi: "Qiymatlar qat'iy teng bo'lishi kutilgan edi: 35000 0 ga teng emas". Chapdagi — haqiqiy natija (35 000), o'ngdagi — siz yozgan taxmin. Endi 0 ni 35000 ga almashtirasiz va test sababini nomiga yozasiz:

js
// tekshiruv/hisob.test.js
// Xulqni qotiruvchi testlar: "to'g'ri" xulq emas, HOZIRGI xulq
import { test } from "node:test";
import assert from "node:assert/strict";
import { calculate } from "../hisob.js";

test("bo'sh buyurtma — 0", () => {
  assert.equal(calculate([]), 0);
});

test("qty: 0 — bitta porsiya deb hisoblanadi (qarz)", () => {
  assert.equal(calculate([{ price: 35000, qty: 0 }]), 35000);
});

test("aynan 200 000 — chegirmasiz", () => {
  assert.equal(calculate([{ price: 50000, qty: 4 }]), 200000);
});

test("200 000 dan ko'p — 10% chegirma", () => {
  assert.equal(calculate([{ price: 35000, qty: 6 }]), 189000);
});

test("6 ta qator — yana 5 000 so'm kam", () => {
  assert.equal(calculate(Array(6).fill({ price: 5000 })), 25000);
});

Natija: ℹ tests 5, ℹ pass 5, ℹ fail 0. Endi Sardorda xavfsizlik to'ri bor. Yangi chegirmani qo'shgach, beshala test yashil qolsa — eski xulq buzilmagan.

Qaysi holatlarni tanlash kerak? Chegaralarni: bo'sh kirish, nol, aynan chegara (200000), chegaradan biroz yuqori, shartlar birga ishlagan holat. Kodning har if i va har || i kamida bitta test oladi.

Diqqat: Xulqni qotiruvchi testda "xatoni" tuzatib qo'ymang. qty: 0 uchun 0 ni kutadigan test yozsangiz, test qizil bo'ladi va siz kodni "tuzatasiz" — kassada nima o'zgarishini bilmasdan. G'alati xulq — alohida qaror: qarz ro'yxatiga yoziladi, egasi bilan gaplashiladi va alohida o'zgarish sifatida tuzatiladi.

4.3 Tikuv (seam)

Ba'zi kodga test qo'yib bo'lmaydi — u o'zi tanlamagan narsaga bog'langan. Masalan, «Bahor» ochiqligini tekshiradigan funksiya:

js
// Oldin: vaqtni o'zi oladi
function isOpen() {
  const hour = new Date().getHours();
  return hour >= 7 && hour < 23;
}

Uni qanday tekshirasiz? "Soat 06:59 da" holatini sinash uchun ertalab turib testni ishga tushirishingiz kerak. Funksiya hozirgi vaqtga qattiq bog'langan.

Tikuv (seam) — kodni tahrirlamasdan, uning xulqini tashqaridan almashtirish mumkin bo'lgan joy. Kiyimdagi chok kabi: kiyimni qayta bichmasdan, chokni ochib, ichiga boshqa mato qo'yish mumkin. JavaScript'da eng oddiy tikuv — parametr:

js
// Keyin: vaqt — parametr, standart qiymati hozirgi vaqt
function isOpen(now = new Date()) {
  const hour = now.getHours();
  return hour >= 7 && hour < 23;
}

console.log(isOpen(new Date("2026-10-05T06:59"))); // false
console.log(isOpen(new Date("2026-10-05T07:00"))); // true
console.log(isOpen(new Date("2026-10-05T23:00"))); // false

Ilova isOpen() ni avvalgidek argumentsiz chaqiradi — standart qiymat ishlaydi, xulq o'zgarmadi. Test esa istalgan vaqtni beradi. Bu Asinxron naqshlar darsida ko'rgan "bog'liqlikni tashqaridan berish" (dependency injection) g'oyasining eng kichik ko'rinishi.

Tikuvning boshqa turlari ham bor: modulni boshqa modul bilan almashtirish, obyekt metodini testda almashtirish (mock.method). Lekin boshlash uchun parametr yetarli — u eng tushunarlisi.

4.4 Tikuv xulqni o'zgartirib yubormasin

Tikuv "kichik va xavfsiz" ko'rinadi, lekin unda ham tuzoq bor. vazifalar dagi saqlash.js ga xuddi shunday tikuv qo'shmoqchi bo'ldik:

js
// ❌ xulqni o'zgartiradi
export function saqla(royxat, ombor = localStorage) {
  try {
    ombor.setItem(SAQLASH_KALITI, eksportMatni(royxat));

Muammo — standart qiymat qachon hisoblanishida. ombor = localStorage funksiya chaqirilganda, try dan oldin hisoblanadi. Cookie'lar bloklangan brauzerda localStorage ga murojaatning o'zi SecurityError tashlaydi. Oldin bu xato try ichida ushlanardi va ilova ishlashda davom etardi. Tikuvdan keyin u try dan tashqarida chiqadi — ilova ochilmay qoladi.

Shuning uchun saqlash.js ga tikuv hozircha qo'shilmadi. Bu qaror TEXNIK-QARZ.md ning 4-qatorida sababi bilan yozilgan. Qoida: tikuv qo'shgandan keyin ham eski testlar o'zgarmay o'tishi kerak — ular xulq o'zgarmaganini isbotlaydi.

Tekshirib ko'ring: isOpen(now = new Date()) dagi standart qiymat qachon hisoblanadi: funksiya e'lon qilinganda yoki har chaqirilganda?

Javob

Har chaqirilganda, faqat argument berilmagan bo'lsa. Shuning uchun isOpen() har safar o'sha paytdagi vaqtni oladi — eski xulq bilan bir xil. Agar u e'lon paytida bir marta hisoblanganida edi, ilova ertalab ochilgan vaqtni kun bo'yi ishlatardi.

5. Katta o'zgarishlar: strangler naqshi

Kichik o'zgarish uchun yuqoridagi besh qadam yetarli. Lekin butun qismni almashtirish kerak bo'lsa-chi? Masalan, «Bahor»ning eski bron tizimini yangisiga.

Martin Fowler 2004-yilda bunga nom bergan: strangler naqshi (strangler fig pattern). Tropik o'rmonlarda bir xil anjir daraxti boshqa daraxtning tepasida o'sib, ildizlarini pastga tushiradi. Yillar davomida u eski daraxtni o'rab oladi, oxiri eski daraxt chirib ketadi — joyida yangisi turadi. Muhimi: hech bir kun "daraxt yo'q" bo'lmaydi.

Dasturda bu shunday ishlaydi:

flowchart LR
  F["Foydalanuvchi"] --> Y["Yo'naltiruvchi<br/>qatlam"]
  Y -->|"bron, menyu"| N["Yangi kod"]
  Y -->|"qolgan hammasi"| E["Eski kod"]
  1. Eski tizim oldiga yo'naltiruvchi qatlam qo'yiladi. Avval u hamma narsani eski kodga uzatadi.
  2. Bitta kichik qism (masalan, menyu) yangidan yoziladi. Qatlam menyu so'rovlarini yangi kodga yuboradi.
  3. Yangi qism ishonchli ishlaganini ko'rgach — keyingisi. Har qadamdan keyin ilova ishlab turadi.
  4. Eski kodga hech kim murojaat qilmay qo'ygach, u o'chiriladi.

Siz bu naqshni allaqachon qo'llagansiz. 11-qismda vazifalar v2 dan v3 ga bir kunda o'tmadik: avval saqlash.js, keyin marshrut.js, keyin sinxron.js, keyin api.js. Har qadamdan keyin ilova ishlab turdi.

6. Skaut qoidasi va qayta yozish

6.1 Skaut qoidasi

Robert Martin "Clean Code" kitobida (2008) skautlar qoidasini dasturchilarga moslagan. Skaut qoidasi (boy scout rule): kodni topganingizdan biroz tozaroq qoldiring. Sardor hisob.js ga chegirma qo'shayotib, b ni items ga, s ni total ga o'zgartirsa — bu skaut qoidasi.

Qoidaning chegarasi ham bor: "biroz". Tegilgan joyni tozalash — ha. Yangi imkoniyat qo'shayotib butun faylni qayta yozish — yo'q. Bunday PR'ni ko'rib chiqib bo'lmaydi: qaysi o'zgarish xulqni o'zgartirdi, qaysi biri faqat tozaladi — aralashib ketadi. To'g'ri tartib: alohida commit (refactor:), testlar yashil, keyin alohida commit (feat:). Bu Yaxshi commit darsidagi atomar commit g'oyasi.

6.2 "Noldan qayta yozamiz"

Legacy kodni ko'rgan har bir dasturchida bu istak tug'iladi. Javob — deyarli hech qachon.

Joel Spolsky 2000-yilda bu haqda mashhur maqola yozgan va Netscape brauzerini misol qilgan. Kompaniya brauzerni noldan qayta yozishga qaror qildi. Yangi versiya chiqquncha taxminan uch yil o'tdi, bu orada raqobatchilar bozorni egallab oldi.

Nega qayta yozish deyarli doim kutilganidan qimmat?

  • Eski kodning "chalkash" joylari ko'pincha yillar davomida tuzatilgan xatolar. || 1 ortida biror kassirning shikoyati turgan bo'lishi mumkin. Yangi kod bu bilimni bilmaydi.
  • Qayta yozish davomida eski tizim ham yashaydi — unga yangi talablar keladi. Ikki tizimni bir vaqtda yuritasiz.
  • Yangi kodda yangi xatolar bo'ladi. Eskisinikini esa allaqachon bilasiz.

Qachon qayta yozish o'rinli? Modul kichik va xulqi testlar bilan qotirilgan bo'lsa — qayta yozish oddiy refaktorga aylanadi. Yoki platformaning o'zi yo'qolsa: Adobe Flash 2020-yil oxirida qo'llab-quvvatlanmay qoldi va unda yozilgan o'yinlarni boshqa texnologiyaga ko'chirishdan boshqa yo'l qolmadi. Bunday holatda ham strangler naqshi bilan, qism-qism.

Tekshirib ko'ring: Jamoa rahbari: "asosiy.js 400 qator, hech kim tushunmaydi, keyingi hafta noldan yozamiz" dedi. Qanday savollar berasiz?

Javob

"Hozirgi xulqni qaysi testlar qotiradi?" (asosiy.js da test yo'q — yangi kod eskisidek ishlashini qanday bilamiz?). "Qayta yozish davomida kelgan yangi talablar qayerga yoziladi?". "Butun faylni emas, bitta funksiyani ajratib, testga ulasak bo'lmaydimi?". Yaxshi javob — avval testlar, keyin qism-qism almashtirish.

7. Ko'p uchraydigan xatolar

7.1 Testsiz "kichik" o'zgarish

"Bitta qator-ku, nima buzilardi?" — legacy kodda eng ko'p uchraydigan xato. Bitta qator kassadagi hisobni o'zgartirishi mumkin. Tuzatish: o'zgarish joyiga kamida bitta xulqni qotiruvchi test, keyin o'zgarish.

7.2 Refaktor va yangi imkoniyat bitta commitda

Test qizil bo'ldi — sabab tozalashdami yoki yangi kodda? Bilib bo'lmaydi. Tuzatish: avval refactor: commit (testlar yashil), keyin feat: commit.

7.3 G'alati xulqni sezdirmay "tuzatish"

Testda qty: 0 uchun 0 kutildi, kod "tuzatildi" — kassir bir porsiyani endi boshqacha kiritishi kerak, lekin bundan xabari yo'q. Tuzatish: g'alati xulqni avval qotiring, qarz ro'yxatiga yozing va egasi bilan kelishib, alohida o'zgartiring.

7.4 Tarqoq TODO izohlari

O'nlab fayllarda // TODO — hech kim ularni ko'rmaydi va ustuvorligini bilmaydi. Tuzatish: bitta ro'yxat (TEXNIK-QARZ.md yoki Issues) — nima, nega, ustuvorlik, qachon.

7.5 Tikuv xulqni o'zgartirdi

Standart parametr try dan tashqarida hisoblandi, ilova cookie'siz brauzerda ochilmay qoldi. Tuzatish: tikuvdan keyin eski testlar o'zgarishsiz o'tishi shart. O'tmasa — tikuv joyini o'zgartiring yoki qarz sifatida qoldiring.

8. Mashqlar

1-mashq (oson): Qarz qaysi katakda?

Fowler jadvalidagi katakni aniqlang (ataylab/bilmasdan, o'ylab/o'ylamay):

  1. "Ko'rgazmaga ertaga ulgurish uchun bron formasini tekshiruvsiz chiqaramiz. Dushanba kuni tekshiruv qo'shamiz — Issues'ga yozdim."
  2. "Testmi? Vaqt yo'q, ishlasa bo'ldi."
  3. "Ikki oy oldin render.js ni import paytida DOM'ga bog'ladik. Test yozishni boshlagandagina bu xalaqit berishini tushundik."
Yechim
  1. Ataylab va o'ylab: qarz ongli olindi va yozib qo'yildi. Eng sog'lom holat.
  2. Ataylab, lekin o'ylamay: oqibat haqida o'ylanmagan va hech qayerga yozilmagan.
  3. Bilmasdan, lekin o'ylab: o'sha paytda yaxshiroq yo'lni bilmagansiz, endi bildingiz. Uni TEXNIK-QARZ.md ga yozish kerak — vazifalar da aynan shunday qilindi (2-qator).

2-mashq (o'rta): Yana ikki qotiruvchi test

calculate uchun ikki test qo'shing: (1) qty umuman yo'q bo'lsa ({ price: 28000 }); (2) 6 ta qator va jami 200 000 dan ko'p bo'lsa — ikkala chegirma birga qanday ishlaydi. Avval kutilgan qiymatni ataylab 0 deb yozing va haqiqiysini testdan oling. Hisoblash tartibi kodning o'zida yozilgan — avval foiz, keyin 5 000.

Yechim
js
test("qty yo'q — bitta porsiya", () => {
  assert.equal(calculate([{ price: 28000 }]), 28000);
});

test("6 qator, 240 000 — avval 10%, keyin 5 000", () => {
  assert.equal(calculate(Array(6).fill({ price: 40000 })), 211000);
});

Ikkinchi testni qo'lda tekshiramiz: 6 × 40 000 = 240 000; 10% chegirma → 216 000; 6 ta qator → yana 5 000 kam → 211 000. Tartib muhim: agar kod avval 5 000 ayirganda edi, natija 211 500 bo'lardi. Aynan shunday nozik farqni qotiruvchi test saqlaydi.

3-mashq (qiyin): Bron vaqtiga tikuv

«Bahor» bron qabul qiladigan vaqt — 10:00 dan 22:00 gacha. Eski funksiya:

js
function isBookingOpen() {
  const now = new Date();
  const minutes = now.getHours() * 60 + now.getMinutes();
  return minutes >= 10 * 60 && minutes < 22 * 60;
}

Unga tikuv qo'shing (ilova uni avvalgidek argumentsiz chaqirsin) va uchta chegara holatini konsolga chiqaring: 09:59, 10:00, 22:00.

Yechim
js
function isBookingOpen(now = new Date()) {
  const minutes = now.getHours() * 60 + now.getMinutes();
  return minutes >= 10 * 60 && minutes < 22 * 60;
}

const day = "2026-10-05";
console.log(isBookingOpen(new Date(`${day}T09:59`))); // false
console.log(isBookingOpen(new Date(`${day}T10:00`))); // true
console.log(isBookingOpen(new Date(`${day}T22:00`))); // false

Faqat birinchi qator o'zgardi: const now = new Date() parametrga ko'chdi. Ichki mantiq tegilmadi. "2026-10-05T09:59" — vaqt zonasi ko'rsatilmagan satr, u mahalliy vaqt deb o'qiladi (Date va vaqt zonalari). Shuning uchun getHours() istalgan kompyuterda 9 qaytaradi.

4-mashq: Vazifalar qadami — qarzlar ro'yxati va fetch tikuvi

Bu qadamda ilova xulqi o'zgarmaydi. Ikki ish qilamiz: qarzni ko'rinadigan qilamiz va api.js ga tikuv ochamiz.

  • Branch: chore/texnik-qarz
  • Commit: chore: TEXNIK-QARZ.md; api.js ga fetch tikuvi (seam) qo'shildi

1. TEXNIK-QARZ.md — loyiha ildizida yangi fayl. Unda 9 qator: nima, nega qoldi, ustuvorlik (Y/O/P), qachon. Eng og'irlari:

  • asosiy.js testsiz (import paytida DOM, tinglovchilar, server so'rovi);
  • render.js elementlarni import paytida oladi;
  • sinxron.js BroadcastChannel ni import paytida ochadi — o'lchandi: Node'da import dan keyin jarayon tugamadi (timeout 8 → exit 124);
  • saqlash.js tikuvi nega hozir qo'shilmadi (ombor = localStorage standart qiymati try dan tashqarida hisoblanadi → cookie bloklangan brauzerda ilova ochilmay qoladi — xulq o'zgarardi);
  • ikki tab + server id gipotezasi;
  • kalit xavfsizligi;
  • 9-qator — Toza kod: nomlash darsidagi nomlash qarzi: almashtir uch ma'noda, royxat ikki ma'noda. Nomlar 10–11-qismlarda kanon, ularni o'zgartirish ko'p darsga tegadi — shuning uchun hozir emas, testlar himoyasida katta refaktor bilan.

Fayl oxirida qoidalar: skaut qoidasi va noldan qayta yozmaslik.

Ro'yxat bir martalik emas — u loyiha bilan birga o'sadi: ikki darsdan keyin yana bitta qator (10-qator) qo'shiladi va bittasi to'lanadi.

2. Tikuv (seam) — apiKlient({ …, fetch }). Hozir api.test.js global fetch ni almashtiradi (mock.method(globalThis, "fetch")) — test butun jarayonning globaliga tegadi. Tikuvdan keyin soxta fetch parametr orqali beriladi.

Oldin:

js
export function apiKlient({
  asosiyUrl, kalit, muvaffaqiyatsiz = false,
}) {
  …
// sorovYubor ichida:
javob = await fetch(url, sorov);

Keyin:

js
export function apiKlient({
  asosiyUrl, kalit, muvaffaqiyatsiz = false,
  fetch: fetchFn = globalThis.fetch,
}) {
  const yubor = (url, sozlama) => sorovYubor(fetchFn, url, sozlama);
// sorovYubor ichida — oddiy chaqiruv (obyekt.fetch(...) emas:
// brauzer this'ni tekshiradi, "Illegal invocation")
javob = await fetchFn(url, sorov);

Ishora: fetch: fetchFn = … — destrukturlashda nomni almashtirish va standart qiymat birga (Parametrda destructuring va options obyekti). Parametrning nomi fetch, lekin funksiya ichida u fetchFn deb ataladi — global fetch bilan chalkashmasin.

Yechim

Brauzerda fetch berilmaydi → globalThis.fetch ishlaydi (xulq bir xil). Bitta farq bor: global fetch endi apiKlient chaqirilgan paytda olinadi, har so'rovda emas. asosiy.js o'zgarmadi.

Nega fetchFn(url, sorov) — oddiy chaqiruv? Chrome'da sinab ko'rdik: fetch ni obyekt metodi qilib chaqirsangiz (sozlama.fetch(...)), brauzer xato beradi:

text
TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation

Tarjimasi: "Window da fetch ni bajarib bo'lmadi: noqonuniy chaqiruv". Brauzerning fetch i this window (yoki undefined) bo'lishini talab qiladi, obyekt metodi sifatida chaqirilganda esa this — o'sha obyekt (this: to'rt qoida). Shuning uchun fetch alohida o'zgaruvchiga olinadi va oddiy funksiyadek chaqiriladi.

api.test.js endi globalga tegmaydi: soxta mock.fn() tikuv orqali beriladi.

js
// Chaqiruvlarni yozib boradigan soxta fetch; oxirgisi soxtaKlient'ga
let joriyFetch;
function soxtaFetch(javob = () => new Response("null")) {
  joriyFetch = mock.fn(async (url, sozlama) => javob(url, sozlama));
  return joriyFetch;
}

function soxtaKlient(qoshimcha = {}) {
  return apiKlient({
    asosiyUrl,
    kalit: "k1",
    fetch: joriyFetch,
    ...qoshimcha,
  });
}

soxtaKlient dagi return faylda bitta uzun qator. Bu yerda telefonda o'qish uchun bo'lib ko'rsatdik — keyingi darsda Prettier ham uni aynan shunday bo'lib yozadi.

Yana ikki test qo'shildi — tikuvning o'zi uchun:

js
describe("tikuv (seam)", () => {
  test("fetch berilmasa — globalThis.fetch ishlatiladi", async () => {
    const global = mock.method(globalThis, "fetch", async () =>
      Response.json({ vazifalar: [], versiya: 0 }));
    const api = apiKlient({ asosiyUrl, kalit: "k1" });
    await api.royxat();
    assert.equal(global.mock.callCount(), 1);
  });

  test("berilgan fetch this'siz chaqiriladi", async () => {
    const soxta = soxtaFetch();
    await soxtaKlient().royxat();
    assert.equal(soxta.mock.calls[0].this, undefined);
  });
});

Xulq o'zgarmaganini qanday bilamiz? Tikuvdan oldingi eski testlar (global fetch ni almashtiradigan api.test.js) yangi api.js bilan ham ishga tushirildi — 57/57 o'tdi. Ya'ni eski usulda ham, yangi usulda ham api.js bir xil ishlaydi.

bash
git switch -c chore/texnik-qarz
git add TEXNIK-QARZ.md assets/js/api.js tekshiruv/api.test.js
git commit -m "chore: TEXNIK-QARZ.md; api.js ga fetch tikuvi \
(seam) qo'shildi"
npm test
git push -u origin chore/texnik-qarz
gh pr create --fill
gh pr merge --merge

Natija: npm test — 59/59 (11 suite; api.test 7 → 9); brauzer ssenariysi 45/45. Diff: 3 fayl, +66 −15.

Commit turi — chore: (ilova xulqi o'zgarmadi, yangi imkoniyat ham yo'q). Qarz ro'yxatining 6-qatori ("noma'lum id'da tushunarsiz TypeError") ikki darsdan keyin, JSDoc va // @ts-check darsida to'lanadi — ro'yxatdan chizib tashlanishini o'sha yerda ko'rasiz.

9. Real ishda

  • Ish boshlaganingizda sizga deyarli doim mavjud loyiha beriladi — ko'pincha testi kam, muallifi ketgan. Birinchi haftadagi eng foydali ish: lokal ishga tushirish, testlarni topish, o'zgartiradigan joyingizga xulqni qotiruvchi test yozish.
  • Kod arxeologiyasi. "Bu qator nega bor?" savoliga git blame va git log -S javob beradi: kim, qachon, qaysi commit xabari bilan qo'shgan (Kod arxeologiyasi). || 1 ortidagi kassir shikoyatini shunday topasiz.
  • Qarzni boshqarish. Ko'p jamoalar har sprintda vaqtning bir qismini (masalan, har beshinchi vazifani) qarzga ajratadi. Qarz Issues'da tech-debt belgisi bilan yuradi.
  • Katta ko'chishlar — eski freymvorkdan yangisiga, monolitdan xizmatlarga — deyarli doim strangler naqshi bilan, oylab davom etadi. Arxitektura darajasidagi qarzni keyinroq Evolyutsion arxitektura darsida ko'ramiz.
  • Intervyu: "Testi yo'q kodni qanday o'zgartirasiz?", "Texnik qarz nima va uni qanday boshqarasiz?", "Qachon qayta yozish kerak?" — middle intervyularida tez-tez so'raladi.

Xulosa

  • Legacy kod — testi yo'q kod: muammo yoshida emas, o'zgartirish xavfli ekanida.
  • Texnik qarz — "keyin" qoldirilgan ish; foizi — har o'zgarish sekinlashishi. Qarz bitta ro'yxatda yashaydi: nima, nega, ustuvorlik, qachon.
  • Xavfsiz o'zgartirish: joyni toping → test nuqtasini toping → kerak bo'lsa tikuv → xulqni qotiruvchi testlar → o'zgarish.
  • Xulqni qotiruvchi test hozirgi xulqni yozadi, "to'g'risini" emas. G'alati xulq — alohida qaror.
  • Tikuv — eng oddiysi parametr; tikuvdan keyin eski testlar o'zgarishsiz o'tishi shart.
  • Noldan qayta yozish deyarli doim qimmat: strangler naqshi bilan qism-qism almashtiring, tegilgan joyni biroz tozalang (skaut qoidasi).

Keyingi dars: ESLint va Prettier — kodni ishga tushirmasdan tekshiradigan va avtomatik formatlaydigan vositalarni vazifalar ga ulaymiz.

Manbalar

  • Michael Feathers, "Working Effectively with Legacy Code", Prentice Hall, 2004
  • Ward Cunningham, "The WyCash Portfolio Management System", OOPSLA '92 Experience Report, 1992
  • Martin Fowler, "TechnicalDebtQuadrant" (2009) va "StranglerFigApplication" — martinfowler.com
  • Joel Spolsky, "Things You Should Never Do, Part I", 2000 — joelonsoftware.com
  • Robert C. Martin, "Clean Code", Prentice Hall, 2008
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Legacy kod va texnik qarz: testsiz kodni buzmasdan o'zgartirish — IlmHamroh