Bereaksi pengembangan bentuk. KISS, YAGNI, prinsip KERING dalam praktik

Halo, dalam tutorial ini kita akan melihat bagaimana mengembangkan bentuk yang sangat sederhana, tetapi terkontrol dalam Bereaksi, dengan fokus pada kualitas kode.

Saat mengembangkan formulir kami, kami akan mengikuti prinsip "KISS", "YAGNI", "KERING". Untuk berhasil menyelesaikan tutorial ini Anda tidak perlu tahu prinsip-prinsip ini, saya akan menjelaskannya sepanjang jalan. Namun, saya percaya bahwa Anda memiliki perintah javascript modern yang bagus dan dapat berpikir dalam Bereaksi .



Struktur tutorial:







Untuk penyebabnya! Menulis formulir sederhana menggunakan KISS dan YAGNI


Jadi, mari kita bayangkan bahwa kita memiliki tugas untuk mengimplementasikan formulir otorisasi:
Salin Kode
const logInData = {
  nickname: 'Vasya',
  email: 'pupkin@gmail.com',
  password: 'Reac5$$$',
};



Kami memulai pengembangan kami dengan menganalisis prinsip-prinsip KISS dan YAGNI, untuk sementara melupakan sisa dari prinsip-prinsip tersebut.

CIUMAN - "Biarkan kodenya sederhana dan bodoh." Saya pikir Anda sudah familiar dengan konsep kode sederhana. Tapi apa arti kode "bodoh"? Dalam pemahaman saya, ini adalah kode yang memecahkan masalah menggunakan jumlah minimum abstraksi, sementara bersarangnya abstraksi ini satu sama lain juga minimal.

YAGNI - "Anda tidak akan membutuhkannya." Kode harus dapat melakukan hanya untuk apa ditulisnya. Kami tidak membuat fungsionalitas apa pun yang mungkin diperlukan nanti atau yang membuat aplikasi lebih baik menurut pendapat kami. Kami hanya melakukan apa yang diperlukan khusus untuk pelaksanaan tugas.

Mari kita dengan ketat mengikuti prinsip-prinsip ini, tetapi juga pertimbangkan:

  • initialDatadan onSubmituntuk LogInFormberasal dari atas (ini adalah teknik yang berguna, terutama ketika formulir harus dapat diproses createdan updatepada saat yang sama)
  • harus ada untuk setiap bidang label

Selain itu, mari kita lupakan stylization, karena ini tidak menarik bagi kami, dan valid, karena ini adalah topik untuk tutorial terpisah.

Silakan laksanakan formulir sendiri, mengikuti prinsip-prinsip yang dijelaskan di atas.

Implementasi formulir saya
const LogInForm = ({ initialData, onSubmit }) => {
  const [logInData, setLogInData] = useState(initialData);

  const handleSubmit = e => {
    e.preventDefault();
    onSubmit(logInData);
  };

  return (
    <form onSubmit={handleSubmit}>
      <label>
        Enter your nickname
        <input
          value={logInData.nickname}
          onChange={e => setLogInData({ ...logInData, nickname: e.target.value })}
        />
      </label>
      <label>
        Enter your email
        <input
          type="email"
          value={logInData.email}
          onChange={e => setLogInData({ ...logInData, email: e.target.value })}
        />
      </label>
      <label>
        Enter your password
        <input
          type="password"
          value={logInData.password}
          onChange={e => setLogInData({ ...logInData, password: e.target.value })}
        />
      </label>
      <button>Submit</button>
    </form>
  );
};
    






Keputusan Anda, kemungkinan besar, agak berbeda, karena setiap pengembang berpikir secara berbeda. Anda dapat membuat negara Anda sendiri untuk setiap bidang atau menempatkan fungsi penangan dalam variabel terpisah, dalam hal apa pun ini tidak penting.

Tetapi jika Anda segera membuat satu fungsi penangan untuk semua bidang, maka ini bukan lagi solusi "paling bodoh" untuk masalah ini. Dan tujuan kami pada tahap ini adalah untuk membuat kode yang paling sederhana dan paling "bodoh" sehingga nantinya kita bisa melihat seluruh gambar dan memilih abstraksi terbaik.

Jika Anda memiliki kode yang sama persis, ini keren dan artinya pemikiran kita menyatu!

Selanjutnya kami akan bekerja dengan kode ini. Ini sederhana, tetapi masih jauh dari ideal.




Refactoring dan KERING


Sudah waktunya untuk berurusan dengan prinsip KERING.

KERING, kata yang disederhanakan - "jangan menduplikasi kode Anda." Prinsipnya kelihatannya sederhana, tetapi memiliki arti: untuk menghilangkan duplikasi kode, Anda perlu membuat abstraksi. Jika abstraksi ini tidak cukup baik, kami akan melanggar prinsip KISS.

Penting juga dipahami bahwa KERING tidak perlu menulis kode lebih cepat. Tugasnya adalah menyederhanakan pembacaan dan dukungan solusi kami. Karena itu, jangan buru-buru membuat abstraksi segera. Lebih baik untuk membuat implementasi sederhana dari beberapa bagian kode, dan kemudian menganalisis abstraksi apa yang perlu Anda buat untuk menyederhanakan pembacaan kode dan mengurangi jumlah tempat untuk perubahan saat diperlukan.

Daftar periksa abstraksi yang benar:
  • nama abstraksi sepenuhnya konsisten dengan tujuannya
  • abstraksi melakukan tugas yang spesifik dan dapat dimengerti
  • membaca kode dari mana abstraksi diekstraksi ditingkatkan

Jadi, mari kita turun ke refactoring.
Dan kami telah mengucapkan kode rangkap:
Salin Kode
  <label>
    Enter your email
    <input
      type="email"
      value={logInData.email}
      onChange={e => setLogInData({ ...logInData, email: e.target.value })}
    />
  </label>
  <label>
    Enter your password
    <input
      type="password"
      value={logInData.password}
      onChange={e => setLogInData({ ...logInData, password: e.target.value })}
    />
  </label>



Kode ini menggandakan komposisi 2 elemen: label, input. Mari kita gabungkan mereka menjadi abstraksi baru InputField:
Salin Kode
  <label>
    Enter your email
    <input
      type="email"
      value={logInData.email}
      onChange={e => setLogInData({ ...logInData, email: e.target.value })}
    />
  </label>
  <label>
    Enter your password
    <input
      type="password"
      value={logInData.password}
      onChange={e => setLogInData({ ...logInData, password: e.target.value })}
    />
  </label>



Sekarang milik kita LogInFormterlihat seperti ini:
Salin Kode
const LogInForm = ({ initialData, onSubmit }) => {
  const [logInData, setLogInData] = useState(initialData);

  const handleSubmit = e => {
    e.preventDefault();
    onSubmit(logInData);
  };

  return (
    <form onSubmit={handleSubmit}>
      <InputField
        label="Enter your nickname"
        value={logInData.nickname}
        onChange={e => setLogInData({ ...logInData, nickname: e.target.value })}
      />
      <InputField
        type="email"
        label="Enter your email"
        value={logInData.email}
        onChange={e => setLogInData({ ...logInData, email: e.target.value })}
      />
      <InputField
        type="password"
        label="Enter your password"
        value={logInData.password}
        onChange={e => setLogInData({ ...logInData, password: e.target.value })}
      />
      <button>Submit</button>
    </form>
  );
};



Membaca menjadi lebih mudah. Nama abstraksi sesuai dengan tugas yang diselesaikannya. Tujuan komponen jelas. Kode menjadi kurang. Jadi kita menuju ke arah yang benar!

Sekarang sudah jelas bahwa InputField.onChangelogika digandakan.
Apa yang terjadi di sana dapat dibagi menjadi 2 tahap:

Salin Kode
const stage1 = e => e.target.value;
const stage2 = password => setLogInData({ ...logInData, password });



Fungsi pertama menjelaskan detail untuk mendapatkan nilai dari acara tersebut input. Kami memiliki pilihan 2 abstraksi di mana kami dapat menyimpan logika ini: InputFielddan LogInForm.

Untuk menentukan dengan benar dari abstraksi mana kita perlu merujuk kode kita, kita harus beralih ke perumusan penuh prinsip KERING: "Setiap bagian dari pengetahuan harus memiliki perwakilan yang unik, konsisten dan otoritatif dalam sistem."

Bagian dari pengetahuan dalam contoh konkret adalah mengetahui cara mendapatkan nilai dari acara di input. Jika kita akan menyimpan pengetahuan ini di milik kita LogInForm, maka jelaslah bahwa ketika menggunakanInputFielddalam bentuk lain, kita harus menduplikasi pengetahuan kita, atau membawanya ke abstraksi terpisah dan menggunakannya dari sana. Dan berdasarkan prinsip KISS, kita harus memiliki jumlah abstraksi seminimal mungkin. Memang, mengapa kita perlu membuat abstraksi lain jika kita bisa meletakkan logika ini di kita InputFielddan kode eksternal tidak akan tahu apa-apa tentang bagaimana input bekerja di dalam InputField. Ini hanya akan mengambil makna yang sudah jadi, sama seperti ia masuk ke dalam.

Jika Anda bingung bahwa Anda mungkin memerlukan suatu acara di masa depan, ingatlah prinsip YAGNI. Anda selalu dapat menambahkan properti tambahan onChangeEventke komponen kami InputField.

Sampai InputFieldnanti akan terlihat seperti ini:
Salin Kode
const InputField = ({ label, type, value, onChange }) => (
  <label>
    {label}
    <input
      type={type}
      value={value}
      onChange={e => onChange(e.target.value)}
    />
  </label>
);



Dengan demikian, keseragaman jenis diamati selama input dan output ke dalam komponen dan sifat sebenarnya dari apa yang terjadi untuk kode eksternal disembunyikan. Jika di masa depan kita membutuhkan komponen ui lain, misalnya, centang atau pilih, maka di dalamnya kita juga akan mempertahankan keseragaman jenis pada input-output.

Pendekatan ini memberi kami fleksibilitas tambahan, karena formulir kami dapat bekerja dengan sumber input-output apa pun tanpa perlu menghasilkan penangan unik tambahan untuk masing-masingnya.

Trik heuristik ini bawaan bawaan di banyak kerangka kerja. Sebagai contoh, ini adalah ide utama v-modeldi Vuemana banyak orang suka untuk kemudahan bekerja dengan bentuk.

Mari kembali ke bisnis, perbarui komponen kami LogInFormsesuai dengan perubahan di InputField:
Salin Kode
const LogInForm = ({ initialData, onSubmit }) => {
  const [logInData, setLogInData] = useState(initialData);

  const handleSubmit = e => {
    e.preventDefault();
    onSubmit(logInData);
  };

  return (
    <form onSubmit={handleSubmit}>
      <InputField
        label="Enter your nickname"
        value={logInData.nickname}
        onChange={nickname => setLogInData({ ...logInData, nickname })}
      />
      <InputField
        type="email"
        label="Enter your email"
        value={logInData.email}
        onChange={email => setLogInData({ ...logInData, email })}
      />
      <InputField
        type="password"
        label="Enter your password"
        value={logInData.password}
        onChange={password => setLogInData({ ...logInData, password })}
      />
      <button>Submit</button>
    </form>
  );
};



Itu sudah terlihat cukup bagus, tetapi kita bisa melakukan yang lebih baik lagi!

Callbackyang diteruskan onChangeselalu melakukan hal yang sama. Hanya kunci yang diubah di dalamnya: kata sandi, email, nama panggilan. Jadi, kita bisa menggantinya dengan panggilan ke fungsi: handleChange('password').

Mari kita terapkan fungsi ini:
Salin Kode
  const handleChange = fieldName => fieldValue => {
    setLogInData({
      ...logInData,
      [fieldName]: fieldValue,
    });
  };



Seperti yang Anda lihat, fungsi menerima argumen dan menyimpannya dalam penutupan pawang, dan pawang segera kembali ke kode eksternal. Fungsi yang tampaknya mengambil argumen satu per satu untuk panggilan juga disebut curried.

Mari kita lihat kode yang dihasilkan:
Salin Kode
  const LogInForm = ({ initialData, onSubmit }) => {
    const [logInData, setLogInData] = useState(initialData);
  
    const handleSubmit = e => {
      e.preventDefault();
      onSubmit(logInData);
    };
  
    const handleChange = fieldName => fieldValue => {
      setLogInData({
        ...logInData,
        [fieldName]: fieldValue,
      });
    };
  
    return (
      <form onSubmit={handleSubmit}>
        <InputField
          label="Enter your nickname"
          value={logInData.nickname}
          onChange={handleChange('nickname')}
        />
        <InputField
          type="email"
          label="Enter your email"
          value={logInData.email}
          onChange={handleChange('email')}
        />
        <InputField
          type="password"
          label="Enter your password"
          value={logInData.password}
          onChange={handleChange('password')}
        />
        <button>Submit</button>
      </form>
    );
  };
  
  // InputField.js
  const InputField = ({ type, label, value, onChange }) => (
    <label>
      {label}
      <input type={type} value={value} onChange={e => onChange(e.target.value)} />
    </label>
  );



Kode ini adalah implementasi tugas yang pendek, singkat dan cukup deklaratif. Refactoring lebih lanjut dalam konteks tugas, menurut pendapat saya, tidak masuk akal.




Apa lagi yang bisa dilakukan?



Jika Anda memiliki banyak formulir di proyek, maka Anda bisa memasukkan perhitungan handleChange ke dalam kait yang terpisah useFieldChange:
Salin Kode
  // hooks/useFieldChange.js
  const useFieldChange = setState => fieldName => fieldValue => {
    setState(state => ({
      ...state,
      [fieldName]: fieldValue,
    }));
  };
  // LogInForm.js
  const handleChange = useFieldChange(setLogInData);



Karena ini adalah fungsi murni (artinya pertama kali disebut karena ia selalu mengembalikan fungsi yang sama), ia tidak harus berupa pengait. Tetapi hook terlihat secara konseptual solusi yang lebih tepat dan alami untuk React.

Anda juga dapat menambahkan dukungan di callbacktempat fieldValueuntuk sepenuhnya mereplikasi perilaku biasa setStatedari React:
Salin Kode
  const isFunc = val => typeof val === "function";

  const useFieldChange = setState => fieldName => fieldValue => {
    setState(state => ({
      ...state,
      [fieldName]: isFunc(fieldValue) ? fieldValue(state[fieldName]) : fieldValue,
    }));
  };



Contoh penggunaan dengan formulir kami:
Salin Kode
  const LogInForm = ({ initialData, onSubmit }) => {
    const [logInData, setLogInData] = useState(initialData);
    const handleChange = useFieldChange(setLogInData);
  
    const handleSubmit = e => {
      e.preventDefault();
      onSubmit(logInData);
    };
  
    return (
      <form onSubmit={handleSubmit}>
        <InputField
          label="Enter your nickname"
          value={logInData.nickname}
          onChange={handleChange('nickname')}
        />
        <InputField
          type="email"
          label="Enter your email"
          value={logInData.email}
          onChange={handleChange('email')}
        />
        <InputField
          type="password"
          label="Enter your password"
          value={logInData.password}
          onChange={handleChange('password')}
        />
        <button>Submit</button>
      </form>
    );
  };



Tetapi jika aplikasi Anda hanya memiliki satu formulir, Anda tidak perlu melakukan semua ini! Karena itu bertentangan dengan prinsip YAGNI, maka kita tidak boleh melakukan apa yang tidak perlu kita selesaikan dengan masalah tertentu. Jika Anda hanya memiliki satu bentuk, maka manfaat nyata dari gerakan tersebut tidak cukup. Bagaimanapun, kami mengurangi kode komponen kami hanya dengan 3 baris, tetapi memperkenalkan abstraksi tambahan, menyembunyikan logika tertentu dari formulir, yang lebih baik disimpan di permukaan.




Oh tidak! Tolong jangan lakukan itu!



Bentuk Config Berbentuk


Konfigurasi yang dikunci seperti konfigurasi webpack, hanya untuk formulir.

Lebih baik dengan contoh, lihat kode ini:
Salin Kode
  const Form = () => (
    <form onSubmit={handleSubmit}>
      <InputField
        label="Enter your nickname"
        value={state.nickname}
        onChange={handleChange('nickname')}
      />
      <InputField
        type="email"
        label="Enter your email"
        value={state.email}
        onChange={handleChange('email')}
      />
      <InputField
        type="password"
        label="Enter your password"
        value={state.password}
        onChange={handleChange('password')}
      />
      <button>Submit</button>
    </form>
  );



Mungkin bagi sebagian orang kode tersebut diduplikasi di sini, karena kami memanggil komponen InputField yang sama, melewati label, nilai, dan parameter onChange yang sama di sana. Dan mereka mulai-KERING-yav kode mereka sendiri untuk menghindari duplikasi imajiner.
Seringkali mereka melakukan ini seperti ini:
Salin Kode
const fields = [
  {
    name: 'nickname',
    label: 'Enter your nickname',
  },
  {
    type: 'email',
    name: 'email',
    label: 'Enter your email',
  },
  {
    type: 'password',
    name: 'password',
    label: 'Enter your password',
  },
];

const Form = () => (
  <form onSubmit={handleSubmit}>
    {fields.map(({ type, name, label }) => (
      <InputField
        type={type}
        label={label}
        value={state[name]}
        onChange={handleChange(name)}
      />
    ))}
    <button>Submit</button>
  </form>
);



Hasilnya, dengan 17 baris kode jsx kita mendapatkan 16 baris konfigurasi. Bravo! Inilah yang saya mengerti KERING. Jika kita memiliki 100 input ini di sini, maka kita mendapatkan masing-masing 605 dan 506 baris.

Tetapi, sebagai hasilnya, kami mendapat kode yang lebih rumit, melanggar prinsip KISS. Setelah semua, sekarang terdiri dari 2 abstraksi: bidang dan algoritma (ya, sebuah algoritma juga merupakan abstraksi), yang mengubahnya menjadi pohon elemen Bereaksi. Saat membaca kode ini, kita harus terus-menerus melompat di antara abstraksi-abstraksi ini.

Tetapi masalah terbesar dengan kode ini adalah dukungan dan ekstensi.
Bayangkan apa yang akan terjadi padanya jika:
  • beberapa tipe bidang lainnya dengan kemungkinan properti berbeda akan ditambahkan
  • bidang diberikan atau tidak diberikan berdasarkan input dari bidang lain
  • beberapa bidang memerlukan beberapa pemrosesan tambahan saat mengubah
  • nilai dalam pemilihan bergantung pada pilihan sebelumnya

Daftar ini dapat dilanjutkan untuk waktu yang lama ...

Untuk menerapkan ini, Anda harus selalu menyimpan beberapa fungsi dalam konfigurasi Anda, dan kode yang membuat konfigurasi tersebut secara bertahap akan berubah menjadi Frankenstein, mengambil sekelompok alat peraga yang berbeda dan melemparkannya ke berbagai komponen.

Kode yang ditulis sebelumnya, berdasarkan komposisi komponen, akan diam-diam berkembang, berubah sesuka Anda, sementara tidak menjadi sangat rumit karena kurangnya abstraksi menengah - suatu algoritma yang membangun pohon elemen Bereaksi dari konfigurasi kami. Kode ini bukan duplikat. Itu hanya mengikuti pola desain tertentu, dan karena itu terlihat "berpola".

Optimasi tidak berguna


Setelah kait muncul di Bereaksi, ada kecenderungan untuk membungkus semua penangan dan komponen tanpa pandang bulu di useCallbackdan memo. Tolong, jangan lakukan itu! Pengait ini tidak disediakan oleh pengembang React karena React lambat dan semuanya perlu dioptimalkan. Mereka menyediakan ruang untuk mengoptimalkan aplikasi Anda jika Anda mengalami masalah kinerja. Dan bahkan jika Anda menghadapi masalah seperti itu, Anda tidak perlu membungkus seluruh proyek memodan useCallback. Gunakan Profiler untuk mengidentifikasi masalah dan hanya kemudian memo di tempat yang tepat.

Memoizing akan selalu membuat kode Anda lebih rumit, tetapi tidak selalu lebih produktif.

Mari kita lihat kode dari proyek nyata. Bandingkan seperti apa fungsi itu dengan useCallbackdan tanpa:
Salin Kode
  const applyFilters = useCallback(() => {
    const newSelectedMetrics = Object.keys(selectedMetricsStatus).filter(
      metric => selectedMetricsStatus[metric],
    );
    onApplyFilterClick(newSelectedMetrics);
  }, [selectedMetricsStatus, onApplyFilterClick]);

  const applyFilters = () => {
    const newSelectedMetrics = Object.keys(selectedMetricsStatus).filter(
      metric => selectedMetricsStatus[metric],
    );
    onApplyFilterClick(newSelectedMetrics);
  };



Keterbacaan kode jelas meningkat setelah penghapusan pembungkus, karena kode ideal adalah kekurangannya.

Performa kode ini tidak meningkat, karena fungsi ini digunakan seperti ini:
Salin Kode
  <RightFooterButton onClick={applyFilters}>APPLY</RightFooterButton>



Di mana RightFooterButton- hanya styled.buttondari styled-componentsmana, yang akan memperbarui dengan sangat cepat. Tetapi konsumsi memori meningkatkan aplikasi kita karena Bereaksi akan selalu menyimpan dalam memori selectedMetricsStatus, onApplyFilterClickdan versi fungsi applyFilters, relevan untuk dependensi ini.

Jika argumen ini tidak cukup untuk Anda, baca artikel yang membahas topik ini lebih luas.




temuan


  • Bentuk reaksi itu mudah. Masalah dengan mereka muncul karena pengembang sendiri dan dokumentasi Bereaksi, di mana topik ini tidak diungkapkan secara cukup rinci.
  • aka value onChange . useFieldChange, , , v-model Vue .
  • KISS YAGNI. DRY, , .
  • , , React- .
  • , .




P.S.


Awalnya, saya berencana untuk menulis tentang validasi deklaratif dari bentuk bersarang yang kompleks. Tetapi, pada akhirnya, saya memutuskan untuk mempersiapkan penonton untuk ini, sehingga gambarnya selengkap mungkin.
Tutorial berikut akan memvalidasi formulir sederhana yang diterapkan dalam tutorial ini.
Terima kasih banyak untuk semua orang yang membaca sampai akhir!

Tidak masalah bagaimana Anda menulis kode atau apa yang Anda lakukan, hal utama adalah menikmatinya.

All Articles