الآن يدرك الجميع أن استخدام مشغل GOTO ليس بالأمر السيء فحسب ، بل إنه تدريب فظيع. انتهى النقاش حول استخدامه في الثمانينات من القرن العشرين واستبعد من معظم لغات البرمجة الحديثة. ولكن ، كما يليق الشر الحقيقي ، تمكن من إخفاء نفسه والقيامه في القرن 21 تحت ستار الاستثناءات.
الاستثناءات ، من ناحية ، هي مفهوم بسيط إلى حد ما في لغات البرمجة الحديثة. من ناحية أخرى ، غالباً ما يتم استخدامها بشكل غير صحيح. هناك قاعدة بسيطة ومعروفة - الاستثناءات هي فقط للتعامل مع الضرر. وهو مجرد تفسير غير مفهوم لمفهوم "الانهيار" يؤدي إلى جميع مشاكل استخدام GOTO.
مثال نظري
الفرق بين الأعطال وسيناريوهات الأعمال السلبية مرئي بوضوح في نافذة تسجيل الدخول مع حالة استخدام بسيطة للغاية:
- مستخدم يدخل تسجيل الدخول / كلمة المرور.
- ينقر المستخدم على زر "تسجيل الدخول".
- يرسل تطبيق العميل طلبًا إلى الخادم.
- يتحقق الخادم بنجاح من اسم المستخدم / كلمة المرور (يعتبر وجود الزوج المقابل نجاحًا).
- يرسل الخادم معلومات إلى العميل تفيد بأن المصادقة كانت ناجحة ورابطًا إلى صفحة النقل.
- يذهب العميل إلى الصفحة المحددة.
وتمديد سلبي واحد:
4.1. لم يعثر الخادم على الزوج المطابق لتسجيل الدخول / كلمة المرور ويرسل إشعارًا إلى العميل حول هذا.
يعتبر اعتبار هذا السيناريو 4.1 "مشكلة" ، وبالتالي يجب تنفيذه باستخدام استثناء هو خطأ شائع إلى حد ما. هذا في الواقع ليس هو الحال. يُعد عدم تطابق تسجيل الدخول وكلمة المرور جزءًا من تجربة المستخدم القياسية كما هو منصوص عليه في منطق الأعمال في البرنامج النصي. يتوقع عملاؤنا من رجال الأعمال هذا التطور. لذلك ، هذا ليس انهيارًا ولا يمكنك استخدام الاستثناءات هنا.
الأعطال هي: انقطاع الاتصال بين العميل والشمال ، وعدم إمكانية الوصول إلى نظام إدارة قواعد البيانات ، مخطط غير صحيح في قاعدة البيانات. وهناك مليون سبب آخر يخرق طلباتنا ولا علاقة له بمنطق عمل المستخدم.
في أحد المشاريع ، التي شاركت في تطويرها ، كان هناك منطق تسجيل دخول أكثر تعقيدًا. عن طريق إدخال كلمة مرور خاطئة 3 مرات على التوالي ، تم حظر المستخدم مؤقتًا لمدة 15 دقيقة. الحصول على 3 مرات على التوالي في قفل مؤقت ، تلقى المستخدم قفل دائم. كانت هناك أيضا قواعد إضافية اعتمادا على نوع المستخدم. تطبيق الاستثناء جعل من الصعب للغاية إدخال قواعد جديدة.
سيكون من المثير للاهتمام التفكير في هذا المثال ، ولكنه كبير جدًا وغير مرئي جدًا. كيف تصبح الشفرة المربكة مع منطق الأعمال على الاستثناءات واضحة وموجزة ، سأريك في مثال آخر.
مثال تحميل خصائص
حاول إلقاء نظرة على هذه الشفرة وفهم ما تفعله بوضوح. الإجراء ليس كبيرا مع منطق بسيط إلى حد ما. مع أسلوب برمجة جيد ، يجب ألا يتجاوز فهم جوهره أكثر من 2-3 دقائق (لا أتذكر المدة التي استغرقتها في فهم هذا الرمز تمامًا ، ولكن بالتأكيد أكثر من 15 دقيقة).
private WorkspaceProperties(){ Properties loadedProperties = readPropertiesFromFile(WORK_PROPERTIES_PATH, true);
لذلك ، دعونا نكشف السر - ما يحدث هنا. يتم WORK_PROPERTIES
الخصائص من ملفين - WORK_PROPERTIES
المطلوبة و MY_WORK_PROPERTIES
الإضافية ، إضافة إلى متجر الخصائص العام. هناك فارق بسيط - لا نعرف بالضبط أين يقع ملف الخصائص المحددة - يمكن أن يكمن في الدليل الحالي وفي أدلة السلف (بحد أقصى ثلاثة مستويات).
هناك شيئان على الأقل throwIfNotExists
هنا: المعلمة throwIfNotExists
وكتلة المنطق الكبيرة في ملف catch FileNotFoundException
. كل هذه التلميحات غير واضحة - يتم استخدام الاستثناءات لتطبيق منطق العمل (ولكن كيف يمكن تفسير ذلك في أحد السيناريوهات ، بإلقاء استثناء هو الفشل ، والآخر لا؟).
جعل العقد الصحيح
أولا ، throwIfNotExists
نتعامل مع throwIfNotExists
. عند العمل مع استثناءات ، من المهم للغاية فهم المكان الذي يجب معالجته فيه بدقة من حيث حالات الاستخدام. في هذه الحالة ، من الواضح أن طريقة readPropertiesFromFile
لا يمكنها أن تقرر متى يكون غياب ملف "سيئًا" ومتى يكون "جيدًا". يتم اتخاذ هذا القرار في وقت دعوتها. تظهر التعليقات أننا نقرر ما إذا كان يجب أن يكون هذا الملف موجودًا أم لا. ولكن في الواقع ، نحن مهتمون ليس بالملف نفسه ، لكن بالإعدادات منه. لسوء الحظ ، هذا لا يتبع من التعليمات البرمجية.
نصلح كل من هذه العيوب:
Properties loadedProperties = readPropertiesFromFile(WORK_PROPERTIES_PATH); if (loadedProperties.isEmpty()) { throw new RuntimeException("Can`t load workspace properties"); } loadedProperties = readPropertiesFromFile(MY_WORK_PROPERTIES_PATH); getProperties().putAll( loadedProperties );
الآن يتم عرض الدلالات بوضوح -
يجب تحديد MY_WORK_PROPERTIES
، ولكن MY_WORK_PROPERTIES
ألا MY_WORK_PROPERTIES
. أيضًا ، عند إعادة بيع readPropertiesFromFile
، لاحظت أن readPropertiesFromFile
لا يمكن أن يعود أبدًا readPropertiesFromFile
واستفاد من ذلك عند قراءة MY_WORK_PROPERTIES
.
نتحقق دون كسر
إعادة بيع الديون السابقة أثرت أيضا على التنفيذ ، ولكن ليس بشكل كبير. لقد قمت فقط بحذف throwIfNotExists
معالجة throwIfNotExists
:
if (throwIfNotExists) throw new RuntimeException(…);
بعد فحص التطبيق عن كثب ، نبدأ في فهم منطق مؤلف الرمز للبحث عن ملف. أولاً ، يتم التحقق من وجود الملف في الدليل الحالي ، وإذا لم يتم العثور عليه ، فإننا نتحقق من مستوى أعلى ، إلخ. أي يصبح من الواضح أن الخوارزمية تنص على عدم وجود ملف. في هذه الحالة ، يتم التحقق باستخدام استثناء. أي تم انتهاك المبدأ - لا يُنظر إلى الاستثناء على أنه "شيء ما قد تم كسره" ، ولكن كجزء من منطق العمل.
هناك وظيفة للتحقق من توفر ملف لقراءة File.canRead()
. باستخدامه ، يمكنك التخلص من منطق العمل في catch
try{ File file = new File(relativePath + filepath); is = new FileInputStream(file); isr = new InputStreamReader( is, "UTF-8"); loadedProperties.load(isr); loadingTryLeft = 0; } catch( FileNotFoundException e) { loadingTryLeft -= 1; if (loadingTryLeft > 0) relativePath += "../"; else throw e; } finally { if (is != null) is.close(); if (isr != null) isr.close(); } }
عند تغيير الكود ، نحصل على ما يلي:
private Properties readPropertiesFromFile(String filepath) { Properties loadedProperties = new Properties(); System.out.println("Try loading workspace properties" + filepath); try { int loadingTryLeft = 3; String relativePath = ""; while (loadingTryLeft > 0) { File file = new File(relativePath + filepath); if (file.canRead()) { InputStream is = null; InputStreamReader isr = null; try { is = new FileInputStream(file); isr = new InputStreamReader(is, "UTF-8"); loadedProperties.load(isr); loadingTryLeft = 0; } finally { if (is != null) is.close(); if (isr != null) isr.close(); } } else { loadingTryLeft -= 1; if (loadingTryLeft > 0) { relativePath += "../"; } else { throw new FileNotFoundException(); } } } System.out.println("Found file " + filepath); } catch (FileNotFoundException e) { System.out.println("File not found " + filepath); } catch (IOException e) { throw new RuntimeException("Can`t read " + filepath, e); } return loadedProperties; }
لقد خفضت أيضًا مستوى المتغيرات ( is
، isr
) إلى الحد الأدنى المسموح به.
إعادة هيكلة بسيطة مثل يحسن كثيرا من قراءة التعليمات البرمجية. تعرض الشفرة الخوارزمية مباشرةً (إذا كان الملف موجودًا ، فإننا نقرأه ، وإلا فإننا نخفض عدد المحاولات ونبحث في الدليل أعلاه).
الكشف عن غوتو
ضع في اعتبارك بالتفصيل ما يحدث في الموقف إذا لم يتم العثور على الملف:
} else { loadingTryLeft -= 1; if (loadingTryLeft > 0) { relativePath += "../"; } else { throw new FileNotFoundException(); } }
يمكن ملاحظة أن الاستثناء هنا يستخدم لمقاطعة دورة التنفيذ وتنفيذ وظيفة GOTO بالفعل.
للمتشككين ، سوف نقوم بتغيير آخر. بدلاً من استخدام عكاز صغير في النموذج loadingTryLeft = 0
(عكاز ، لأنه في الواقع لا تؤدي المحاولة الناجحة إلى loadingTryLeft = 0
عدد المحاولات المتبقية) ، فنحن نشير بوضوح إلى أن قراءة الملف ستنتهي من الوظيفة (دون أن ننسى كتابة رسالة):
try { is = new FileInputStream(file); isr = new InputStreamReader(is, "UTF-8"); loadedProperties.load(isr); System.out.println("Found file " + filepath); return loadedProperties; } finally {
هذا يسمح لنا باستبدال شرط while (loadingTryLeft > 0)
بـ while(true)
:
try { int loadingTryLeft = 3; String relativePath = ""; while (true) { File file = new File(relativePath + filepath); if (file.canRead()) { InputStream is = null; InputStreamReader isr = null; try { is = new FileInputStream(file); isr = new InputStreamReader(is, "UTF-8"); loadedProperties.load(isr); System.out.println("Found file " + filepath); return loadedProperties; } finally { if (is != null) is.close(); if (isr != null) isr.close(); } } else { loadingTryLeft -= 1; if (loadingTryLeft > 0) { relativePath += "../"; } else { throw new FileNotFoundException();
للتخلص من رائحة كريهة واضحة throw new FileNotFoundException
، تحتاج إلى تذكر عقد الوظيفة. في أي حال ، تقوم الدالة بإرجاع مجموعة من الخصائص ، إذا لم يتمكنوا من قراءة الملف ، فسنعيده فارغًا. لذلك ، ليس هناك سبب لرمي استثناء والقبض عليه. الحالة المعتادة while (loadingTryLeft > 0)
كافية:
private Properties readPropertiesFromFile(String filepath) { Properties loadedProperties = new Properties(); System.out.println("Try loading workspace properties" + filepath); try { int loadingTryLeft = 3; String relativePath = ""; while (loadingTryLeft > 0) { File file = new File(relativePath + filepath); if (file.canRead()) { InputStream is = null; InputStreamReader isr = null; try { is = new FileInputStream(file); isr = new InputStreamReader(is, "UTF-8"); loadedProperties.load(isr); System.out.println("Found file " + filepath); return loadedProperties; } finally { if (is != null) is.close(); if (isr != null) isr.close(); } } else { loadingTryLeft -= 1; if (loadingTryLeft > 0) relativePath += "../"; } } System.out.println("file not found"); } catch (IOException e) { throw new RuntimeException("Can`t read " + filepath, e); } return loadedProperties; }
من حيث المبدأ ، من وجهة نظر العمل الصحيح مع استثناءات ، كل شيء هنا. هناك شك حول الحاجة إلى رمي RuntimeException في حالة حدوث مشكلات IOException ، ولكن دعنا نتركها كما هي للتوافق.
جلب لمعان
هناك القليل من الأشياء المتبقية التي يمكننا أن نجعلها أكثر مرونة ومفهومة:
- كشف اسم الأسلوب readPropertiesFromFile عن تنفيذه (بالمناسبة ، وكذلك رميات FileNotFoundException). من الأفضل أن نسميها أكثر حيادية وموجزة - loadProperties (...)
- الطريقة في وقت واحد بالبحث والقراءة. بالنسبة لي ، هاتان مسؤولتان مختلفتان يمكن تقسيمهما بطرق مختلفة.
- تمت كتابة الكود في الأصل تحت Java 6 ، ولكنه يستخدم الآن في Java 7. وهذا يسمح باستخدام الموارد القابلة للإغلاق.
- أعلم من التجربة أنه عند عرض معلومات حول ملف موجود أو غير موجود ، من الأفضل استخدام المسار الكامل للملف بدلاً من النسبي.
if (loadingTryLeft > 0) relativePath += "../";
- إذا نظرت بعناية إلى الكود ، يمكنك أن ترى - هذا الاختيار غير ضروري ، لأنه إذا تم استنفاد حد البحث ، فلن يتم استخدام القيمة الجديدة على أي حال. وإذا كان هناك شيء لا لزوم له في التعليمات البرمجية ، وهذا هو القمامة التي يجب إزالتها.
النسخة النهائية من الكود المصدري:
private WorkspaceProperties() { super(new Properties()); if (defaultInstance != null) throw new IllegalStateException(); Properties loadedProperties = readPropertiesFromFile(WORK_PROPERTIES_PATH); if (loadedProperties.isEmpty()) { throw new RuntimeException("Can`t load workspace properties"); } getProperties().putAll(loadedProperties); loadedProperties = readPropertiesFromFile(MY_WORK_PROPERTIES_PATH); getProperties().putAll(loadedProperties); System.out.println("Loaded properties:" + getProperties()); } private Properties readPropertiesFromFile(String filepath) { System.out.println("Try loading workspace properties" + filepath); try { int loadingTryLeft = 3; String relativePath = ""; while (loadingTryLeft > 0) { File file = new File(relativePath + filepath); if (file.canRead()) { return read(file); } else { relativePath += "../"; loadingTryLeft -= 1; } } System.out.println("file not found"); } catch (IOException e) { throw new RuntimeException("Can`t read " + filepath, e); } return new Properties(); } private Properties read(File file) throws IOException { try (InputStream is = new FileInputStream(file); InputStreamReader isr = new InputStreamReader(is, "UTF-8")) { Properties loadedProperties = new Properties(); loadedProperties.load(isr); System.out.println("Found file " + file.getAbsolutePath()); return loadedProperties; } }
ملخص
يوضح المثال الذي تم تحليله بوضوح ما تؤدي إليه معالجة التعليمات البرمجية المصدر من الإهمال. بدلاً من استخدام استثناء لمعالجة الانهيار ، تقرر استخدامه لتنفيذ منطق العمل. وأدى ذلك على الفور إلى تعقيد دعمها ، والذي انعكس في مزيد من التطوير لتلبية الاحتياجات الجديدة ، ونتيجة لذلك ، الخروج عن مبادئ البرمجة الهيكلية. سيساعدك استخدام قاعدة بسيطة - باستثناء الاستثناءات فقط - على تجنب العودة إلى عصر GOTO والحفاظ على رمزك نظيفًا ومفهومًا وقابل للتوسعة.