ربما لست أنا وحيدًا بعد قراءة
الوثائق حول الفصول المختومة فكرت: "حسنًا. ربما ستكون مفيدة في يوم من الأيام. " لاحقًا ، عندما التقيت في عملي بمهمتين تمكنت من استخدام هذه الأداة بنجاح ، فكرت: "هذا ليس سيئًا. يجب عليك في كثير من الأحيان التفكير في التطبيق ". وأخيرًا ، صادفت وصفًا لفئة المهام في كتاب
Java الفعال (Joshua Bloch ، 3rd) (نعم ، في كتاب عن Java).
دعونا نلقي نظرة على تطبيق واحد ونقيمه من حيث الدلالات والأداء.
أعتقد أن كل من عمل مع واجهة المستخدم التقى ذات مرة بتنفيذ تفاعل واجهة المستخدم مع
الخدمة من خلال
حالات معينة ، حيث كانت
علامة النوع واحدة من السمات. عادة ما تعتمد آليات معالجة الحالة التالية في مثل هذه التطبيقات بشكل مباشر على العلامة المحددة. على سبيل المثال ، مثل هذا التنفيذ لطبقة
الدولة :
class State( val type: Type, val data: String?, val error: Throwable? ) { enum class Type { LOADING, ERROR, EMPTY, DATA } }
نذكر عيوب هذا التنفيذ (جربه بنفسك)ملاحظات من الفصل 23 من كتاب "تفضيل التسلسل الهرمي للفصول إلى فئات ذات علامات". أقترح التعرف عليها.
- استهلاك الذاكرة للسمات التي تمت تهيئتها لأنواع معينة فقط. يمكن للعامل أن يكون كبيرا في كميات كبيرة. يتفاقم الموقف إذا تم إنشاء كائنات افتراضية لملء السمات.
- الحمل الدلالي المفرط. يحتاج مستخدم الفصل إلى مراقبة ما هو النوع والسمات المتوفرة.
- دعم معقد في فئات منطق الأعمال. افترض تنفيذ حيث يمكن للكائن تنفيذ بعض العمليات على بياناته. سيبدو مثل هذا الفصل كمحصد ، وقد يصبح من الصعب إضافة نوع أو عملية جديدة.
قد تبدو معالجة حالة جديدة كما يلي:
fun handleState(state: State) { when(state.type) { State.Type.LOADING -> onLoading() State.Type.ERROR -> state.error?.run(::onError) ?: throw AssertionError("Unexpected error state: $state") State.Type.EMPTY -> onEmpty() State.Type.DATA -> state.data?.run(::onData) ?: throw AssertionError("Unexpected data state: $state") } } fun onLoading() {} fun onError(error: Throwable) {} fun onEmpty() {} fun onData(data: String) {}
يرجى ملاحظة أنه في حالات مثل ERROR و DATA ، فإن المترجم غير قادر على تحديد أمان استخدام السمات ، لذلك يجب على المستخدم كتابة رمز زائدة. لا يمكن الكشف عن التغييرات في دلالات إلا في وقت التشغيل.
فئة مختومة
من خلال إعادة هيكلة بسيطة ، يمكننا تقسيم دولتنا إلى مجموعة من الفئات:
sealed class State // stateless - singleton object Loading : State() data class Error(val error: Throwable) : State()
من جانب المستخدم ، نحصل على معالجة الحالة ، حيث سيتم تحديد إمكانية الوصول إلى السمات على مستوى اللغة ، وسيؤدي سوء الاستخدام إلى حدوث أخطاء في مرحلة التصنيف:
fun handleState(state: State) { when(state) { Loading -> onLoading() is Error -> onError(state.error) Empty -> onEmpty() is Data -> onData(state.data) } }
نظرًا لوجود سمات مهمة فقط في النسخ ، يمكننا التحدث عن توفير الذاكرة ، والأهم من ذلك ، تحسين دلالات. لا يحتاج مستخدمو الفئات المختومة إلى تنفيذ القواعد يدويًا للعمل مع السمات اعتمادًا على
علامة النوع ؛ يتم ضمان توفر السمات عن طريق الفصل في الأنواع.
هل كل هذا مجاني؟
لا بد أن مطوري جافا الذين جربوا Kotlin قد نظروا إلى الشفرة المترجمة لترى كيف تبدو مصطلحات Java Kotlin. تعبير مع
متى سيبدو شيئًا مثل هذا:
public static final void handleState(@NotNull State state) { Intrinsics.checkParameterIsNotNull(state, "state"); if (Intrinsics.areEqual(state, Loading.INSTANCE)) { onLoading(); } else if (state instanceof Error) { onError(((Error)state).getError()); } else if (Intrinsics.areEqual(state, Empty.INSTANCE)) { onEmpty(); } else if (state instanceof Data) { onData(((Data)state).getData()); } }
قد تكون الفروع التي تحتوي على وفرة من
الأمثلة مثيرة للقلق بسبب الصور النمطية حول "علامة الرمز السيئ" و "تأثير الأداء" ، ولكن ليس لدينا أي فكرة. من الضروري مقارنة سرعة التنفيذ بطريقة أو بأخرى ، على سبيل المثال ، باستخدام
jmh .
استنادًا إلى مقالة
"قياس سرعة شفرة جافا بشكل صحيح" ، تم إعداد
اختبار لمعالجة أربع حالات (LOADING ، ERROR ، EMPTY ، DATA) ، إليك نتائجها:
Benchmark Mode Cnt Score Error Units CompareSealedVsTagged.sealed thrpt 500 940739,966 ± 5350,341 ops/s CompareSealedVsTagged.tagged thrpt 500 1281274,381 ± 10675,956 ops/s
يمكن ملاحظة أن التنفيذ المختوم يعمل بشكل أبطأ ≈ 25٪ (كان هناك افتراض بأن التأخر لن يتجاوز 10-15٪).
إذا كان لدينا أربعة تأخيرات في أربعة أنواع ، مع زيادة الأنواع (عدد أمثلة الشيكات) ، فيجب أن يتأخر التأخر فقط. للتحقق ، سنزيد عدد الأنواع إلى 16 نوعًا (لنفترض أننا تمكنا من الحصول على مثل هذا التسلسل الهرمي الواسع):
Benchmark Mode Cnt Score Error Units CompareSealedVsTagged.sealed thrpt 500 149493,062 ± 622,313 ops/s CompareSealedVsTagged.tagged thrpt 500 235024,737 ± 3372,754 ops/s
إلى جانب انخفاض الإنتاجية ، زاد تأخر المبيعات المختومة إلى ≈35٪ - لم تحدث معجزة.
الخلاصة
في هذه المقالة ، لم نكتشف أمريكا ، كما أن التطبيقات المختومة في الفروع على سبيل المثال تعمل بشكل أبطأ من مقارنات الروابط.
ومع ذلك ، يجب التعبير عن فكرتين:
- في معظم الحالات ، نرغب في العمل باستخدام دلالات جيدة للكود ، وتسريع التطوير بسبب تلميحات إضافية من IDE وفحص المترجم - في مثل هذه الحالات ، يمكنك استخدام فئات مختومة
- إذا لم تتمكن من التضحية بالأداء في مهمة ما ، يجب عليك تجاهل التطبيق المختوم واستبداله ، على سبيل المثال ، بتنفيذ علامة. ربما يجب عليك التخلي تمامًا عن kotlin لصالح اللغات ذات المستوى الأدنى