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

هذه المادة سوف تناقش استخدام نمط الأنابيب والمرشحات.


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


وبالتالي ، سنقوم بإنشاء العديد من الحلول ، وتوفير الفرصة لاستخدام أي.
في النهاية ، نقارن بين التطبيقات الأولية والنهائية ، وننظر في أمثلة للتطبيق في المشاريع الحقيقية ونلخصها.


مهمة


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


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


ثم لأول مرة نقوم بإنشاء Modifier ، Modifier فيه التغييرات:


  public class Modifier { public List<> modify(List<> ){ (); return ; } private void (List<> ) { .stream() .filter(::) .forEach(o -> { // }); } } 

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


ولكن مع مرور الوقت ، تظهر متطلبات جديدة وفي كل مرة يتم فيها توسيع وظيفة فئة Modifier :


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

تسلسل التغييرات مهم أيضا. على سبيل المثال ، لا يمكنك أولاً تعليق الملابس على أكتافها ، ثم المكواة.


وبالتالي ، في مرحلة ما ، يمكن أن يأخذ Modifier الشكل التالي:


 public class Modifier { private static final Predicate<> ___ = ((Predicate<>).class::isInstance) .or(.class::isInstance) .or(.class::isInstance) ; public List<> modify(List<> ){ (); (); (); (); //   return ; } private void (List<> ) { .stream() .filter(.class::isInstance) .map(.class::cast) .filter(::) .forEach(o -> { // }); } private void (List<> ) { .stream() .filter(___) .forEach(o -> { //   }); } private void (List<> ) { .removeIf(::); } private void (List<> ) { .stream() .filter(::) .forEach(o -> { // }); } //  } 

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


وعندما يصل متطلب جديد ، بالنظر إلى الكود ، نقرر أن الوقت قد حان لإعادة Refactoring.


إعادة بيع ديون


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


 public class Modifier { private static final Predicate<> ___ = ((Predicate<>).class::isInstance) .or(.class::isInstance) .or(.class::isInstance) ; public List<> modify(List<> ){ List<> result = new ArrayList<>(); for(var o : ){ if(o.()){ continue; } result.add(o); (o); (o); (o); //   } return result; } private void ( ) { if( instanceof ){ // ()  } } private void ( ) { if(___.test()){ //   } } private void ( ) { if(.()){ // } } //  } 

الآن ، يتم تقليل وقت معالجة الملابس ، لكن الكود لا يزال طويلاً للغاية بالنسبة لفئة واحدة ولجسم الدورة. دعونا نحاول تقصير جسم الدورة أولاً.


  • بعد التحقق من النظافة ، يمكنك إجراء جميع المكالمات بطريقة modify( ) منفصلة modify( ) :


     public List<> modify(List<> ){ List<> result = new ArrayList<>(); for(var o : ){ if(o.()){ continue; } result.add(o); modify(o); } return result; } private void modify( o) { (o); (o); (o); //   } 

  • يمكنك دمج جميع المكالمات في Consumer واحد:


     private Consumer<> modification = ((Consumer<>) this::) .andThen(this::) .andThen(this::); //   public List<> modify(List<> ){ return .stream() .filter(o -> !o.()) .peek(modification) .collect(Collectors.toList()); } 

    بلانت: نظرة خاطفة
    كنت نظرة خاطفة لفترة قصيرة. سيقول سونار أن مثل هذا الرمز لا ينبغي أن يتم ، لأنه يخبر Javadoc نظرة خاطفة أن الطريقة موجودة بشكل أساسي لتصحيح الأخطاء. ولكن إذا قمت بإعادة كتابتها على الخريطة: .map (o -> {modification.accept (o)؛ return o؛}) ، فستقول IDEA أنه من الأفضل استخدام النظرة الخاطفة


عثرة: المستهلك
يتم إعطاء مثال مع المستهلك (واتباعًا للوظيفة) لإظهار قدرات اللغة.

الآن أصبح نص الدورة أقصر ، لكن حتى الآن ما زالت الفئة نفسها كبيرة جدًا وتحتوي على الكثير من المعلومات (معرفة بجميع الخطوات).


دعنا نحاول حل هذه المشكلة باستخدام أنماط البرمجة القائمة بالفعل. في هذه الحالة ، سوف نستخدم Pipes & Filters .


الأنابيب والمرشحات


يصف قالب القناة وعامل التصفية الطريقة التي تمر بها البيانات الواردة عبر العديد من خطوات المعالجة.


دعونا نحاول تطبيق هذا النهج على الكود الخاص بنا.


الخطوة 1


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


سننقل الآن كل خطوة إلى فصل منفصل ونرى ما نحصل عليه:


 public class Modifier { private final  ; private final  ; private final  ; //  public Modifier( ,  ,   //  ) { this. = ; this. = ; this. = ; //  } public List<> modify(List<> ) { return .stream() .filter(o -> !o.()) .peek(o -> { .(o); .(o); .(o); //  }) .collect(Collectors.toList()); } } 

وبالتالي ، وضعنا الكود في فصول منفصلة ، لتبسيط اختبارات التحولات الفردية (وخلق إمكانية إعادة استخدام الخطوات). يحدد ترتيب المكالمات تسلسل الخطوات.


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


الخطوة 2


تبسيط الكود باستخدام Spring.
أولاً ، قم بإنشاء واجهة لكل خطوة فردية:


 interface Modification { void modify( ); } 

سيكون Modifier نفسه الآن أقصر بكثير:


 public class Modifier { private final List<Modification> steps; @Autowired public Modifier(List<Modification> steps) { this.steps = steps; } public List<> modify(List<> ) { return .stream() .filter(o -> !o.()) .peek(o -> { steps.forEach(m -> m.modify(o)); }) .collect(Collectors.toList()); } } 

الآن ، لإضافة خطوة جديدة ، تحتاج فقط إلى كتابة فئة جديدة تنفذ واجهة Modification وتضع @Component فوقه. سوف يجدها الربيع ويضيفها إلى القائمة.


Modifer نفسه Modifer يعرف أي شيء عن الخطوات الفردية ، مما يخلق "اتصال ضعيف" بين المكونات.


الصعوبة الوحيدة هي ضبط التسلسل. للقيام بذلك ، يحتوي Spring على تعليق توضيحي @Order يمكنك من خلاله تمرير قيمة int. يتم فرز القائمة بترتيب تصاعدي.
وبالتالي ، قد يحدث أنه بإضافة خطوة جديدة في منتصف القائمة ، سيتعين عليك تغيير قيم الفرز للخطوات الحالية.


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

الخطوة 3


الآن نجتاز اختبار النظافة في خطوة منفصلة. للقيام بذلك ، نعيد كتابة الواجهة الخاصة بنا حتى تُرجع دائمًا قيمة:


 interface Modification {  modify( ); } 

تحقق من النظافة:


 @Component @Order(Ordered.HIGHEST_PRECEDENCE) class CleanFilter implements Modification {  modify( ) { if(.()){ return null; } return ; } } 

Modifier.modify :


  public List<> modify(List<> ) { return .stream() .map(o -> { var modified = o; for(var step : steps){ modified = step.modify(o); if(modified == null){ return null; } } return modified; }) .filter(Objects::nonNull) .collect(Collectors.toList()); } 

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


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


يتم استخدام مبدأ مماثل في Spring لـ HandlerInterceptors. قبل وبعد استدعاء وحدة التحكم ، يتم استدعاء جميع المعترضات المناسبة لعنوان URL هذا. في الوقت نفسه ، تقوم بإرجاع صواب أو خطأ في طريقة preHandle للإشارة إلى ما إذا كان يمكن معالجة واستدعاء الاعتراضات اللاحقة


الخطوة ن


والخطوة التالية هي إضافة طريقة matches إلى واجهة Modification ، والتي يتم فيها فحص الخطوات إلى سمة منفصلة من الملابس:


 interface Modification {  modify( ); default matches( ) {return true;} } 

لهذا السبب ، يمكنك تبسيط المنطق قليلاً في modify الطرق عن طريق نقل عمليات التحقق من الفئات والخصائص إلى طريقة منفصلة.


يتم استخدام أسلوب مشابه في عامل تصفية Spring (Request) ، ولكن الفرق الرئيسي هو أن كل Filter عبارة عن مجمّع حول التالي ويدعو FilterChain.doFilter بشكل صريح لمتابعة المعالجة.


في المجموع


النتيجة النهائية مختلفة تمامًا عن الإصدار الأولي. عند مقارنتها ، يمكننا استخلاص النتائج التالية:


  • يعمل التنفيذ على أساس Pipes & Filters على تبسيط فئة Modifier نفسها.
  • توزيع المسؤوليات بشكل أفضل والصلات "الضعيفة" بين المكونات.
  • أسهل لاختبار الخطوات الفردية.
  • أسهل لإضافة وإزالة الخطوات.
  • أصعب قليلا لاختبار سلسلة كاملة من المرشحات. نحن بحاجة إلى IntegrationTests بالفعل.
  • المزيد من الفصول

في نهاية المطاف ، خيار أكثر ملاءمة ومرونة من الأصلي.


بالإضافة إلى ذلك ، يمكنك ببساطة موازاة معالجة البيانات باستخدام نفس parallelStream.


ما هذا المثال لا يحل


  1. يوضح وصف النموذج أن المرشحات الفردية يمكن إعادة استخدامها من خلال إنشاء سلسلة مرشحات أخرى (قناة).
    • من ناحية ، من السهل القيام بذلك باستخدام @Qualifier .
    • من ناحية أخرى ، سيفشل وضع ترتيب مختلف باستخدام @Order .
  2. للحصول على أمثلة أكثر تعقيدًا ، سيتعين عليك استخدام سلاسل متعددة ، واستخدام سلاسل متداخلة ، وتغيير التطبيق الحالي.
    • على سبيل المثال ، المهمة: "لكل جورب ، ابحث عن زوج ووضعه في مثيل واحد من <؟ Extends Clothing>" لن يتناسب بشكل جيد مع التنفيذ الموصوف ، لأن الآن ، لكل اصبع قدم ، عليك الفرز من خلال كل الكتان وتغيير قائمة البيانات الأولية.
    • لحل هذه المشكلة ، يمكنك كتابة واجهة جديدة تقبل وترجع قائمة <Clothing> وتحولها إلى سلسلة جديدة. ولكن عليك أن تكون حذراً مع تسلسل مكالمات السلاسل نفسها ، إذا كان من الممكن خياطة الجوارب فقط عن طريق الفندق.

شكرا لاهتمامكم

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


All Articles