Anda sedang membaca petikan daripada buku saya tentang kod bersih, "Mencuci kod anda." Tersedia sebagai PDF, EPUB dan sebagai edisi paperback dan Kindle. Dapatkan salinan anda sekarang.
Mengetahui cara menyusun kod ke dalam modul atau fungsi, dan masa yang sesuai untuk memperkenalkan abstraksi dan bukannya menduplikasi kod, adalah kemahiran penting. Menulis kod generik yang boleh digunakan oleh orang lain dengan berkesan adalah satu lagi kemahiran. Terdapat banyak sebab untuk memisahkan kod seperti yang ada untuk menyimpannya bersama-sama. Dalam bab ini, kita akan membincangkan beberapa sebab ini.
Kami, pembangun, tidak suka melakukan kerja yang sama dua kali. KERING adalah mantra bagi ramai orang. Walau bagaimanapun, apabila kita mempunyai dua atau tiga keping kod yang melakukan perkara yang sama, mungkin masih terlalu awal untuk memperkenalkan abstraksi, tidak kira betapa menggodanya.
Maklumat: Prinsip Jangan ulangi diri sendiri (KERING) menuntut bahawa "setiap pengetahuan mesti mempunyai perwakilan tunggal, tidak jelas, berwibawa dalam sistem", yang sering ditafsirkan sebagai sebarang pertindihan kod adalah verboten sepenuhnya.
Hidup dengan kesakitan pertindihan kod untuk seketika; mungkin ia tidak begitu teruk pada akhirnya, dan kod itu sebenarnya tidak betul-betul sama. Beberapa tahap pertindihan kod adalah sihat dan membolehkan kami mengulang dan mengembangkan kod dengan lebih pantas tanpa rasa takut untuk memecahkan sesuatu.
Sukar juga untuk menghasilkan API yang baik apabila kami hanya mempertimbangkan beberapa kes penggunaan.
Menguruskan kod kongsi dalam projek besar dengan ramai pembangun dan pasukan adalah sukar. Keperluan baharu untuk satu pasukan mungkin tidak berfungsi untuk Pasukan lain dan melanggar kod mereka, atau kita akan mendapat raksasa spageti yang tidak dapat diselenggara dengan berpuluh-puluh syarat.
Bayangkan Pasukan A sedang menambahkan borang ulasan pada halaman mereka: nama, mesej dan butang hantar. Kemudian, Pasukan B memerlukan borang maklum balas, supaya mereka mencari komponen Pasukan A dan cuba menggunakannya semula. Kemudian, Pasukan A juga mahukan medan e-mel, tetapi mereka tidak tahu bahawa Pasukan B menggunakan komponen mereka, jadi mereka menambah medan e-mel yang diperlukan dan memecahkan ciri untuk pengguna Pasukan B. Kemudian, Pasukan B memerlukan medan nombor telefon, tetapi mereka tahu bahawa Pasukan A menggunakan komponen tanpanya, jadi mereka menambah pilihan untuk menunjukkan medan nombor telefon. Setahun kemudian, kedua-dua pasukan membenci satu sama lain kerana melanggar kod satu sama lain, dan komponen itu penuh dengan syarat dan mustahil untuk dikekalkan. Kedua-dua pasukan akan menjimatkan banyak masa dan mempunyai hubungan yang lebih sihat antara satu sama lain jika mereka mengekalkan komponen berasingan yang terdiri daripada komponen kongsi peringkat rendah, seperti medan input atau butang.
Petua: Adalah idea yang baik untuk melarang pasukan lain daripada menggunakan kod kami melainkan ia direka bentuk dan ditandakan sebagai dikongsi. Kapal penjelajah Dependency ialah alat yang boleh membantu menyediakan peraturan sedemikian.
Kadang-kadang, kita perlu melancarkan abstraksi. Apabila kita mula menambah syarat dan pilihan, kita harus bertanya kepada diri sendiri: adakah ia masih merupakan variasi daripada perkara yang sama atau perkara baharu yang harus dipisahkan? Menambah terlalu banyak syarat dan parameter pada modul boleh menjadikan API sukar digunakan dan kod sukar untuk diselenggara dan diuji.
Penduaan adalah lebih murah dan lebih sihat daripada abstraksi yang salah.
Maklumat: Lihat artikel Sandi Metz The Wrong Abstraction untuk penjelasan yang bagus.
Semakin tinggi tahap kod, semakin lama kita perlu menunggu sebelum kita mengabstrakkannya. Abstraksi utiliti peringkat rendah jauh lebih jelas dan stabil daripada logik perniagaan.
Penggunaan semula kod bukan satu-satunya, malah paling penting, sebab untuk mengekstrak sekeping kod ke dalam fungsi atau modul yang berasingan.
Panjang kod sering digunakan sebagai metrik apabila kita harus memisahkan modul atau fungsi, tetapi saiz sahaja tidak menjadikan kod sukar dibaca atau diselenggara.
Memisahkan algoritma linear, walaupun yang panjang, kepada beberapa fungsi dan kemudian memanggilnya satu demi satu jarang menjadikan kod lebih mudah dibaca. Melompat antara fungsi (dan lebih-lebih lagi — fail) adalah lebih sukar daripada menatal, dan jika kita perlu melihat ke dalam setiap pelaksanaan fungsi untuk memahami kod, maka abstraksi itu bukanlah yang betul.
Maklumat: Egon Elbre menulis artikel bagus tentang psikologi kebolehbacaan kod.
Berikut ialah contoh, disesuaikan daripada Blog Pengujian Google:
function createPizza(order) { const pizza = new Pizza({ base: order.size, sauce: order.sauce, cheese: 'Mozzarella' }); if (order.kind === 'Veg') { pizza.toppings = vegToppings; } else if (order.kind === 'Meat') { pizza.toppings = meatToppings; } const oven = new Oven(); if (oven.temp !== cookingTemp) { while (oven.temp < cookingTemp) { time.sleep(checkOvenInterval); oven.temp = getOvenTemp(oven); } } if (!pizza.baked) { oven.insert(pizza); time.sleep(cookTime); oven.remove(pizza); pizza.baked = true; } const box = new Box(); pizza.boxed = box.putIn(pizza); pizza.sliced = box.slicePizza(order.size); pizza.ready = box.close(); return pizza; }
Saya mempunyai begitu banyak soalan tentang API kelas Pizza, tetapi mari lihat penambahbaikan yang dicadangkan oleh pengarang:
function prepare(order) { const pizza = new Pizza({ base: order.size, sauce: order.sauce, cheese: 'Mozzarella' }); addToppings(pizza, order.kind); return pizza; } function addToppings(pizza, kind) { if (kind === 'Veg') { pizza.toppings = vegToppings; } else if (kind === 'Meat') { pizza.toppings = meatToppings; } } function bake(pizza) { const oven = new Oven(); heatOven(oven); bakePizza(pizza, oven); } function heatOven(oven) { if (oven.temp !== cookingTemp) { while (oven.temp < cookingTemp) { time.sleep(checkOvenInterval); oven.temp = getOvenTemp(oven); } } } function bakePizza(pizza, oven) { if (!pizza.baked) { oven.insert(pizza); time.sleep(cookTime); oven.remove(pizza); pizza.baked = true; } } function pack(pizza) { const box = new Box(); pizza.boxed = box.putIn(pizza); pizza.sliced = box.slicePizza(pizza.size); pizza.ready = box.close(); } function createPizza(order) { const pizza = prepare(order); bake(pizza); pack(pizza); return pizza; }
Apa yang dahulunya rumit dan berbelit-belit kini menjadi lebih rumit dan berbelit-belit, dan separuh daripada kod itu hanyalah panggilan fungsi. Ini tidak menjadikan kod lebih mudah untuk difahami, tetapi ia menjadikannya hampir mustahil untuk digunakan. Artikel itu tidak menunjukkan kod lengkap versi pemfaktoran semula, mungkin untuk menjadikan perkara itu lebih menarik.
Pierre “catwell” Chapuis mencadangkan dalam catatan blognya untuk menambah ulasan dan bukannya fungsi baharu:
function createPizza(order) { const pizza = new Pizza({ base: order.size, sauce: order.sauce, cheese: 'Mozzarella' }); if (order.kind === 'Veg') { pizza.toppings = vegToppings; } else if (order.kind === 'Meat') { pizza.toppings = meatToppings; } const oven = new Oven(); if (oven.temp !== cookingTemp) { while (oven.temp < cookingTemp) { time.sleep(checkOvenInterval); oven.temp = getOvenTemp(oven); } } if (!pizza.baked) { oven.insert(pizza); time.sleep(cookTime); oven.remove(pizza); pizza.baked = true; } const box = new Box(); pizza.boxed = box.putIn(pizza); pizza.sliced = box.slicePizza(order.size); pizza.ready = box.close(); return pizza; }
Ini sudah jauh lebih baik daripada versi split. Penyelesaian yang lebih baik ialah menambah baik API dan menjadikan kod lebih jelas. Pierre mencadangkan bahawa memanaskan ketuhar tidak sepatutnya menjadi sebahagian daripada fungsi createPizza() (dan membakar sendiri banyak piza, saya sangat bersetuju!) kerana dalam kehidupan sebenar ketuhar sudah ada dan mungkin sudah panas daripada piza sebelumnya. Pierre juga mencadangkan bahawa fungsi itu harus memulangkan kotak, bukan pizza, kerana dalam kod asal, jenis kotak itu hilang selepas semua penghirisan dan pembungkusan ajaib, dan kami berakhir dengan pizza yang dihiris di tangan kami.
Terdapat banyak cara untuk memasak piza, sama seperti terdapat banyak cara untuk mengekodkan masalah. Hasilnya mungkin kelihatan sama, tetapi sesetengah penyelesaian lebih mudah difahami, diubah suai, digunakan semula dan dipadamkan berbanding yang lain.
Penamaan juga boleh menjadi masalah apabila semua fungsi yang diekstrak adalah sebahagian daripada algoritma yang sama. Kita perlu mencipta nama yang lebih jelas daripada kod dan lebih pendek daripada ulasan — bukan tugas yang mudah.
Maklumat: Kami bercakap tentang mengulas kod dalam bab Elakkan ulasan dan tentang penamaan dalam bab Penamaan adalah sukar.
Anda mungkin tidak akan menemui banyak fungsi kecil dalam kod saya. Dalam pengalaman saya, sebab paling berguna untuk memisahkan kod ialah menukar kekerapan dan menukar sebab.
Mari mulakan dengan tukar kekerapan. Logik perniagaan berubah lebih kerap daripada fungsi utiliti. Adalah wajar untuk memisahkan kod yang sering berubah daripada kod yang sangat stabil.
Borang ulasan yang kita bincangkan sebelum ini dalam bab ini adalah contoh yang pertama; fungsi yang menukar rentetan camelCase kepada kebab-case ialah contoh yang terakhir. Borang ulasan mungkin berubah dan menyimpang dari semasa ke semasa apabila keperluan perniagaan baharu timbul; fungsi penukaran kes tidak mungkin berubah sama sekali dan ia selamat untuk digunakan semula di banyak tempat.
Bayangkan bahawa kami sedang membuat jadual yang kelihatan cantik untuk memaparkan beberapa data. Kami mungkin fikir kami tidak akan memerlukan reka bentuk jadual ini lagi, jadi kami memutuskan untuk menyimpan semua kod untuk jadual dalam satu modul.
Pecut seterusnya, kami mendapat tugas untuk menambah lajur lain pada jadual, jadi kami menyalin kod lajur sedia ada dan menukar beberapa baris di sana. Pecut seterusnya, kita perlu menambah meja lain dengan reka bentuk yang sama. Pecut seterusnya, kita perlu menukar reka bentuk jadual…
Modul jadual kami mempunyai sekurang-kurangnya tiga sebab untuk menukar, atau tanggungjawab:
Ini menjadikan modul lebih sukar untuk difahami dan lebih sukar untuk diubah. Kod pembentangan menambah banyak kata kerja, menjadikannya lebih sukar untuk memahami logik perniagaan. Untuk membuat perubahan dalam mana-mana tanggungjawab, kita perlu membaca dan mengubah suai lebih banyak kod. Ini menjadikannya lebih sukar dan lebih perlahan untuk diulang.
Mempunyai jadual generik sebagai modul berasingan menyelesaikan masalah ini. Sekarang, untuk menambah lajur lain pada jadual, kita hanya perlu memahami dan mengubah suai salah satu daripada dua modul. Kami tidak perlu mengetahui apa-apa tentang modul jadual generik kecuali API awamnya. Untuk menukar reka bentuk semua jadual, kami hanya perlu menukar kod modul jadual generik dan kemungkinan besar tidak perlu menyentuh jadual individu sama sekali.
Walau bagaimanapun, bergantung pada kerumitan masalah, tidak mengapa, dan selalunya lebih baik, bermula dengan pendekatan monolitik dan mengekstrak abstraksi kemudian.
Malah penggunaan semula kod boleh menjadi alasan yang sah untuk memisahkan kod: jika kami menggunakan beberapa komponen pada satu halaman, kami mungkin akan memerlukannya pada halaman lain tidak lama lagi.
Mungkin menarik untuk mengekstrak setiap fungsi ke dalam modulnya sendiri. Walau bagaimanapun, ia juga mempunyai kelemahan:
Saya lebih suka menyimpan fungsi kecil yang hanya digunakan dalam satu modul pada permulaan modul. Dengan cara ini, kita tidak perlu mengimportnya untuk digunakan dalam modul yang sama, tetapi menggunakannya semula di tempat lain akan menjadi janggal.
function createPizza(order) { const pizza = new Pizza({ base: order.size, sauce: order.sauce, cheese: 'Mozzarella' }); if (order.kind === 'Veg') { pizza.toppings = vegToppings; } else if (order.kind === 'Meat') { pizza.toppings = meatToppings; } const oven = new Oven(); if (oven.temp !== cookingTemp) { while (oven.temp < cookingTemp) { time.sleep(checkOvenInterval); oven.temp = getOvenTemp(oven); } } if (!pizza.baked) { oven.insert(pizza); time.sleep(cookTime); oven.remove(pizza); pizza.baked = true; } const box = new Box(); pizza.boxed = box.putIn(pizza); pizza.sliced = box.slicePizza(order.size); pizza.ready = box.close(); return pizza; }
Dalam kod di atas, kami mempunyai komponen (FormattedAddress) dan fungsi (getMapLink()) yang hanya digunakan dalam modul ini, jadi ia ditakrifkan di bahagian atas fail.
Jika kita perlu menguji fungsi ini (dan kita harus!), kita boleh mengeksportnya daripada modul dan mengujinya bersama-sama dengan fungsi utama modul.
Perkara yang sama berlaku untuk fungsi yang bertujuan untuk digunakan hanya bersama dengan fungsi atau komponen tertentu. Menyimpannya dalam modul yang sama menjadikannya lebih jelas bahawa semua fungsi adalah bersama dan menjadikan fungsi ini lebih mudah ditemui.
Faedah lain ialah apabila kami memadamkan modul, kami memadamkan kebergantungannya secara automatik. Kod dalam modul kongsi selalunya kekal dalam pangkalan kod selama-lamanya kerana sukar untuk mengetahui sama ada ia masih digunakan (walaupun TypeScript menjadikannya lebih mudah).
Maklumat: Modul sedemikian kadangkala dipanggil modul mendalam: modul yang agak besar yang merangkumi masalah kompleks tetapi mempunyai API yang ringkas. Lawan daripada modul mendalam ialah modul cetek: banyak modul kecil yang perlu berinteraksi antara satu sama lain.
Jika kita sering terpaksa menukar beberapa modul atau fungsi pada masa yang sama, mungkin lebih baik untuk menggabungkannya menjadi satu modul atau fungsi. Pendekatan ini kadangkala dipanggil kolokasi.
Berikut ialah beberapa contoh kolokasi:
Begini cara pepohon fail berubah dengan colocation:
Separated | Colocated |
---|---|
React components | |
src/components/Button.tsx | src/components/Button.tsx |
styles/Button.css | |
Tests | |
src/util/formatDate.ts | src/util/formatDate.ts |
tests/formatDate.ts | src/util/formatDate.test.ts |
Ducks | |
src/actions/feature.js | src/ducks/feature.js |
src/actionCreators/feature.js | |
src/reducers/feature.js |
Maklumat: Untuk mengetahui lebih lanjut tentang kolokasi, baca artikel Kent C. Dodds.
Aduan biasa mengenai kolokasi ialah ia menjadikan komponen terlalu besar. Dalam kes sedemikian, adalah lebih baik untuk mengekstrak beberapa bahagian ke dalam komponennya sendiri, bersama-sama dengan penanda, gaya dan logik.
Idea kolokasi juga bercanggah dengan pemisahan kebimbangan — idea lapuk yang menyebabkan pembangun web menyimpan HTML, CSS dan JavaScript dalam fail berasingan (dan selalunya dalam bahagian berasingan pepohon fail) untuk terlalu lama, memaksa kami mengedit tiga fail pada masa yang sama untuk membuat perubahan yang paling asas pada halaman web.
Maklumat: sebab perubahan juga dikenali sebagai prinsip tanggungjawab tunggal, yang menyatakan bahawa "setiap modul, kelas, atau fungsi harus mempunyai tanggungjawab ke atas satu bahagian fungsi disediakan oleh perisian dan tanggungjawab itu harus dirangkumkan sepenuhnya oleh kelas.”
Kadangkala, kita perlu bekerja dengan API yang amat sukar untuk digunakan atau terdedah kepada ralat. Sebagai contoh, ia memerlukan beberapa langkah dalam susunan tertentu atau memanggil fungsi dengan berbilang parameter yang sentiasa sama. Ini adalah sebab yang baik untuk mencipta fungsi utiliti untuk memastikan kami sentiasa melakukannya dengan betul. Sebagai bonus, kami kini boleh menulis ujian untuk sekeping kod ini.
Manipulasi rentetan — seperti URL, nama fail, penukaran kes atau pemformatan — adalah calon yang baik untuk abstraksi. Kemungkinan besar, sudah ada perpustakaan untuk perkara yang kami cuba lakukan.
Pertimbangkan contoh ini:
function createPizza(order) { const pizza = new Pizza({ base: order.size, sauce: order.sauce, cheese: 'Mozzarella' }); if (order.kind === 'Veg') { pizza.toppings = vegToppings; } else if (order.kind === 'Meat') { pizza.toppings = meatToppings; } const oven = new Oven(); if (oven.temp !== cookingTemp) { while (oven.temp < cookingTemp) { time.sleep(checkOvenInterval); oven.temp = getOvenTemp(oven); } } if (!pizza.baked) { oven.insert(pizza); time.sleep(cookTime); oven.remove(pizza); pizza.baked = true; } const box = new Box(); pizza.boxed = box.putIn(pizza); pizza.sliced = box.slicePizza(order.size); pizza.ready = box.close(); return pizza; }
Ia mengambil sedikit masa untuk menyedari bahawa kod ini mengalih keluar sambungan fail dan mengembalikan nama asas. Ia bukan sahaja tidak perlu dan sukar dibaca, tetapi ia juga menganggap sambungannya sentiasa tiga aksara, yang mungkin tidak berlaku.
Mari kita tulis semula menggunakan perpustakaan, modul laluan Node.js terbina dalam:
function prepare(order) { const pizza = new Pizza({ base: order.size, sauce: order.sauce, cheese: 'Mozzarella' }); addToppings(pizza, order.kind); return pizza; } function addToppings(pizza, kind) { if (kind === 'Veg') { pizza.toppings = vegToppings; } else if (kind === 'Meat') { pizza.toppings = meatToppings; } } function bake(pizza) { const oven = new Oven(); heatOven(oven); bakePizza(pizza, oven); } function heatOven(oven) { if (oven.temp !== cookingTemp) { while (oven.temp < cookingTemp) { time.sleep(checkOvenInterval); oven.temp = getOvenTemp(oven); } } } function bakePizza(pizza, oven) { if (!pizza.baked) { oven.insert(pizza); time.sleep(cookTime); oven.remove(pizza); pizza.baked = true; } } function pack(pizza) { const box = new Box(); pizza.boxed = box.putIn(pizza); pizza.sliced = box.slicePizza(pizza.size); pizza.ready = box.close(); } function createPizza(order) { const pizza = prepare(order); bake(pizza); pack(pizza); return pizza; }
Kini, jelas perkara yang berlaku, tiada nombor ajaib dan ia berfungsi dengan sambungan fail dalam sebarang panjang.
Calon lain untuk abstrak termasuk tarikh, keupayaan peranti, borang, pengesahan data, pengantarabangsaan dan banyak lagi. Saya mengesyorkan mencari perpustakaan sedia ada sebelum menulis fungsi utiliti baharu. Kami sering memandang rendah kerumitan fungsi yang kelihatan mudah.
Berikut ialah beberapa contoh perpustakaan tersebut:
Kadangkala, kami terbawa-bawa dan mencipta abstraksi yang tidak memudahkan kod mahupun menjadikannya lebih pendek:
function createPizza(order) { // Prepare pizza const pizza = new Pizza({ base: order.size, sauce: order.sauce, cheese: 'Mozzarella' }); // Add toppings if (order.kind == 'Veg') { pizza.toppings = vegToppings; } else if (order.kind == 'Meat') { pizza.toppings = meatToppings; } const oven = new Oven(); if (oven.temp !== cookingTemp) { // Heat oven while (oven.temp < cookingTemp) { time.sleep(checkOvenInterval); oven.temp = getOvenTemp(oven); } } if (!pizza.baked) { // Bake pizza oven.insert(pizza); time.sleep(cookTime); oven.remove(pizza); pizza.baked = true; } // Box and slice const box = new Box(); pizza.boxed = box.putIn(pizza); pizza.sliced = box.slicePizza(order.size); pizza.ready = box.close(); return pizza; }
Contoh lain:
function createPizza(order) { const pizza = new Pizza({ base: order.size, sauce: order.sauce, cheese: 'Mozzarella' }); if (order.kind === 'Veg') { pizza.toppings = vegToppings; } else if (order.kind === 'Meat') { pizza.toppings = meatToppings; } const oven = new Oven(); if (oven.temp !== cookingTemp) { while (oven.temp < cookingTemp) { time.sleep(checkOvenInterval); oven.temp = getOvenTemp(oven); } } if (!pizza.baked) { oven.insert(pizza); time.sleep(cookTime); oven.remove(pizza); pizza.baked = true; } const box = new Box(); pizza.boxed = box.putIn(pizza); pizza.sliced = box.slicePizza(order.size); pizza.ready = box.close(); return pizza; }
Perkara terbaik yang boleh kita lakukan dalam kes sedemikian ialah menerapkan pemfaktoran semula dalam talian yang maha kuasa: menggantikan setiap panggilan fungsi dengan badannya. Tiada abstraksi, tiada masalah.
Contoh pertama menjadi:
function prepare(order) { const pizza = new Pizza({ base: order.size, sauce: order.sauce, cheese: 'Mozzarella' }); addToppings(pizza, order.kind); return pizza; } function addToppings(pizza, kind) { if (kind === 'Veg') { pizza.toppings = vegToppings; } else if (kind === 'Meat') { pizza.toppings = meatToppings; } } function bake(pizza) { const oven = new Oven(); heatOven(oven); bakePizza(pizza, oven); } function heatOven(oven) { if (oven.temp !== cookingTemp) { while (oven.temp < cookingTemp) { time.sleep(checkOvenInterval); oven.temp = getOvenTemp(oven); } } } function bakePizza(pizza, oven) { if (!pizza.baked) { oven.insert(pizza); time.sleep(cookTime); oven.remove(pizza); pizza.baked = true; } } function pack(pizza) { const box = new Box(); pizza.boxed = box.putIn(pizza); pizza.sliced = box.slicePizza(pizza.size); pizza.ready = box.close(); } function createPizza(order) { const pizza = prepare(order); bake(pizza); pack(pizza); return pizza; }
Dan contoh kedua menjadi:
function createPizza(order) { // Prepare pizza const pizza = new Pizza({ base: order.size, sauce: order.sauce, cheese: 'Mozzarella' }); // Add toppings if (order.kind == 'Veg') { pizza.toppings = vegToppings; } else if (order.kind == 'Meat') { pizza.toppings = meatToppings; } const oven = new Oven(); if (oven.temp !== cookingTemp) { // Heat oven while (oven.temp < cookingTemp) { time.sleep(checkOvenInterval); oven.temp = getOvenTemp(oven); } } if (!pizza.baked) { // Bake pizza oven.insert(pizza); time.sleep(cookTime); oven.remove(pizza); pizza.baked = true; } // Box and slice const box = new Box(); pizza.boxed = box.putIn(pizza); pizza.sliced = box.slicePizza(order.size); pizza.ready = box.close(); return pizza; }
Hasilnya bukan sahaja lebih pendek dan lebih mudah dibaca; kini pembaca tidak perlu meneka apa yang fungsi ini lakukan, kerana kami kini menggunakan fungsi dan ciri asli JavaScript tanpa abstraksi buatan sendiri.
Dalam banyak kes, sedikit pengulangan adalah baik. Pertimbangkan contoh ini:
function FormattedAddress({ address, city, country, district, zip }) { return [address, zip, district, city, country] .filter(Boolean) .join(', '); } function getMapLink({ name, address, city, country, zip }) { return `https://www.google.com/maps/?q=${encodeURIComponent( [name, address, zip, city, country].filter(Boolean).join(', ') )}`; } function ShopsPage({ url, title, shops }) { return ( <PageWithTitle url={url} title={title}> <Stack as="ul" gap="l"> {shops.map(shop => ( <Stack key={shop.name} as="li" gap="m"> <Heading level={2}> <Link href={shop.url}>{shop.name}</Link> </Heading> {shop.address && ( <Text variant="small"> <Link href={getMapLink(shop)}> <FormattedAddress {...shop} /> </Link> </Text> )} </Stack> ))} </Stack> </PageWithTitle> ); }
Ia kelihatan baik dan tidak akan menimbulkan sebarang soalan semasa semakan kod. Walau bagaimanapun, apabila kami cuba menggunakan nilai ini, autolengkap hanya menunjukkan nombor dan bukannya nilai sebenar (lihat ilustrasi). Ini menjadikan lebih sukar untuk memilih nilai yang betul.
Kita boleh menyelaraskan pemalar baseSpacing:
const file = 'pizza.jpg'; const prefix = file.slice(0, -4); // → 'pizza'
Kini, kami mempunyai kurang kod, ia sama mudah difahami, dan autolengkap menunjukkan nilai sebenar (lihat ilustrasi). Dan saya tidak fikir kod ini akan sering berubah — mungkin tidak pernah.
Pertimbangkan petikan ini daripada fungsi pengesahan borang:
const file = 'pizza.jpg'; const prefix = path.parse(file).name; // → 'pizza'
Agak sukar untuk memahami perkara yang berlaku di sini: logik pengesahan bercampur dengan mesej ralat, banyak semakan diulang…
Kita boleh membahagikan fungsi ini kepada beberapa bahagian, masing-masing bertanggungjawab untuk satu perkara sahaja:
Kami boleh menerangkan pengesahan secara deklaratif sebagai tatasusunan:
// my_feature_util.js const noop = () => {}; export const Utility = { noop // Many more functions… }; // MyComponent.js function MyComponent({ onClick }) { return <button onClick={onClick}>Hola!</button>; } MyComponent.defaultProps = { onClick: Utility.noop };
Setiap fungsi pengesahan dan fungsi yang menjalankan pengesahan adalah agak generik, jadi kita boleh sama ada mengabstrakkannya atau menggunakan pustaka pihak ketiga.
Kini, kami boleh menambah pengesahan untuk sebarang bentuk dengan menerangkan medan mana yang memerlukan pengesahan dan ralat mana yang perlu ditunjukkan apabila semakan tertentu gagal.
Maklumat: Lihat bab Elakkan syarat untuk kod lengkap dan penjelasan yang lebih terperinci tentang contoh ini.
Saya panggil proses ini pemisahan "apa" dan "bagaimana":
Faedahnya ialah:
Banyak projek mempunyai fail yang dipanggil utils.js, helpers.js atau misc.js tempat pembangun memasukkan fungsi utiliti apabila mereka tidak dapat mencari tempat yang lebih baik untuk mereka. Selalunya, fungsi ini tidak pernah digunakan semula di tempat lain dan kekal dalam fail utiliti selama-lamanya, jadi ia terus berkembang. Begitulah cara fail utiliti raksasa dilahirkan.
Fail utiliti raksasa mempunyai beberapa isu:
Ini adalah peraturan ibu jari saya:
Modul JavaScript mempunyai dua jenis eksport. Yang pertama ialah eksport bernama:
function createPizza(order) { const pizza = new Pizza({ base: order.size, sauce: order.sauce, cheese: 'Mozzarella' }); if (order.kind === 'Veg') { pizza.toppings = vegToppings; } else if (order.kind === 'Meat') { pizza.toppings = meatToppings; } const oven = new Oven(); if (oven.temp !== cookingTemp) { while (oven.temp < cookingTemp) { time.sleep(checkOvenInterval); oven.temp = getOvenTemp(oven); } } if (!pizza.baked) { oven.insert(pizza); time.sleep(cookTime); oven.remove(pizza); pizza.baked = true; } const box = new Box(); pizza.boxed = box.putIn(pizza); pizza.sliced = box.slicePizza(order.size); pizza.ready = box.close(); return pizza; }
Yang boleh diimport seperti ini:
function prepare(order) { const pizza = new Pizza({ base: order.size, sauce: order.sauce, cheese: 'Mozzarella' }); addToppings(pizza, order.kind); return pizza; } function addToppings(pizza, kind) { if (kind === 'Veg') { pizza.toppings = vegToppings; } else if (kind === 'Meat') { pizza.toppings = meatToppings; } } function bake(pizza) { const oven = new Oven(); heatOven(oven); bakePizza(pizza, oven); } function heatOven(oven) { if (oven.temp !== cookingTemp) { while (oven.temp < cookingTemp) { time.sleep(checkOvenInterval); oven.temp = getOvenTemp(oven); } } } function bakePizza(pizza, oven) { if (!pizza.baked) { oven.insert(pizza); time.sleep(cookTime); oven.remove(pizza); pizza.baked = true; } } function pack(pizza) { const box = new Box(); pizza.boxed = box.putIn(pizza); pizza.sliced = box.slicePizza(pizza.size); pizza.ready = box.close(); } function createPizza(order) { const pizza = prepare(order); bake(pizza); pack(pizza); return pizza; }
Dan yang kedua ialah eksport lalai:
function createPizza(order) { // Prepare pizza const pizza = new Pizza({ base: order.size, sauce: order.sauce, cheese: 'Mozzarella' }); // Add toppings if (order.kind == 'Veg') { pizza.toppings = vegToppings; } else if (order.kind == 'Meat') { pizza.toppings = meatToppings; } const oven = new Oven(); if (oven.temp !== cookingTemp) { // Heat oven while (oven.temp < cookingTemp) { time.sleep(checkOvenInterval); oven.temp = getOvenTemp(oven); } } if (!pizza.baked) { // Bake pizza oven.insert(pizza); time.sleep(cookTime); oven.remove(pizza); pizza.baked = true; } // Box and slice const box = new Box(); pizza.boxed = box.putIn(pizza); pizza.sliced = box.slicePizza(order.size); pizza.ready = box.close(); return pizza; }
Yang boleh diimport seperti ini:
function FormattedAddress({ address, city, country, district, zip }) { return [address, zip, district, city, country] .filter(Boolean) .join(', '); } function getMapLink({ name, address, city, country, zip }) { return `https://www.google.com/maps/?q=${encodeURIComponent( [name, address, zip, city, country].filter(Boolean).join(', ') )}`; } function ShopsPage({ url, title, shops }) { return ( <PageWithTitle url={url} title={title}> <Stack as="ul" gap="l"> {shops.map(shop => ( <Stack key={shop.name} as="li" gap="m"> <Heading level={2}> <Link href={shop.url}>{shop.name}</Link> </Heading> {shop.address && ( <Text variant="small"> <Link href={getMapLink(shop)}> <FormattedAddress {...shop} /> </Link> </Text> )} </Stack> ))} </Stack> </PageWithTitle> ); }
Saya tidak nampak apa-apa kelebihan untuk eksport lalai, tetapi ia mempunyai beberapa isu:
Maklumat: Kami bercakap lebih lanjut tentang kebolehcapaian dalam bahagian Tulis kod boleh dipalsukan dalam bab Teknik lain.
Malangnya, sesetengah API pihak ketiga, seperti React.lazy() memerlukan eksport lalai, tetapi untuk semua kes lain, saya tetap menggunakan eksport bernama.
Fail tong ialah modul (biasanya dinamakan index.js atau index.ts) yang mengeksport semula sekumpulan modul lain:
function createPizza(order) { const pizza = new Pizza({ base: order.size, sauce: order.sauce, cheese: 'Mozzarella' }); if (order.kind === 'Veg') { pizza.toppings = vegToppings; } else if (order.kind === 'Meat') { pizza.toppings = meatToppings; } const oven = new Oven(); if (oven.temp !== cookingTemp) { while (oven.temp < cookingTemp) { time.sleep(checkOvenInterval); oven.temp = getOvenTemp(oven); } } if (!pizza.baked) { oven.insert(pizza); time.sleep(cookTime); oven.remove(pizza); pizza.baked = true; } const box = new Box(); pizza.boxed = box.putIn(pizza); pizza.sliced = box.slicePizza(order.size); pizza.ready = box.close(); return pizza; }
Kelebihan utama adalah import yang lebih bersih. Daripada mengimport setiap modul secara individu:
function prepare(order) { const pizza = new Pizza({ base: order.size, sauce: order.sauce, cheese: 'Mozzarella' }); addToppings(pizza, order.kind); return pizza; } function addToppings(pizza, kind) { if (kind === 'Veg') { pizza.toppings = vegToppings; } else if (kind === 'Meat') { pizza.toppings = meatToppings; } } function bake(pizza) { const oven = new Oven(); heatOven(oven); bakePizza(pizza, oven); } function heatOven(oven) { if (oven.temp !== cookingTemp) { while (oven.temp < cookingTemp) { time.sleep(checkOvenInterval); oven.temp = getOvenTemp(oven); } } } function bakePizza(pizza, oven) { if (!pizza.baked) { oven.insert(pizza); time.sleep(cookTime); oven.remove(pizza); pizza.baked = true; } } function pack(pizza) { const box = new Box(); pizza.boxed = box.putIn(pizza); pizza.sliced = box.slicePizza(pizza.size); pizza.ready = box.close(); } function createPizza(order) { const pizza = prepare(order); bake(pizza); pack(pizza); return pizza; }
Kami boleh mengimport semua komponen daripada fail tong:
function createPizza(order) { // Prepare pizza const pizza = new Pizza({ base: order.size, sauce: order.sauce, cheese: 'Mozzarella' }); // Add toppings if (order.kind == 'Veg') { pizza.toppings = vegToppings; } else if (order.kind == 'Meat') { pizza.toppings = meatToppings; } const oven = new Oven(); if (oven.temp !== cookingTemp) { // Heat oven while (oven.temp < cookingTemp) { time.sleep(checkOvenInterval); oven.temp = getOvenTemp(oven); } } if (!pizza.baked) { // Bake pizza oven.insert(pizza); time.sleep(cookTime); oven.remove(pizza); pizza.baked = true; } // Box and slice const box = new Box(); pizza.boxed = box.putIn(pizza); pizza.sliced = box.slicePizza(order.size); pizza.ready = box.close(); return pizza; }
Walau bagaimanapun, fail tong mempunyai beberapa isu:
Maklumat: TkDodo menerangkan kelemahan fail tong dengan terperinci.
Faedah fail tong terlalu kecil untuk membenarkan penggunaannya, jadi saya syorkan untuk mengelakkannya.
Satu jenis fail tong yang saya amat tidak suka ialah fail yang mengeksport satu komponen hanya untuk membenarkan pengimportannya sebagai ./components/button dan bukannya ./components/button/button.
Untuk troll DRYers (pembangun yang tidak pernah mengulangi kod mereka), seseorang mencipta istilah lain: WET, tulis semuanya dua kali atau kami seronok menaip, mencadangkan kami perlu menduplikasi kod di sekurang-kurangnya dua kali sehingga kita menggantikannya dengan abstraksi. Ia adalah jenaka, dan saya tidak bersetuju sepenuhnya dengan idea itu (kadangkala tidak mengapa untuk menduplikasi beberapa kod lebih daripada dua kali), tetapi ini adalah peringatan yang baik bahawa semua perkara yang baik adalah yang terbaik dalam kesederhanaan.
Pertimbangkan contoh ini:
function createPizza(order) { const pizza = new Pizza({ base: order.size, sauce: order.sauce, cheese: 'Mozzarella' }); if (order.kind === 'Veg') { pizza.toppings = vegToppings; } else if (order.kind === 'Meat') { pizza.toppings = meatToppings; } const oven = new Oven(); if (oven.temp !== cookingTemp) { while (oven.temp < cookingTemp) { time.sleep(checkOvenInterval); oven.temp = getOvenTemp(oven); } } if (!pizza.baked) { oven.insert(pizza); time.sleep(cookTime); oven.remove(pizza); pizza.baked = true; } const box = new Box(); pizza.boxed = box.putIn(pizza); pizza.sliced = box.slicePizza(order.size); pizza.ready = box.close(); return pizza; }
Ini adalah contoh ekstrem Pengeringan kod, yang tidak menjadikan kod lebih mudah dibaca atau diselenggara, terutamanya apabila kebanyakan pemalar ini digunakan sekali sahaja. Melihat nama pembolehubah dan bukannya rentetan sebenar di sini adalah tidak membantu.
Mari sebaris semua pembolehubah tambahan ini. (Malangnya, pemfaktoran semula sebaris dalam Kod Visual Studio tidak menyokong sifat objek sebaris, jadi kami perlu melakukannya secara manual.)
function prepare(order) { const pizza = new Pizza({ base: order.size, sauce: order.sauce, cheese: 'Mozzarella' }); addToppings(pizza, order.kind); return pizza; } function addToppings(pizza, kind) { if (kind === 'Veg') { pizza.toppings = vegToppings; } else if (kind === 'Meat') { pizza.toppings = meatToppings; } } function bake(pizza) { const oven = new Oven(); heatOven(oven); bakePizza(pizza, oven); } function heatOven(oven) { if (oven.temp !== cookingTemp) { while (oven.temp < cookingTemp) { time.sleep(checkOvenInterval); oven.temp = getOvenTemp(oven); } } } function bakePizza(pizza, oven) { if (!pizza.baked) { oven.insert(pizza); time.sleep(cookTime); oven.remove(pizza); pizza.baked = true; } } function pack(pizza) { const box = new Box(); pizza.boxed = box.putIn(pizza); pizza.sliced = box.slicePizza(pizza.size); pizza.ready = box.close(); } function createPizza(order) { const pizza = prepare(order); bake(pizza); pack(pizza); return pizza; }
Kini, kami mempunyai kod yang jauh lebih sedikit, dan lebih mudah untuk memahami perkara yang sedang berlaku serta lebih mudah untuk mengemas kini atau memadamkan ujian.
Saya telah menemui begitu banyak abstraksi tanpa harapan dalam ujian. Sebagai contoh, corak ini sangat biasa:
function createPizza(order) { // Prepare pizza const pizza = new Pizza({ base: order.size, sauce: order.sauce, cheese: 'Mozzarella' }); // Add toppings if (order.kind == 'Veg') { pizza.toppings = vegToppings; } else if (order.kind == 'Meat') { pizza.toppings = meatToppings; } const oven = new Oven(); if (oven.temp !== cookingTemp) { // Heat oven while (oven.temp < cookingTemp) { time.sleep(checkOvenInterval); oven.temp = getOvenTemp(oven); } } if (!pizza.baked) { // Bake pizza oven.insert(pizza); time.sleep(cookTime); oven.remove(pizza); pizza.baked = true; } // Box and slice const box = new Box(); pizza.boxed = box.putIn(pizza); pizza.sliced = box.slicePizza(order.size); pizza.ready = box.close(); return pizza; }
Corak ini cuba mengelakkan panggilan mount(...) berulang dalam setiap kes ujian, tetapi ia menjadikan ujian lebih mengelirukan daripada yang sepatutnya. Mari sebaris mount() panggilan:
function FormattedAddress({ address, city, country, district, zip }) { return [address, zip, district, city, country] .filter(Boolean) .join(', '); } function getMapLink({ name, address, city, country, zip }) { return `https://www.google.com/maps/?q=${encodeURIComponent( [name, address, zip, city, country].filter(Boolean).join(', ') )}`; } function ShopsPage({ url, title, shops }) { return ( <PageWithTitle url={url} title={title}> <Stack as="ul" gap="l"> {shops.map(shop => ( <Stack key={shop.name} as="li" gap="m"> <Heading level={2}> <Link href={shop.url}>{shop.name}</Link> </Heading> {shop.address && ( <Text variant="small"> <Link href={getMapLink(shop)}> <FormattedAddress {...shop} /> </Link> </Text> )} </Stack> ))} </Stack> </PageWithTitle> ); }
Selain itu, corak beforeEach hanya berfungsi apabila kita ingin memulakan setiap kes ujian dengan nilai yang sama, yang jarang berlaku:
const file = 'pizza.jpg'; const prefix = file.slice(0, -4); // → 'pizza'
Untuk mengelakkan beberapa pertindihan semasa menguji komponen React, saya sering menambah objek Props lalai dan menyebarkannya di dalam setiap kes ujian:
const file = 'pizza.jpg'; const prefix = path.parse(file).name; // → 'pizza'
Dengan cara ini, kami tidak mempunyai terlalu banyak pertindihan, tetapi pada masa yang sama, setiap kes ujian diasingkan dan boleh dibaca. Perbezaan antara kes ujian kini lebih jelas kerana lebih mudah untuk melihat prop unik setiap kes ujian.
Berikut ialah variasi yang lebih melampau bagi masalah yang sama:
// my_feature_util.js const noop = () => {}; export const Utility = { noop // Many more functions… }; // MyComponent.js function MyComponent({ onClick }) { return <button onClick={onClick}>Hola!</button>; } MyComponent.defaultProps = { onClick: Utility.noop };
Kita boleh menyelaraskan fungsi beforeEach() seperti yang kita lakukan dalam contoh sebelumnya:
const findByReference = (wrapper, reference) => wrapper.find(reference); const favoriteTaco = findByReference( ['Al pastor', 'Cochinita pibil', 'Barbacoa'], x => x === 'Cochinita pibil' ); // → 'Cochinita pibil'
Saya akan pergi lebih jauh dan menggunakan kaedah test.each() kerana kami menjalankan ujian yang sama dengan sekumpulan input yang berbeza:
function MyComponent({ onClick }) { return <button onClick={onClick}>Hola!</button>; } MyComponent.defaultProps = { onClick: () => {} };
Kini, kami telah mengumpulkan semua input ujian dengan keputusan yang dijangkakan di satu tempat, menjadikannya lebih mudah untuk menambah kes ujian baharu.
Maklumat: Lihat helaian tipu Jest and Vitest saya.
Cabaran terbesar dengan abstraksi ialah mencari keseimbangan antara menjadi terlalu tegar dan terlalu fleksibel, serta mengetahui bila untuk mula mengabstrakkan sesuatu dan bila untuk berhenti. Selalunya berbaloi untuk menunggu untuk melihat sama ada kita benar-benar perlu mengabstraksi sesuatu — berkali-kali, lebih baik tidak melakukannya.
Memang bagus untuk mempunyai komponen butang global, tetapi jika ia terlalu fleksibel dan mempunyai sedozen prop boolean untuk bertukar antara variasi yang berbeza, ia akan menjadi sukar untuk digunakan. Walau bagaimanapun, jika ia terlalu tegar, pembangun akan mencipta komponen butang mereka sendiri dan bukannya menggunakan butang yang dikongsi.
Kita harus berwaspada tentang membenarkan orang lain menggunakan semula kod kita. Terlalu kerap, ini mewujudkan gandingan yang ketat antara bahagian pangkalan kod yang sepatutnya bebas, memperlahankan pembangunan dan membawa kepada pepijat.
Mula berfikir tentang:
Jika anda mempunyai sebarang maklum balas, mastodon saya, tweet saya, buka isu di GitHub, atau e-mel saya di artem@sapegin.ru. Dapatkan salinan anda.
Atas ialah kandungan terperinci Mencuci kod anda: bahagikan dan takluki, atau gabung dan berehat. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!