JavaRush /مدونة جافا /Random-AR /لماذا NULL سيئة؟
Helga
مستوى

لماذا NULL سيئة؟

نشرت في المجموعة

لماذا NULL سيئة؟

فيما يلي مثال بسيط لاستخدام NULL في Java: public Employee getByName(String name) { int id = database.find(name); if (id == 0) { return null; } return new Employee(id); } ما الخطأ في هذه الطريقة؟ يمكنه إرجاع NULL بدلاً من كائن - وهذا هو الخطأ. يعد استخدام NULL ممارسة سيئة في OOP ويجب تجنبها بأي ثمن. لقد تم بالفعل نشر عدد لا بأس به من الآراء المختلفة حول هذه القضية، بما في ذلك العرض التقديمي الذي قدمه توني هور بعنوان "الروابط الصفرية: خطأ مليار دولار" وكتاب ديفيد ويست بأكمله "التفكير الموجه للكائنات". سأحاول هنا تلخيص جميع الوسائط وإظهار أمثلة لكيفية تجنب استخدام NULL عن طريق استبدالها ببنيات مناسبة موجهة للكائنات. أولاً، دعونا نلقي نظرة على بديلين محتملين لـ NULL. الأول هو نمط تصميم كائن فارغ (من الأفضل تنفيذه باستخدام ثابت): public Employee getByName(String name) { int id = database.find(name); if (id == 0) { return Employee.NOBODY; } return Employee(id); } البديل الثاني المحتمل هو "الفشل السريع" عن طريق طرح استثناء إذا تعذر إرجاع الكائن: public Employee getByName(String name) { int id = database.find(name); if (id == 0) { throw new EmployeeNotFoundException(name); } return Employee(id); } الآن دعونا نلقي نظرة على الحجج ضد استخدام NULL قبل كتابة هذا أثناء ومن خلال المنشور تعرفت، بالإضافة إلى العروض المذكورة أعلاه التي قدمها توني هور وكتاب ديفيد ويست، على عدد من الإصدارات. هذه هي "Clean Code" لروبرت مارتن، و "Clean Code" لستيف ماكونيل، و "Say No to NULL" لجون سونميز، ومناقشة حول StackOverflow بعنوان "هل تعتبر إرجاع NULL ممارسة سيئة؟"
معالجة الأخطاء يدويًا
في كل مرة تتلقى كائنًا كمدخل، يجب عليك التحقق مما إذا كان مرجعًا لكائن حقيقي أم NULL. إذا نسيت التحقق، فقد تتم مقاطعة برنامجك في منتصف التنفيذ بواسطة NullPointerExeption (NPE). ولهذا السبب، يبدأ الكود الخاص بك في الامتلاء بالعديد من عمليات التحقق وفروع if/then/else. // this is a terrible design, don't reuse Employee employee = dept.getByName("Jeffrey"); if (employee == null) { System.out.println("can't find an employee"); System.exit(-1); } else { employee.transferTo(dept2); } هذه هي الطريقة التي يجب أن يتم بها التعامل مع الاستثناءات في لغة C وغيرها من لغات البرمجة الإجرائية الصارمة. في OOP، تم تقديم معالجة الاستثناءات بشكل أساسي للتخلص من كتل المعالجة المكتوبة يدويًا. في OOP، نسمح للاستثناءات بالظهور حتى تصل إلى معالج الأخطاء على مستوى التطبيق، وهذا يجعل الكود الخاص بنا أكثر وضوحًا وأقصر: dept.getByName("Jeffrey").transferTo(dept2); ضع في اعتبارك أن المراجع NULL هي بقايا من أسلوب البرمجة الإجرائية واستخدم 1) كائنات فارغة أو 2) استثناءات بدلاً من ذلك .
فهم غامض
لنقل معنى ما يحدث بدقة في الاسم، getByName() يجب إعادة تسمية الطريقة إلى getByNameOrNullIfNotFound(). يجب أن يتم نفس الشيء لكل طريقة تُرجع كائنًا أو NULL، وإلا سيكون هناك غموض عند قراءة الكود. وبالتالي، لكي تكون أسماء الطرق دقيقة، يجب عليك إعطاء أسماء أطول للطرق. لتجنب الغموض، قم دائمًا بإرجاع كائن حقيقي أو كائن فارغ أو طرح استثناء. قد يجادل المرء بأننا في بعض الأحيان نحتاج فقط إلى إرجاع NULL لتحقيق النتيجة المرجوة. على سبيل المثال، تقوم طريقة get()واجهة الخريطة في Java بإرجاع NULL عندما لا يكون هناك المزيد من الكائنات في الخريطة. Employee employee = employees.get("Jeffrey"); if (employee == null) { throw new EmployeeNotFoundException(); } return employee; بفضل استخدام Map لـ NULL، يحتاج هذا الرمز إلى دورة بحث واحدة فقط للحصول على نتيجة. إذا أعدنا كتابة الخريطة بحيث تطرح طريقة get() استثناءً إذا لم يتم العثور على أي شيء، فسيبدو الكود الخاص بنا كما يلي: if (!employees.containsKey("Jeffrey")) { // first search throw new EmployeeNotFoundException(); } return employees.get("Jeffrey"); // second search من الواضح أن هذه الطريقة أبطأ مرتين من الطريقة الأصلية. ما يجب القيام به؟ يوجد (ليس هناك أي إساءة مقصودة) خلل في التصميم في واجهة الخريطة. يجب أن تقوم طريقة get() الخاصة بها بإرجاع Iterator، ومن ثم سيبدو الكود الخاص بنا كما يلي: Iterator found = Map.search("Jeffrey"); if (!found.hasNext()) { throw new EmployeeNotFoundException(); } return found.next(); بالمناسبة، هذه هي بالضبط الطريقة التي تم بها تصميم طريقة STL Map::find() في لغة C++.
التفكير الحسابي مقابل التفكير الشيئي
سطر التعليمات البرمجية if (employee == null) مفهوم تمامًا لشخص يعرف أن الكائن في Java هو مؤشر إلى بنية البيانات، وأن NULL هو مؤشر إلى لا شيء (في معالجات Intel x86 - 0x00000000). ومع ذلك، إذا بدأت في التفكير بأسلوب الكائن، فإن هذا الخط يصبح أقل منطقية بكثير. إليك ما يبدو عليه الكود الخاص بنا من وجهة نظر الكائن:
- مرحبا، هل هذا قسم تطوير البرمجيات؟ - نعم. - من فضلك قم بدعوة موظفك جيفري إلى الهاتف. - انتظر لحظة... - مرحبا. - هل أنت فارغ؟
السؤال الأخير يبدو غريبا بعض الشيء، أليس كذلك؟ إذا، بدلاً من ذلك، بعد طلبك لدعوة جيفري إلى الهاتف، قام الشخص الموجود على الطرف الآخر بإغلاق الخط، فهذا سيسبب لنا بعض الصعوبات (استثناء). في هذه الحالة، يمكننا محاولة معاودة الاتصال، أو يمكننا إبلاغ رئيسنا بأننا لم نتمكن من التحدث إلى جيفري وإكمال مهمتنا الرئيسية. بالإضافة إلى ذلك، على الجانب الآخر، قد يُطلب منك التحدث إلى شخص آخر، والذي، على الرغم من أنه ليس جيفري، يمكنه إما مساعدتك في معظم أسئلتك، أو رفض المساعدة إذا كنا بحاجة إلى معرفة شيء لا يعرفه سوى جيفري (الكائن الصفري). ).
الفشل البطيء
بدلاً من الخروج بسرعة ، يحاول الكود أعلاه أن يموت ببطء، مما يؤدي إلى قتل الآخرين على طول الطريق. فبدلاً من السماح للجميع بمعرفة حدوث خطأ ما وأنه بحاجة إلى البدء في معالجة الحدث الاستثنائي على الفور، فإنه يحاول إخفاء فشله عن العميل. وهذا مشابه جدًا لمعالجة الاستثناءات اليدوية التي ناقشناها أعلاه. إن جعل التعليمات البرمجية الخاصة بك هشة قدر الإمكان والسماح لها بالكسر إذا لزم الأمر هي ممارسة جيدة. اجعل أساليبك تتطلب الكثير من البيانات التي تعمل بها. اسمح لهم بالشكوى عن طريق طرح استثناءات إذا كانت البيانات المقدمة لهم غير كافية، أو إذا كانت البيانات ببساطة غير مناسبة للاستخدام بالطريقة المقصودة. بخلاف ذلك، قم بإرجاع كائن فارغ يتصرف بطريقة تقليدية ويطرح استثناءات في جميع الحالات الأخرى. public Employee getByName(String name) { int id = database.find(name); Employee employee; if (id == 0) { employee = new Employee() { @Override public String name() { return "anonymous"; } @Override public void transferTo(Department dept) { throw new AnonymousEmployeeException( "I can't be transferred, I'm anonymous" ); } }; } else { employee = Employee(id); } return employee; }
كائنات قابلة للتغيير وغير مكتملة
بشكل عام، يوصى بشدة بتصميم الكائنات لتكون غير قابلة للتغيير. وهذا يعني أن الكائن يجب أن يتلقى جميع البيانات اللازمة عند إنشائه ولا يغير حالته أبدًا خلال دورة حياته بأكملها. غالبًا ما يتم استخدام القيم NULL في نمط التصميم Lazy Loading لجعل الكائنات غير مكتملة وقابلة للتغيير. مثال: public class Department { private Employee found = null; public synchronized Employee manager() { if (this.found == null) { this.found = new Employee("Jeffrey"); } return this.found; } } على الرغم من انتشار هذه التقنية إلا أنها تعتبر نمطًا مضادًا لـ OOP. ويرجع ذلك أساسًا إلى أنه يجعل الكائن مسؤولاً عن مشكلات الأداء في النظام الأساسي للحوسبة، وهو بالضبط ما لا يمكن لكائن الموظف أن يعرفه. بدلاً من إدارة حالته والتصرف بطريقة تتفق مع الغرض المقصود منه، يضطر الكائن إلى القلق بشأن تخزين نتائجه مؤقتًا - وهذا ما يؤدي إليه التحميل البطيء. لكن التخزين المؤقت ليس على الإطلاق ما يفعله الموظف في المكتب، أليس كذلك؟ مخرج؟ لا تستخدم التحميل البطيء بطريقة بدائية كما في المثال أعلاه. بدلاً من ذلك، انقل مشكلة التخزين المؤقت إلى طبقة أخرى من التطبيق الخاص بك. على سبيل المثال، في Java يمكنك الاستفادة من ميزات البرمجة الموجهة نحو الجوانب. على سبيل المثال، يحتوي jcabi-aspects على تعليق توضيحي @Cacheable يقوم بتخزين القيمة المرجعة للأسلوب مؤقتًا. import com.jcabi.aspects.Cacheable; public class Department { @Cacheable(forever = true) public Employee manager() { return new Employee("Jacky Brown"); } } أتمنى أن يكون هذا التحليل مقنعًا بدرجة كافية لكي تتوقف عن إلغاء الكود الخاص بك :) المقال الأصلي هنا . قد تكون مهتمًا أيضًا بموضوعات مثل: • حاويات DI هي ملوثات الكودGetters/Setters. شر. فترة. مكافحة الأنماط في OOPتجنب تسلسل السلسلةيجب أن تكون الكائنات غير قابلة للتغيير
تعليقات
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION