خطأ في التعامل مع Kotlin / Java: كيف نفعل ذلك بشكل صحيح؟


مصدر


خطأ في معالجة أي تطور يلعب دورا حاسما. يمكن أن يحدث أي خطأ في البرنامج تقريبًا: سيقوم المستخدم بإدخال بيانات غير صحيحة ، أو قد يحدث ذلك عبر http ، أو قد ارتكبنا خطأ أثناء كتابة التسلسل / إلغاء التسلسل وخلال تعطل البرنامج مع وجود خطأ. نعم ، قد تنفد مساحة القرص.


المفسد

¯_ (ツ) _ / ¯ ، لا توجد طريقة واحدة ، وفي كل موقف معين يجب عليك اختيار الخيار الأنسب ، ولكن هناك توصيات حول كيفية القيام بذلك بشكل أفضل.


مقدمة


لسوء الحظ (أو مجرد مثل هذه الحياة؟) ، هذه القائمة تطول وتطول. يحتاج المطور باستمرار إلى التفكير في حقيقة أنه قد يحدث خطأ في مكان ما ، وهناك حالتان:


  • عندما يحدث الخطأ المتوقع في استدعاء الوظيفة التي قدمناها ويمكن أن نحاول معالجتها ؛
  • عندما يحدث خطأ غير متوقع أثناء العملية التي لم نتوقعها.

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


قبل أن ننظر في طرق لمعالجة الأخطاء ، بضع كلمات حول استثناء (استثناءات):


استثناء



مصدر


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


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


لنلقِ نظرة على مثال. لنفترض أن لدينا وظيفة method يمكنها رمي PanicException محددًا. ستبدو هذه الوظيفة كما يلي:


 public void method() throws PanicException { } 

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


تتطلب الاستثناءات التي تم فحصها ، حسب المواصفات ، أن يتم إدراج جميع الاستثناءات الممكنة التي تم التحقق منها (أو سلف مشترك لها) في توقيع الوظيفة. لذلك ، إذا كانت لدينا سلسلة من المكالمات a -> b -> c وكانت الوظيفة الأكثر تداخلًا ترمي نوعًا من الاستثناء ، فيجب وضعها على الجميع في السلسلة. وإذا كان هناك العديد من الاستثناءات ، فيجب أن تحتوي الوظيفة العليا في التوقيع على وصف لها جميعًا.


لذا ، نظرًا لأن البرنامج يصبح أكثر تعقيدًا ، يؤدي هذا النهج إلى حقيقة أن الاستثناءات في الوظيفة العليا تنهار تدريجيًا إلى أسلاف مشتركين وتنتهي في النهاية إلى Exception . ما في هذا النموذج يصبح مشابهًا لاستثناء unchecked يتم unchecked وينفي جميع مزايا الاستثناءات المحددة.


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


لكن الأخير ، وربما أكبر مسمار في الاستثناءات المحددة "قاد" lambdas من Java 8. لا توجد استثناءات محددة ¯_ (ツ) _ / ¯ في توقيعها (حيث يمكن استدعاء أي وظيفة في lambda ، مع أي التوقيع) ، لذلك أي استدعاء دالة مع استثناء محدد من lambda يفرض أن تكون ملفوفة في إعادة توجيه استثناء كما لم يتم التحقق منه:


 Stream.of(1,2,3).forEach(item -> { try { functionWithCheckedException(); } catch (Exception e) { throw new RuntimeException("rethrow", e); } }); 

لحسن الحظ ، في مواصفات JVM لا توجد استثناءات محددة على الإطلاق ، لذلك في Kotlin لا يمكنك لف أي شيء في نفس امدا ، ولكن ببساطة استدعاء الوظيفة المطلوبة.


على الرغم من أحيانا ...

على الرغم من أن هذا يؤدي في بعض الأحيان إلى عواقب غير متوقعة ، على سبيل المثال ، العملية غير الصحيحة لـ @Transactional في Spring Framework ، والتي "تتوقع" استثناءات غير unckecked فقط. ولكن هذا هو أكثر من سمة من سمات الإطار ، وربما هذا السلوك في الربيع سيتغير في قضية جيثب المستقبل القريب.


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


خطأ في التعامل


الشيء الرئيسي الذي يجب فعله مع الأخطاء "غير المتوقعة" هو العثور على مكان يمكنك اعتراضه عليه. في لغات JVM ، يمكن أن يكون هذا إما نقطة إنشاء دفق أو نقطة تصفية / إدخال إلى طريقة http ، حيث يمكنك وضع اختبار جذاب مع معالجة الأخطاء التي unchecked . إذا كنت تستخدم أي إطار عمل ، فمن المحتمل أن يكون لديه بالفعل القدرة على إنشاء معالجات الأخطاء الشائعة ، على سبيل المثال ، في Spring Framework ، يمكنك استخدام الأساليب مع التعليق التوضيحي @ExceptionHandler .


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


  1. لا تزال تستخدم الاستثناءات ونفس تجربة المحاولة:


      int a = 10; int b = 20; int sum; try { sum = calculateSum(a,b); } catch (Exception e) { sum = -1; } 

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


  2. استخدم الطبقة المختومة كنتيجة للمكالمة (Kotlin).
    في Kotlin ، يمكنك تحديد عدد ورثة الفصل ، وجعلها قابلة للحساب في مرحلة الترجمة - وهذا يسمح للمترجم بالتحقق من أن جميع الخيارات الممكنة قد تم تحليلها في الكود. في Java ، يمكنك إنشاء واجهة مشتركة وعدة أحفاد ، ومع ذلك ، تفقد عمليات التحقق من مستوى الترجمة.


     sealed class Result data class SuccessResult(val value: Int): Result() data class ExceptionResult(val exception: Exception): Result() val a = 10 val b = 20 val sum = when (val result = calculateSum(a,b)) { is SuccessResult -> result.value is ExceptionResult -> { result.exception.printStackTrace() -1 } } 

    هنا نحصل على شيء مثل نهج خطأ golang عندما تحتاج إلى التحقق بوضوح من القيم الناتجة (أو تجاهلها بشكل صريح). الأسلوب عملي للغاية وملائم بشكل خاص عندما تحتاج إلى رمي الكثير من المعلمات في كل موقف. يمكن توسيع فئة Result بعدة طرق تسهل الحصول على النتيجة مع استثناء رمي أعلاه ، إن وجدت (أي لا نحتاج إلى معالجة الخطأ في مكان المكالمة). سيكون العيب الرئيسي فقط إنشاء كائنات زائدة وسيطة (وإدخال مطوّل قليلاً) ، لكن يمكن إزالته أيضًا باستخدام فئات inline (إذا كانت حجة واحدة كافية بالنسبة لنا). وكمثال معين ، هناك فئة Result من Kotlin. صحيح ، هو فقط للاستخدام الداخلي ، كما في المستقبل ، قد يتغير تطبيقه قليلاً ، ولكن إذا كنت ترغب في استخدامه ، فيمكنك إضافة علامة -Xallow-result-return-type .


  3. كأحد الأنواع المحتملة من المطالبة 2 ، استخدام النوع من البرمجة الوظيفية لـ Either ، والذي يمكن أن يكون نتيجة أو خطأ. يمكن أن يكون النوع نفسه إما فئة sealed أو فئة inline . فيما يلي مثال لاستخدام التطبيق من مكتبة arrow :


     val a = 10 val b = 20 val value = when(val result = calculateSum(a,b)) { is Either.Left -> { result.a.printStackTrace() -1 } is Either.Right -> result.b } 

    Either أنسب ما يناسب أولئك الذين يحبون النهج الوظيفي والذين يحبون بناء سلاسل الاتصال.


  4. استخدم Option أو نوع nullable من Kotlin:


     fun testFun() { val a = 10 val b = 20 val sum = calculateSum(a,b) ?: throw RuntimeException("some exception") } fun calculateSum(a: Int, b: Int): Int? 

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


  5. على غرار البند 4 ، يستخدم فقط قيمة القرص الثابت كعلامة خطأ:


     fun testFun() { val a = 10 val b = 20 val sum = calculateSum(a,b) if (sum == -1) { throw RuntimeException(“error”) } } fun calculateSum(a: Int, b: Int): Int 

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



النتائج


يمكن الجمع بين جميع الأساليب وفقًا للموقف ، ولا يوجد منها مناسب في جميع الحالات.


لذلك ، على سبيل المثال ، يمكنك تحقيق نهج golang للأخطاء باستخدام الطبقات sealed ، وحيث لا تكون مريحة للغاية ، انتقل إلى الأخطاء التي unchecked .


أو في معظم الأماكن ، nullable كعلامة أنه لم يكن من الممكن حساب القيمة أو الحصول عليها من مكان ما (على سبيل المثال ، كمؤشر على عدم العثور على القيمة في قاعدة البيانات).


وإذا كان لديك رمز وظيفي بالكامل مع arrow أو مكتبة أخرى مماثلة ، فمن الأفضل استخدام Either .


بالنسبة إلى خوادم http ، من الأسهل رفع جميع الأخطاء إلى النقاط المركزية وفقط في بعض الأماكن تجمع الطريقة nullable مع الطبقات sealed .


سأكون سعيدًا برؤية التعليقات التي تستخدمها ، أو ربما هناك طرق أخرى مناسبة لمعالجة الأخطاء؟


وبفضل كل من قرأ حتى النهاية!

Source: https://habr.com/ru/post/ar471766/


All Articles