تفاعل شكل رد فعل. KISS ، YAGNI ، مبادئ DRY في الممارسة

مرحبًا ، في هذا البرنامج التعليمي ، سنلقي نظرة على كيفية تطوير نموذج بسيط جدًا ولكن يتم التحكم فيه في React ، مع التركيز على جودة الشفرة.

عند تطوير نموذجنا ، سوف نتبع مبادئ "KISS" ، "YAGNI" ، "DRY". لإكمال هذا البرنامج التعليمي بنجاح ، لا تحتاج إلى معرفة هذه المبادئ ، سأشرحها على طول الطريق. ومع ذلك ، أعتقد أن لديك إتقان جيد لجافا سكريبت الحديثة ويمكنك التفكير في رد فعل .



هيكل البرنامج التعليمي:







للقضية! كتابة نموذج بسيط باستخدام KISS و YAGNI


لذا ، دعنا نتخيل أن لدينا مهمة لتنفيذ نموذج التفويض:
رمز النسخ
const logInData = {
  nickname: 'Vasya',
  email: 'pupkin@gmail.com',
  password: 'Reac5$$$',
};



نبدأ تطويرنا من خلال تحليل مبادئ KISS و YAGNI ، مع نسيان مؤقتًا لبقية المبادئ.

KISS - "اترك الرمز بسيطًا وغبيًا". أعتقد أنك معتاد على مفهوم الكود البسيط. ولكن ماذا يعني رمز "البكم"؟ حسب فهمي ، هذا هو الرمز الذي يحل المشكلة باستخدام الحد الأدنى من التجريد ، في حين أن تداخل هذه التجريد في بعضها البعض هو أيضًا الحد الأدنى.

YAGNI - "لن تحتاجها". يجب أن تكون الشفرة قادرة على القيام فقط بما هو مكتوب من أجله. نحن لا ننشئ أي وظيفة قد تكون مطلوبة لاحقًا أو تجعل التطبيق أفضل في رأينا. نحن نفعل فقط ما هو مطلوب على وجه التحديد لتنفيذ المهمة.

دعونا نتبع هذه المبادئ بدقة ، ولكن ضع في اعتبارك أيضًا:

  • initialDataو onSubmitل LogInFormيأتي من أعلى (وهذا هو تقنية مفيدة، وخصوصا عندما يجب أن يكون شكل قادرة على عملية createو updateفي نفس الوقت)
  • يجب أن يكون لكل مجال label

بالإضافة إلى ذلك ، دعنا نفتقد الأسلوب ، لأنها ليست مثيرة للاهتمام بالنسبة لنا ، وصالحة ، لأن هذا موضوع لبرنامج تعليمي منفصل.

يُرجى تنفيذ النموذج بنفسك ، باتباع المبادئ الموضحة أعلاه.

تنفيذ النموذج الخاص بي
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>
  );
};
    






قرارك ، على الأرجح ، مختلف إلى حد ما ، لأن كل مطور يفكر بشكل مختلف. يمكنك إنشاء حالتك الخاصة لكل حقل أو وظائف معالج المكان في متغير منفصل ، على أي حال هذا ليس مهمًا.

ولكن إذا أنشأت على الفور وظيفة معالج واحدة لجميع الحقول ، فهذا لم يعد الحل "الأكثر غباء" للمشكلة. وهدفنا في هذه المرحلة هو إنشاء كود أبسط وأكثر "غبي" حتى نتمكن لاحقًا من النظر إلى الصورة بأكملها واختيار أفضل تجريد.

إذا كان لديك نفس الرمز بالضبط ، فهذا رائع ويعني أن تفكيرنا يتقارب!

علاوة على ذلك سنعمل مع هذا الرمز. إنها بسيطة ، لكنها لا تزال بعيدة عن المثالية.




إعادة بيع و تجفيف


حان الوقت للتعامل مع مبدأ DRY.

جاف ، صياغة مبسطة - "لا تكرر رمزك." يبدو المبدأ بسيطًا ، ولكن له تأثير: للتخلص من ازدواج التعليمات البرمجية ، تحتاج إلى إنشاء تجريدات. إذا لم تكن هذه التجريدات جيدة بما فيه الكفاية ، فسوف ننتهك مبدأ KISS.

من المهم أيضًا أن تفهم أن DRY غير مطلوب لكتابة التعليمات البرمجية بشكل أسرع. وتتمثل مهمتها في تبسيط قراءة ودعم حلنا. لذلك ، لا تتسرع في إنشاء تجريد على الفور. من الأفضل إجراء تنفيذ بسيط لجزء ما من التعليمات البرمجية ، ثم تحليل ما هي الملخصات التي تحتاج إلى إنشائها لتبسيط قراءة التعليمات البرمجية وتقليل عدد الأماكن للتغييرات عند الحاجة إليها.

قائمة التحقق من التجريد الصحيح:
  • يتوافق اسم التجريد تمامًا مع الغرض منه
  • يؤدي التجريد مهمة محددة ومفهومة
  • قراءة الشفرة التي تم استخراج التجريد منها تحسن

لذا ، دعنا ننتقل إلى إعادة البيع.
وقد أعلنا كود مكرر:
رمز النسخ
  <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>



يكرر هذا الرمز تكوين عنصرين: label، input. دعونا ندمجها في تجريد جديد InputField:
رمز النسخ
  <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>



الآن LogInFormيبدو لنا ما يلي:
رمز النسخ
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>
  );
};



أصبحت القراءة أسهل. يتوافق اسم التجريد مع المهمة التي يحلها. الغرض من المكون واضح. أصبح الرمز أقل. لذلك نحن نسير في الاتجاه الصحيح!

الآن من الواضح أن InputField.onChangeالمنطق مكرر.
يمكن تقسيم ما يحدث إلى مرحلتين:

رمز النسخ
const stage1 = e => e.target.value;
const stage2 = password => setLogInData({ ...logInData, password });



تصف الوظيفة الأولى تفاصيل الحصول على القيمة من الحدث input. لدينا خياران من مجردة يمكننا تخزين هذا المنطق فيه: InputFieldو LogInForm.

من أجل تحديد أي من التجريدات التي نحتاج إلى الرجوع إلى التعليمات البرمجية الخاصة بنا بشكل صحيح ، سيتعين علينا أن ننتقل إلى الصيغة الكاملة لمبدأ DRY: "يجب أن يكون لكل جزء من المعرفة تمثيل فريد ومتسق وموثوق داخل النظام".

جزء من المعرفة في المثال الملموس هو معرفة كيفية الحصول على القيمة من الحدث input. إذا قمنا بتخزين هذه المعرفة في علمنا LogInForm، فمن الواضح أنه عند استخدامInputFieldفي شكل آخر ، سيكون علينا نسخ معرفتنا ، أو إدخالها في تجريد منفصل واستخدامها من هناك. وبناءً على مبدأ KISS ، يجب أن يكون لدينا أقل عدد ممكن من التجريد. في الواقع ، لماذا نحتاج إلى إنشاء تجريد آخر إذا كان بإمكاننا فقط وضع هذا المنطق في منطقتنا InputFieldولن يعرف الرمز الخارجي أي شيء حول كيفية عمل الإدخال في الداخل InputField. ستكتسب ببساطة معنى جاهزًا ، تمامًا كما يمر من الداخل.

إذا كنت مرتبكًا بأنك قد تحتاج إلى حدث في المستقبل ، فتذكر مبدأ YAGNI. يمكنك إضافة دائما دعامة إضافية onChangeEventلعنصر لدينا InputField.

حتى ذلك الحين InputFieldستبدو كما يلي:
رمز النسخ
const InputField = ({ label, type, value, onChange }) => (
  <label>
    {label}
    <input
      type={type}
      value={value}
      onChange={e => onChange(e.target.value)}
    />
  </label>
);



وبالتالي ، لوحظ انتظام النوع أثناء الإدخال والإخراج في المكون والطبيعة الحقيقية لما يحدث للرمز الخارجي مخفية. إذا احتجنا في المستقبل إلى مكون آخر لواجهة المستخدم ، على سبيل المثال ، مربع اختيار أو تحديد ، فسوف نحافظ أيضًا على توحيد النوع في الإدخال والإخراج.

يمنحنا هذا النهج مرونة إضافية ، حيث يمكن أن يعمل النموذج الخاص بنا مع أي مصادر إدخال وإخراج دون الحاجة إلى إنتاج معالجات فريدة إضافية لكل منها.

يتم تضمين هذه الحيلة الإرشادية بشكل افتراضي في العديد من الأطر. على سبيل المثال، هذه هي الفكرة الرئيسية v-modelفي Vueالذي كثير من الناس يحبون لسهولة العمل مع النماذج.

دعنا نعود إلى العمل ، نقوم بتحديث المكون الخاص بنا LogInFormوفقًا للتغييرات في InputField:
رمز النسخ
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>
  );
};



يبدو الأمر جيدًا بالفعل ، ولكن يمكننا أن نفعل ما هو أفضل!

Callbackالتي يتم تمريرها onChangeتفعل نفس الشيء دائمًا. يتم تغيير المفتاح فقط فيه: كلمة المرور ، البريد الإلكتروني ، اللقب. لذلك، يمكننا استبدالها استدعاء الدالة: handleChange('password').

دعنا ننفذ هذه الوظيفة:
رمز النسخ
  const handleChange = fieldName => fieldValue => {
    setLogInData({
      ...logInData,
      [fieldName]: fieldValue,
    });
  };



كما ترى ، تتلقى الدالة الوسيطة وتخزنها في إغلاق المعالج ، ويعود المعالج على الفور إلى الرمز الخارجي. الوظائف التي يبدو أنها تأخذ الحجج واحدة في كل مرة لإجراء مكالمة تسمى أيضا الكاري.

دعونا نلقي نظرة على الكود الناتج:
رمز النسخ
  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>
  );



هذا الرمز هو تنفيذ قصير وموجز ومعلن بشكل معتدل للمهمة. المزيد من إعادة الهيكلة في سياق المهمة ، في رأيي ، لا معنى له.




ما الذي يمكن فعله أيضًا؟



إذا كان لديك العديد من النماذج في المشروع ، فيمكنك وضع حساب handleChange في خطاف منفصل useFieldChange:
رمز النسخ
  // hooks/useFieldChange.js
  const useFieldChange = setState => fieldName => fieldValue => {
    setState(state => ({
      ...state,
      [fieldName]: fieldValue,
    }));
  };
  // LogInForm.js
  const handleChange = useFieldChange(setLogInData);



نظرًا لأن هذه وظيفة نقية (بمعنى أنه يتم استدعاؤها في المرة الأولى لأنها تُرجع دائمًا نفس الوظيفة) ، فلا يجب أن تكون خطافًا. لكن الخطاف يبدو مفاهيميًا أنه حل أكثر صحة وطبيعية لرد فعل.

يمكنك أيضًا إضافة دعم callbackموضعي fieldValueلتكرار السلوك المعتاد تمامًا setStateمن React:
رمز النسخ
  const isFunc = val => typeof val === "function";

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



مثال للاستخدام مع نموذجنا:
رمز النسخ
  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>
    );
  };



ولكن إذا كان تطبيقك يحتوي على نموذج واحد فقط ، فلن تحتاج إلى القيام بكل هذا! لأنه يتناقض مع مبدأ YAGNI ، وبعد ذلك لا ينبغي لنا أن نفعل ما لا نحتاجه لحل مشكلة معينة. إذا كان لديك شكل واحد فقط ، فإن الفوائد الحقيقية لهذه الحركات ليست كافية. بعد كل شيء ، قمنا بتقليل كود المكون الخاص بنا بثلاثة أسطر فقط ، ولكننا قدمنا ​​تجريدًا إضافيًا ، مخفيًا منطقًا معينًا للنموذج ، والذي من الأفضل الاحتفاظ به على السطح.




أوه لا! لا تفعل ذلك من فضلك!



أشكال التكوين على شكل


تشبه عمليات التهيئة المقفلة تهيئة حزمة الويب ، للنموذج فقط.

أفضل مع مثال ، انظر إلى هذا الرمز:
رمز النسخ
  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>
  );



قد يبدو للبعض أن الرمز مكرر هنا ، لأننا نسمي نفس مكون InputField ، ونمرر نفس التسمية والقيمة ومعلمات onChange هناك. ويبدأون في تجفيف التعليمات البرمجية الخاصة بهم لتجنب الازدواجية الخيالية.
غالبًا ما يفعلون مثل هذا:
رمز النسخ
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>
);



نتيجة لذلك ، مع 17 سطرًا من كود jsx نحصل على 16 سطرًا من التكوين. أحسنت! هذا ما أفهمه الجاف. إذا كان لدينا 100 من هذه المدخلات هنا ، فإننا نحصل على 605 و 506 سطرًا على التوالي.

ولكن ، نتيجة لذلك ، حصلنا على رمز أكثر تعقيدًا ، ينتهك مبدأ KISS. بعد كل شيء ، يتألف الآن من تجريدين: الحقول وخوارزمية (نعم ، الخوارزمية هي أيضًا تجريد) ، والتي تحولها إلى شجرة من عناصر رد الفعل. عند قراءة هذا الرمز ، سيكون علينا القفز باستمرار بين هذه التجريدات.

لكن أكبر مشكلة في هذا الرمز هي دعمه وتمديده.
تخيل ماذا سيحدث له إذا:
  • سيتم إضافة المزيد من أنواع الحقول ذات الخصائص المحتملة المختلفة
  • يتم تقديم الحقول أو لا يتم تقديمها بناءً على إدخال الحقول الأخرى
  • تتطلب بعض الحقول بعض المعالجة الإضافية عند التغيير
  • تعتمد القيم في الاختيارات على الاختيارات السابقة

يمكن متابعة هذه القائمة لفترة طويلة ...

لتنفيذ ذلك ، سيتعين عليك دائمًا تخزين بعض الوظائف في التكوين الخاص بك ، وسوف يتحول الرمز الذي يعرض التكوين تدريجيًا إلى Frankenstein ، مع أخذ مجموعة من الدعائم المختلفة وإلقائها على مكونات مختلفة.

سيتم توسيع التعليمات البرمجية التي تمت كتابتها من قبل ، استنادًا إلى تكوين المكونات ، بهدوء ، وتغييرها كما تريد ، بينما لن تصبح معقدة للغاية بسبب نقص التجريد الوسيط - خوارزمية تبني شجرة من عناصر رد الفعل من التكوين لدينا. هذا الرمز غير مكرر. إنها تتبع نمط تصميم معين فقط ، وبالتالي تبدو "منقوشة".

تحسين غير مجدي


بعد ظهور الخطافات في React ، كان هناك ميل إلى لف جميع المناولات والمكونات بشكل عشوائي في useCallbackو memo. رجاءا لا تفعل ذلك! لم يتم توفير هذه الخطافات من قبل مطوري React لأن React بطيء وكل شيء يحتاج إلى التحسين. أنها توفر مساحة لتحسين التطبيق الخاص بك في حالة مواجهة مشكلات في الأداء. وحتى إذا واجهت مثل هذه المشاكل، لا تحتاج إلى التفاف كامل المشروع في memoو useCallback. استخدم Profiler لتحديد المشاكل وبعد ذلك فقط استظهر في المكان الصحيح.

سيؤدي الحفظ دائمًا إلى زيادة تعقيد شفرتك ، ولكن ليس دائمًا أكثر إنتاجية.

دعونا نلقي نظرة على الكود من مشروع حقيقي. قارن كيف تبدو الوظيفة مع useCallbackأو بدون:
رمز النسخ
  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);
  };



زادت قابلية قراءة الرمز بشكل واضح بعد إزالة الغلاف ، لأن الرمز المثالي هو افتقاره.

لم يزد أداء هذا الرمز ، لأنه يتم استخدام هذه الوظيفة على النحو التالي:
رمز النسخ
  <RightFooterButton onClick={applyFilters}>APPLY</RightFooterButton>



أين RightFooterButton- انها مجرد styled.buttonمن styled-componentsالذي سيتم تحديث بسرعة كبيرة. لكن استهلاك الذاكرة يزيد من تطبيقنا لأن React سيبقي دائمًا في الذاكرة selectedMetricsStatus، onApplyFilterClickوإصدار الوظيفة applyFilters، مرتبط بهذه التبعيات.

إذا لم تكن هذه الحجج كافية بالنسبة لك ، فاقرأ المقالة التي تناقش هذا الموضوع على نطاق أوسع.




الموجودات


  • أشكال رد الفعل سهلة. تنشأ مشاكل معهم بسبب المطورين أنفسهم ووثائق التفاعل ، حيث لا يتم تغطية هذا الموضوع بتفاصيل كافية.
  • aka value onChange . useFieldChange, , , v-model Vue .
  • KISS YAGNI. DRY, , .
  • , , React- .
  • , .




P.S.


في البداية ، خططت للكتابة عن التحقق من صحة الأشكال المتداخلة المعقدة. لكن ، في النهاية ، قررت إعداد الجمهور لهذا ، بحيث تكون الصورة كاملة قدر الإمكان.
سيتمحور البرنامج التعليمي التالي حول التحقق من صحة النموذج البسيط الذي تم تنفيذه في هذا البرنامج التعليمي.
شكرا جزيلا لكل من قرأ حتى النهاية!

لا يهم كيف تكتب الرمز أو ما تفعله ، الشيء الرئيسي هو الاستمتاع بها.

All Articles