اكتب الرمز الخاص بك في النهاية

مرحباً هبر!


في اليوم الآخر ، حصلت مرة أخرى على كود النوع


if(someParameter.Volatilities.IsEmpty()) { // We have to report about the broken channels, however we could not differ it from just not started cold system. // Therefore write this case into the logs and then in case of emergency IT Ops will able to gather the target line Log.Info("Channel {0} is broken or was not started yet", someParameter.Key) } 

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


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


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


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


ومع ذلك ، إذا كنت تلعب منذ فترة طويلة - مرحبا بكم في القط.


استخدام نمط الزائر


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


في هذه الحالة ، يتحول المثال من الرأس إلى رمز النموذج:


 class Response { public IVolatilityResponse Data { get; } } interface IVolatilityResponse { TOutput Visit<TInput, TOutput>(IVolatilityResponseVisitor<TInput, TOutput> visitor, TInput input) } class VolatilityValues : IVolatilityResponse { public Surface Data; TOutput Visit<TInput, TOutput>(IVolatilityResponseVisitor<TInput, TOutput> visitor, TInput input) => visitor.Visit(this, input); } class CalculationIsBroken : IVolatilityResponse { TOutput Visit<TInput, TOutput>(IVolatilityResponseVisitor<TInput, TOutput> visitor, TInput input) => visitor.Visit(this, input); } interface IVolatilityResponseVisitor<TInput, TOutput> { TOutput Visit(VolatilityValues instance, TInput input); TOutput Visit(CalculationIsBroken instance, TInput input); } 

مع هذا النوع من المعالجة:


  • نحن بحاجة إلى المزيد من التعليمات البرمجية. للأسف ، إذا كنا نريد التعبير عن مزيد من المعلومات في النموذج ، فيجب أن يكون أكثر.
  • بسبب هذا النوع من الميراث ، لم يعد بإمكاننا إجراء تسلسل Response إلى json / protobuf ، حيث يتم فقدان معلومات النوع هناك. سيتعين علينا إنشاء حاوية خاصة ستقوم بذلك (على سبيل المثال ، يمكنك إنشاء فصل يحتوي على حقل منفصل لكل تطبيق ، ولكن سيتم ملء واحد منهم فقط).
  • يتطلب تمديد النموذج (على سبيل المثال إضافة فئات جديدة) توسيع IVolatilityResponseVisitor<TInput, TOutput> ، مما يعني أن المترجم سوف IVolatilityResponseVisitor<TInput, TOutput> على دعمها في الشفرة. لن ينسى المبرمج معالجة النوع الجديد ، وإلا فلن يتم تجميع المشروع.
  • نظرًا للكتابة الثابتة ، لا نحتاج إلى تخزين الوثائق في مكان ما باستخدام مجموعات محتملة من الحقول ، إلخ. وصفنا جميع الخيارات الممكنة في التعليمات البرمجية المفهومة لكل من المترجم والشخص. لن نحصل على التزامن بين الوثائق والرمز ، لأننا نستطيع الاستغناء عن الأول.

حول تقييد الميراث بلغات أخرى


يحتوي عدد من اللغات الأخرى (على سبيل المثال ، Scala أو Kotlin ) على كلمات رئيسية تسمح لك بحظر الوراثة من نوع معين ، في ظل ظروف معينة. وهكذا ، في مرحلة التجميع ، نعرف كل الأحفاد المحتملين من نوعنا.


على وجه الخصوص ، يمكن إعادة كتابة المثال أعلاه في Kotlin مثل هذا:


 class Response ( val data: IVolatilityResponse ) sealed class VolatilityResponse class VolatilityValues : VolatilityResponse() { val data: Surface } class CalculationIsBroken : VolatilityResponse() 

لقد تبين أنها أقل بقليل من الكود ، لكننا الآن في عملية التجميع نعلم أن جميع VolatilityResponse المحتملين هم في نفس الملف ، مما يعني أن الكود التالي لن يتم تجميعه ، حيث أننا لم نستعرض كل القيم الممكنة للفئة.


 fun getResponseString(response: VolatilityResponse) = when(response) { is VolatilityValues -> data.toString() } 

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


 fun getResponseString(response: VolatilityResponse) { when(response) { is VolatilityValues -> println(data.toString()) } } 

ليست كل الأنواع البدائية تعني نفس الشيء


النظر في تطوير نموذجي نسبيا لقاعدة البيانات. على الأرجح ، في مكان ما في الكود ، سيكون لديك معرفات الكائنات. على سبيل المثال:


 class Group { public int Id { get; } public string Name { get; } } class User { public int Id { get; } public int GroupId { get; } public string Name { get; } } 

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


 public bool IsInGroup(User user, Group group) { return user.Id == group.Id; } public User CreateUser(string name, Group group) { return new User { Id = group.Id, GroupId = group.Id, name = name } } 

الإجابة على الأرجح لا ، لأننا نقارن Id المستخدم Id المجموعة في المثال الأول. وفي المجموعة الثانية ، قمنا عن طريق الخطأ بتعيين id من Group id من User .


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


علاوة على ذلك ، ستعمل الطرق التي لها نفس المعلمات بشكل صحيح ، حيث لن يتم تكرارها الآن:


 public void SetUserGroup(UserId userId, GroupId groupId) { /* some sql code */ } 

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


ويمكنك القيام بذلك على النحو التالي:


 class GroupId { public int Id { get; } public bool Equals(GroupId groupId) => Id == groupId?.Id; [Obsolete("GroupId can be equal only with GroupId", error: true)] public override bool Equals(object obj) => Equals(obj as GroupId) public static bool operator==(GroupId id1, GroupId id2) { if(ReferenceEquals(id1, id2)) return true; if(ReferenceEquals(id1, null) || ReferenceEquals(id2, null)) return false; return id1.Id == id2.Id; } [Obsolete("GroupId can be equal only with GroupId", error: true)] public static bool operator==(object _, GroupId __) => throw new NotSupportedException("GroupId can be equal only with GroupId") [Obsolete("GroupId can be equal only with GroupId", error: true)] public static bool operator==(GroupId _, object __) => throw new NotSupportedException("GroupId can be equal only with GroupId") } 

نتيجة لذلك:


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

أكثر قليلا عن sql والقيود


غالبًا في طلباتنا للأنواع ، يتم تقديم قواعد إضافية يسهل التحقق منها. في أسوأ الحالات ، تبدو بعض الوظائف مثل هذا:


 void SetName(string name) { if(name == null || name.IsEmpty() || !name[0].IsLetter || !name[0].IsCapital || name.Length > MAX_NAME_COLUMN_LENGTH) { throw .... } /**/ } 

أي أن الوظيفة تأخذ نوعًا واسعًا جدًا من المدخلات ، ثم تقوم بتشغيل الاختبارات. هذا ليس هو الحال عمومًا منذ:


  • لم نوضح للمبرمج والمترجم ما نريده هنا.
  • في وظيفة أخرى مماثلة ، سوف تحتاج إلى نسخ الشيكات.
  • عندما تلقينا string تشير إلى name ، لم نقع على الفور ، ولكن لسبب ما استمر التنفيذ في الوقوع على بعض تعليمات المعالج لاحقًا.

السلوك الصحيح:


  • قم بإنشاء نوع منفصل (في حالتنا ، على ما يبدو ، Name ).
  • في ذلك ، القيام بجميع عمليات التحقق والتحقق اللازمة.
  • التفاف string في Name في أسرع وقت ممكن للحصول على خطأ في أسرع وقت ممكن.

نتيجة لذلك ، نحصل على:


  • كود أقل ، نظرًا لأننا قمنا بفحص الشيكات name في المنشئ.
  • إستراتيجية Fail Fast - الآن ، بعد أن تلقينا اسمًا إشكاليًا ، سنقع على الفور ، بدلاً من استدعاء طريقتين أخريين ، لكن مازلنا نسقط. علاوة على ذلك ، بدلاً من خطأ من قاعدة بيانات من النوع type type type كبير جدًا ، نكتشف على الفور أنه لا معنى حتى لبدء معالجة هذه الأسماء.
  • يصعب علينا بالفعل خلط الوسائط إذا كان توقيع الوظيفة هو: void UpdateData(Name name, Email email, PhoneNumber number) . بعد كل شيء ، الآن لا نعبر ثلاثة string متطابقة ، ولكن ثلاثة كيانات مختلفة مختلفة.

قليلا عن الصب


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


  • إضافة تطبيق واجهة واجهة النموذج interface IValueGet<TValue>{ TValue Wrapped { get; } } interface IValueGet<TValue>{ TValue Wrapped { get; } } . في هذه الحالة ، في طبقة الترجمة في Sql ، يمكننا الحصول على القيمة مباشرة
  • بدلاً من إنشاء مجموعة من أكثر أو أقل من أنواع متطابقة في التعليمات البرمجية ، يمكنك إنشاء سلف مجردة ، ورث الباقي منه. والنتيجة هي رمز النموذج:

 interface IValueGet<TValue> { TValue Wrapped { get; } } abstract class BaseWrapper : IValueGet<TValue> { protected BaseWrapper(TValue initialValue) { Wrapped = initialValue; } public TValue Wrapped { get; private set; } } sealed class Name : BaseWrapper<string> { public Name(string value) :base(value) { /*no necessary validations*/ } } sealed class UserId : BaseWrapper<int> { public UserId(int id) :base(id) { /*no necessary validations*/ } } 

إنتاجية


عند الحديث عن إنشاء عدد كبير من الأنواع ، يمكنك في كثير من الأحيان مواجهة حجة جدلية:


  • وكلما زاد عدد أنواع التداخل والتشفير والرمز il ، كان البرنامج أبطأ ، لأنه يصعب على البرنامج تحسين البرنامج. لذلك ، سيؤدي هذا النوع من الكتابة الصارمة إلى فرامل خطيرة في المشروع.
  • وكلما زاد عدد الأغلفة ، كلما كان التطبيق يأكل الذاكرة. لذلك ، إضافة مغلفة سيزيد بشكل كبير متطلبات RAM.

بالمعنى الدقيق للكلمة ، غالباً ما يتم تقديم كل الحجج دون حقائق ، ولكن:


  • في الواقع ، في معظم التطبيقات على نفس جافا ، تأخذ السلاسل (ومصفوفات البايت) الذاكرة الرئيسية. بمعنى أن إنشاء الأغلفة من غير المرجح أن يكون ملحوظًا للمستخدم النهائي. ومع ذلك ، نظرًا لهذا النوع من الكتابة ، نحصل على ميزة إضافية مهمة: عند تحليل ملف تفريغ الذاكرة ، يمكنك تقييم المساهمة التي يقدمها كل نوع من أنواعك في الذاكرة. بعد كل شيء ، لا ترى فقط قائمة مجهولة من الخطوط موزعة على المشروع. على العكس ، يمكننا أن نفهم أنواع الكائنات الأكبر. بالإضافة إلى ذلك ، نظرًا لحقيقة أن Wrappers هي التي تحمل السلاسل والكائنات الضخمة الأخرى ، فمن الأسهل بالنسبة لك أن تفهم المساهمة التي يقدمها كل نوع مجمّع معين في الذاكرة المشتركة.
  • الجدل حول تحسين أداء jit صحيح جزئيًا ، لكنه غير كامل تمامًا. في الواقع ، بسبب الكتابة الصارمة ، يبدأ البرنامج في التخلص من العديد من الفحوصات عند مدخل الوظائف. يتم فحص جميع النماذج الخاصة بك للتأكد من كفاية في تصميمها. وبالتالي ، في الحالة العامة ، سيكون لديك عدد أقل من الاختبارات (يكفي أن تطلب النوع الصحيح). بالإضافة إلى ذلك ، نظرًا لحقيقة أن الشيكات يتم نقلها إلى المُنشئ ، وليس لطختها بواسطة الكود ، يصبح من السهل تحديد أيًا منها يستغرق وقتًا.
  • لسوء الحظ ، لا يمكنني في هذا المقال تقديم اختبار أداء كامل ، والذي يقارن المشروع بعدد كبير من الأشكال المجهرية والتطور الكلاسيكي ، باستخدام أنواع int و string و أنواع بدائية أخرى فقط. السبب الرئيسي هو أنه يجب عليك أولاً إجراء مشروع جريء نموذجي للاختبار ، ثم تبرير أن هذا المشروع المعين هو مشروع نموذجي. مع النقطة الثانية ، كل شيء معقد ، لأن المشاريع في الحياة الواقعية مختلفة بالفعل. ومع ذلك ، سيكون من الغريب إجراء اختبارات تركيبية ، لأنه كما قلت سابقًا ، فإن إنشاء كائنات صغيرة في تطبيقات Enterprise ، وفقًا لقياساتي ، يترك دائمًا موارد ضئيلة (على مستوى خطأ القياس).

كيف يمكنك تحسين رمز يتكون من عدد كبير من هذه الأنواع.


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


ومع ذلك ، فإن الحيل نفسها:


  • استخدم أنواع ذات معنى بدلاً من الأنواع المرجعية. قد يكون ذلك مفيدًا إذا كان Wrapper يعمل أيضًا مع أنواع مهمة ، مما يعني أنه من الناحية النظرية يمكنك تمرير جميع المعلومات الضرورية من خلال المجموعة. على الرغم من أنه يجب أن نتذكر أن التسارع لن يكون إلا إذا كان رمزك يعاني حقًا من تكرار GC على وجه التحديد بسبب الأشكال الدقيقة.
    • struct في. صافي يمكن أن يسبب الملاكمة / unboxing متكررة. وفي الوقت نفسه ، قد تتطلب هذه الهياكل مزيدًا من الذاكرة في مجموعات Dictionary / Map (حيث يتم تخصيص المصفوفات بهامش فيها).
    • أنواع inline من Kotlin / Scala لها تطبيق محدود. على سبيل المثال ، لا يمكنك تخزين حقول متعددة فيها (والتي قد تكون مفيدة في بعض الأحيان للتخزين المؤقت لقيمة ToString / GetHashCode ).
    • يستطيع عدد من المحسنين تخصيص الذاكرة على المكدس. على وجه الخصوص ، يقوم .Net بذلك للكائنات المؤقتة الصغيرة ، ويمكن لـ GraalVM في Java تخصيص كائن على المكدس ، ولكن بعد ذلك قم بنسخه إلى الكومة إذا كان يجب إرجاعه (مناسب لرمز غني بالشروط).
  • استخدم فترة اختبار الكائنات (أي ، حاول أن تأخذ كائنات جاهزة الصنع مسبقة الصنع).
    • إذا كان المُنشئ له وسيطة واحدة ، فيمكنك ببساطة إنشاء ذاكرة تخزين مؤقت حيث يكون المفتاح هو هذه الوسيطة ، وتكون القيمة هي الكائن الذي تم إنشاؤه مسبقًا. وبالتالي ، إذا كانت مجموعة الكائنات صغيرة جدًا ، فيمكنك ببساطة إعادة استخدام الكائنات الجاهزة.
    • إذا كان الكائن يحتوي على عدة وسائط ، فيمكنك ببساطة إنشاء كائن جديد ، ثم تحقق لمعرفة ما إذا كان موجودًا في ذاكرة التخزين المؤقت. إذا كان هناك واحد مماثل ، فمن الأفضل إرجاع واحد تم إنشاؤه بالفعل.
    • يعمل مثل هذا المخطط على إبطاء عمل المصممين ، حيث يجب إجراء Equals / GetHashCode لجميع الحجج. ومع ذلك ، فإنه يسرع أيضًا المقارنات المستقبلية للكائنات ، إذا قمت بتخزين قيمة التجزئة مؤقتًا ، لأنه في هذه الحالة ، إذا كانت مختلفة ، فستكون الكائنات مختلفة. والأجسام المتطابقة غالباً ما يكون لها رابط واحد.
    • ومع ذلك ، فإن هذا التحسين سوف يسرع البرنامج ، وذلك بسبب GetHashCode / Equals الأسرع (انظر الفقرة أعلاه). بالإضافة إلى ذلك ، ستنخفض حياة الكائنات الجديدة (الموجودة ، مع ذلك ، في ذاكرة التخزين المؤقت) بشكل كبير ، بحيث ستدخل الجيل 0 فقط.
  • عند إنشاء كائنات جديدة ، تحقق من معلمات الإدخال ، ولا تضبطها. على الرغم من حقيقة أن هذه النصيحة غالبًا ما تدخل في الفقرة الخاصة بأسلوب الترميز ، إلا أنها في الحقيقة تسمح لك بزيادة فعالية البرنامج. على سبيل المثال ، إذا كان الكائن يتطلب سلسلة ذات حروف كبيرة فقط ، فغالبًا ما يتم استخدام ToUpperInvariant للتحقق: إما جعل ToUpperInvariant من الوسيطة ، أو تحقق من أن تكون الأحرف كبيرة. في الحالة الأولى ، يتم ضمان إنشاء سطر جديد ، في الحالة الثانية ، كحد أقصى للتكرار. نتيجة لذلك ، يمكنك حفظ الذاكرة (ومع ذلك ، في كلتا الحالتين ، سيظل كل حرف محددًا ، بحيث سيزداد الأداء فقط في سياق مجموعة البيانات المهملة النادرة).

استنتاج


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


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


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


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


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

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


All Articles