بدء الربيع StateMachine

دخول


في المشاريع التي قابلتها ثلاثة أمثلة ، بطريقة أو بأخرى مرتبطة بنظرية الأتمتة المحدودة

  • مثال 1. رمز govnokod مسلية . يستغرق الكثير من الوقت لفهم ما يحدث. السمة المميزة لتجسيد النظرية المشار إليها في الكود هي تفريغ شديد إلى حد ما يشبه في بعض الأحيان رمزًا إجرائيًا. حقيقة أن هذا الإصدار من الكود هو الأفضل عدم التطرق إلى المشروع يعرف كل تقني ومنهجي ومتخصص في المنتج. يذهبون إلى هذا الكود لإصلاح شيء ما في حالة الطوارئ (عندما يكون مكسورًا تمامًا) ، لا يوجد أي شك في إنهاء أي ميزات. لأنه مخيف لكسر. الميزة الثانية المذهلة التي تعزل هذا النوع هي وجود مفاتيح قوية كهذه ، بملء الشاشة.
    هناك حتى نكتة في هذه النتيجة:
    الحجم الأمثل
    في أحد JPoint ، تحدث أحد المتحدثين ، ربما نيكولاي أليمنكوف ، عن عدد الحالات الموجودة في المحول العادي ، وقال إن الإجابة العليا "تناسب حتى الآن الشاشة". وفقًا لذلك ، إذا كنت تتداخل ولم يكن رمز التبديل لديك طبيعيًا بالفعل ، فقم بتقليل حجم الخط في IDE وتقليله
  • مثال 2. حالة النمط . الفكرة الرئيسية (بالنسبة لأولئك الذين لا يرغبون في متابعة الروابط) هي أننا نقسم مهمة تجارية معينة إلى مجموعة من الحالات النهائية ونصفها بالشفرة.
    العيب الرئيسي لنمط الحالة هو أن الولايات تعرف عن بعضها البعض ، وهم يعرفون أن هناك إخوة ويدعون بعضهم البعض. مثل هذا الرمز من الصعب جدا جعل عالمية. على سبيل المثال ، عند تطبيق نظام للدفع به عدة أنواع من المدفوعات ، فإنك تخاطر بالبحث في Generic-s لدرجة أن إعلان أساليبك قد يصبح مثل هذا:

    private < T extends BaseContextPayment, Q extends BaseDomainPaymentRequest, S, B extends AbstractPaymentDetailBuilder<T, Q, S, B>, F extends AbstractPaymentBuilder<T, Q, S, B> > PaymentContext<T, S> build(final Q request, final Class<F> factoryClass){ //""  } 

    تلخيص الحالة: يمكن أن يؤدي التنفيذ إلى رمز معقد إلى حد ما.
  • مثال 3 StateMachine الفكرة الأساسية للنمط هي أن الدول لا تعرف شيئًا عن بعضها البعض ، ويتم التحكم في الانتقال من خلال السياق ، إنه أفضل وأقل اتصالًا - الرمز أكثر بساطة.

بعد تجربة كل "القوة" من النوع الأول وتعقيد النوع الثاني ، قررنا استخدام Pattern StateMachine لحالة العمل الجديدة.
من أجل عدم إعادة اختراع العجلة ، تقرر اتخاذ Statemachine Spring كأساس (هذا الربيع).

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

الجسم الرئيسي


سننشئ تطبيق Spring Boot ، ونضيف كاتب ويب (نحصل على تشغيل تطبيق الويب بأسرع ما يمكن) ، وسيكون التطبيق بمثابة تجريد من عملية الشراء. سوف يمر المنتج عند الشراء بمراحل التراجع الجديد والمحجوز والمحجوز والشراء الكامل.
القليل من الارتجال ، سيكون هناك المزيد من الحالات في مشروع حقيقي ، لكن حسنًا ، لدينا أيضًا مشروع حقيقي جدًا.
في pom.xml لتطبيق الويب المخبوز حديثًا ، أضف التبعية على الجهاز وفي اختباراته (يجب أن يكون Web Starter بالفعل ، إذا تم جمعه عبر start.spring.io ):
 <dependency> <groupId>org.springframework.statemachine</groupId> <artifactId>spring-statemachine-core</artifactId> <version>2.1.3.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.statemachine</groupId> <artifactId>spring-statemachine-test</artifactId> <version>2.1.3.RELEASE</version> <scope>test</scope> </dependency> <cut /> 

إنشاء الهيكل:

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

لذلك دعونا نذهب.
لدينا مشروع نظيف مع التبعيات اللازمة ، لبداية سنقوم بإنشاء التعداد ، مع الدول والأحداث ، التجريد بسيطة إلى حد ما ، هذه المكونات نفسها لا تحمل أي منطق.
 public enum PurchaseEvent { RESERVE, BUY, RESERVE_DECLINE } 

 public enum PurchaseState { NEW, RESERVED, CANCEL_RESERVED, PURCHASE_COMPLETE } 

على الرغم من أنه رسميًا ، يمكنك إضافة حقول إلى هذه التعدادات ، وربطها بشيء مميز فيها ، على سبيل المثال ، حالة معينة ، وهو أمر منطقي تمامًا (لقد فعلنا ذلك عن طريق حل قضيتنا بسهولة تامة).
سنقوم بتهيئة الجهاز من خلال تكوين java ، وإنشاء ملف التكوين ، وبالنسبة للفئة الممتدة EnumStateMachineConfigurerAdapter <PurchaseState، PurchaseEvent>. نظرًا لأن حالتنا وأحداثنا تعداد ، فإن الواجهة مناسبة ، ولكن ليس من الضروري ، يمكن استخدام أي نوع من الكائنات بشكل عام (لن نأخذ في الاعتبار أمثلة أخرى في المقالة ، نظرًا لأن EnumStateMachineConfigurerAdapter أكثر من كافٍ في رأيي).

النقطة المهمة التالية هي ما إذا كان جهاز واحد سيعيش في سياق التطبيق: في نسخة واحدة منEnableStateMachine ، أو في كل مرة سيتم إنشاءEnableStateMachineFactory جديد. إذا كان هذا هو تطبيق ويب متعدد المستخدمين مع مجموعة من المستخدمين ، فإن الخيار الأول بالكاد مناسب لك ، لذلك سنستخدم الخيار الثاني كخيار أكثر شعبية. يمكن أيضًا إنشاء StateMachine عبر المنشئ كحبة عادية (راجع الوثائق) ، وهو مناسب في بعض الحالات (على سبيل المثال ، تحتاج إلى إعلان الجهاز صراحةً باعتباره حبة) ، وإذا كانت حبة منفصلة ، فيمكننا تحديد نطاقنا على سبيل المثال الجلسة أو الطلب. في مشروعنا ، تم تطبيق المجمع (ميزات منطق عملنا) على حبة statemachine ، و المجمع كان singleton ، و آلة النموذج الأولي نفسها
أشعل النار
كيفية تنفيذ النموذج الأولي في singlton؟
في الواقع ، كل ما عليك فعله هو الحصول على فاصوليا جديدة من التطبيقContext في كل مرة تدخل فيها إلى الكائن. إنها خطيئة لحقن applicationContext في منطق العمل ، لذلك ، يجب أن يكون برنامج إعداد الفاصوليا إما واجهة مع طريقة واحدة على الأقل ، أو طريقة مجردة (حقن الأسلوب) ، عند إنشاء تكوين جافا ، سوف تحتاج إلى تنفيذ طريقة مجردة المشار إليها ، وفي التطبيق سنقوم من applicationContext فاصوليا جديدة. من الممارسات المعتادة أن يكون لديك رابط إلى applicationContext في فئة التكوين ، ومن خلال الطريقة المجردة ، سوف نسمي .getBean ()؛

فئة EnumStateMachineConfigurerAdapter لديها العديد من الطرق ، التي تجاوزنا نحن تكوين الجهاز.
للبدء ، سجل الحالات:
  @Override public void configure(final StateMachineStateConfigurer<PurchaseState, PurchaseEvent> states) throws Exception { states .withStates() .initial(NEW) .end(PURCHASE_COMPLETE) .states(EnumSet.allOf(PurchaseState.class)); } 

الأولي (NEW) هي الحالة التي ستكون فيها الآلة بعد إنشاء الحبة ، النهاية (PURCHASE_COMPLETE) هي الحالة عن طريق الانتقال إلى حيث سيقوم الجهاز بتنفيذ statemachine.stop () ، بالنسبة للجهاز غير المحدد (معظمه) غير ذي صلة ، ولكن يجب تحديد شيء ما . .states (EnumSet.allOf (PurchaseState.class) قائمة بجميع الحالات ، يمكنك الدفع بكميات كبيرة.

تكوين إعدادات الجهاز العالمية
  @Override public void configure(final StateMachineConfigurationConfigurer<PurchaseState, PurchaseEvent> config) throws Exception { config .withConfiguration() .autoStartup(true) .listener(new PurchaseStateMachineApplicationListener()); } 

يحدد التشغيل التلقائي هنا ما إذا كان سيتم تشغيل الجهاز مباشرة بعد الإنشاء افتراضيًا ، بمعنى آخر - ما إذا كان سيتم التبديل تلقائيًا إلى الحالة الجديدة (خطأ افتراضياً). على الفور ، نسجل مستمعًا لسياق الجهاز (حوله لاحقًا بقليل) ، في نفس التكوين ، يمكنك تعيين TaskExecutor منفصل ، وهو مناسب عند تنفيذ إجراء طويل على بعض انتقالاتهم ، ويجب أن يذهب التطبيق إلى أبعد من ذلك.
حسنا ، التحولات نفسها:
  @Override public void configure(final StateMachineTransitionConfigurer<PurchaseState, PurchaseEvent> transitions) throws Exception { transitions .withExternal() .source(NEW) .target(RESERVED) .event(RESERVE) .action(reservedAction(), errorAction()) .and() .withExternal() .source(RESERVED) .target(CANCEL_RESERVED) .event(RESERVE_DECLINE) .action(cancelAction(), errorAction()) .and() .withExternal() .source(RESERVED) .target(PURCHASE_COMPLETE) .event(BUY) .guard(hideGuard()) .action(buyAction(), errorAction()); } 

يتم تعيين كل منطق التحولات أو التحولات هنا ، يمكن تعليق الحرس على التحولات ، وهو المكون الذي يعود دائمًا منطقيًا ، ما الذي ستقوم بالتحقق منه بالتحقق من الانتقال من حالة إلى أخرى وفقًا لتقديرك ، أي منطق يمكن أن يكون مثاليًا في الحرس ، وهذا مكون عادي تمامًا لكنه يجب أن يعود منطقية. في إطار مشروعنا ، على سبيل المثال ، يمكن لـ HideGuard التحقق من إعداد معين يمكن للمستخدم تعيينه (لا تعرض هذا المنتج) ، ووفقًا لذلك ، لا تدع الجهاز في الحالة المحمية بواسطة Guard. ألاحظ أن Guard ، يمكن إضافة واحد فقط إلى انتقال واحد في التكوين ، لن يعمل مثل هذا التصميم:
  .withExternal() .source(RESERVED) .target(PURCHASE_COMPLETE) .event(BUY) .guard(hideGuard()) .guard(veryHideGuard()) 

بتعبير أدق ستعمل ، لكن الحارس الأول فقط (hideGuard ())
ولكن يمكنك إضافة العديد من الإجراءات (الآن نحن نتحدث عن الإجراء ، الذي نصفه في تكوين الانتقالات) ، وأنا شخصياً حاولت إضافة ثلاثة إجراءات إلى عملية نقل واحدة.
  .withExternal() .source(NEW) .target(RESERVED) .event(RESERVE) .action(reservedAction(), errorAction()) 

الوسيطة الثانية هي ErrorAction ، وسيحصل عليها التحكم إذا ألقيت ReservedAction استثناء (رمي ).
أشعل النار
ضع في اعتبارك أنه في الإجراء الخاص بك لا تزال تتعامل مع الخطأ من خلال try / catch ، فلن تدخل في ErrorAction ، وإذا كنت بحاجة إلى معالجة وتذهب إلى ErrorAction ، فعليك رمي RuntimeException () من catch ، على سبيل المثال (أنت نفسك قلت أنه ضروري جدا).

بالإضافة إلى "تعليق" الإجراء في التحولات ، يمكنك أيضًا "تعليق" في طريقة التكوين للحالة ، في النموذج التالي تقريبًا:
  @Override public void configure(final StateMachineStateConfigurer<PurchaseState, PurchaseEvent> states) throws Exception { states .withStates() .initial(NEW) .end(PURCHASE_COMPLETE) .stateEntry() .stateExit() .state() .states(EnumSet.allOf(PurchaseState.class)); } 

كل هذا يتوقف على الطريقة التي تريد بها تشغيل الإجراء.
أشعل النار
لاحظ أنه إذا قمت بتحديد إجراء عند تكوين الحالة () ، مثل ذلك
  states .withStates() .initial(NEW) .end(PURCHASE_COMPLETE) .state(randomAction()) 

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

الإصدار النهائي من التكوين سيبدو كما يلي:
 @Configuration @EnableStateMachineFactory public class StateMachineConfig extends EnumStateMachineConfigurerAdapter<PurchaseState, PurchaseEvent> { @Override public void configure(final StateMachineConfigurationConfigurer<PurchaseState, PurchaseEvent> config) throws Exception { config .withConfiguration() .autoStartup(true) .listener(new PurchaseStateMachineApplicationListener()); } @Override public void configure(final StateMachineStateConfigurer<PurchaseState, PurchaseEvent> states) throws Exception { states .withStates() .initial(NEW) .end(PURCHASE_COMPLETE) .stateEntry() .stateExit() .state() .states(EnumSet.allOf(PurchaseState.class)); } @Override public void configure(final StateMachineTransitionConfigurer<PurchaseState, PurchaseEvent> transitions) throws Exception { transitions .withExternal() .source(NEW) .target(RESERVED) .event(RESERVE) .action(reservedAction(), errorAction()) .and() .withExternal() .source(RESERVED) .target(CANCEL_RESERVED) .event(RESERVE_DECLINE) .action(cancelAction(), errorAction()) .and() .withExternal() .source(RESERVED) .target(PURCHASE_COMPLETE) .event(BUY) .guard(hideGuard()) .action(buyAction(), errorAction()); } @Bean public Action<PurchaseState, PurchaseEvent> reservedAction() { return new ReservedAction(); } @Bean public Action<PurchaseState, PurchaseEvent> cancelAction() { return new CancelAction(); } @Bean public Action<PurchaseState, PurchaseEvent> buyAction() { return new BuyAction(); } @Bean public Action<PurchaseState, PurchaseEvent> errorAction() { return new ErrorAction(); } @Bean public Guard<PurchaseState, PurchaseEvent> hideGuard() { return new HideGuard(); } @Bean public StateMachinePersister<PurchaseState, PurchaseEvent, String> persister() { return new DefaultStateMachinePersister<>(new PurchaseStateMachinePersister()); } 

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

لنجعل تحكم:
 @RestController @SuppressWarnings("unused") public class PurchaseController { private final PurchaseService purchaseService; public PurchaseController(PurchaseService purchaseService) { this.purchaseService = purchaseService; } @RequestMapping(path = "/reserve") public boolean reserve(final String userId, final String productId) { return purchaseService.reserved(userId, productId); } @RequestMapping(path = "/cancel") public boolean cancelReserve(final String userId) { return purchaseService.cancelReserve(userId); } @RequestMapping(path = "/buy") public boolean buyReserve(final String userId) { return purchaseService.buy(userId); } } 


واجهة الخدمة
 public interface PurchaseService { /** *    ,          * * @param userId id ,    ,      id  *    http- * @param productId id ,     * @return /  ,             *      . */ boolean reserved(String userId, String productId); /** *   /    * * @param userId id ,    ,      id  *    http- * @return /  ,             *      . */ boolean cancelReserve(String userId); /** *     * * @param userId id ,    ,      id  *    http- * @return /  ,             *      . */ boolean buy(String userId); } 

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

النقطة المهمة التالية هي كيف ستستعيد حالة جهازك ، لأنه لكل مكالمة سيتم إنشاء فاصوليا جديدة لا تعرف أي شيء عن حالاتك السابقة للجهاز وسياقه.
لهذه الأغراض ، فإن statemachine spring لديه آلية Persistens:
 public class PurchaseStateMachinePersister implements StateMachinePersist<PurchaseState, PurchaseEvent, String> { private final HashMap<String, StateMachineContext<PurchaseState, PurchaseEvent>> contexts = new HashMap<>(); @Override public void write(final StateMachineContext<PurchaseState, PurchaseEvent> context, String contextObj) { contexts.put(contextObj, context); } @Override public StateMachineContext<PurchaseState, PurchaseEvent> read(final String contextObj) { return contexts.get(contextObj); } } 

من أجل تطبيقنا الساذج ، نستخدم الخريطة المعتادة كمتجر حكومي ، في تطبيق غير ساذج سيكون نوعًا من قاعدة البيانات ، مع الانتباه إلى النوع العام الثالث String ، هذا هو المفتاح الذي سيتم حفظ حالة جهازك به ، مع كل الحالات ، المتغيرات في السياق ، المعرف و هكذا. في المثال الخاص بي ، استخدمت معرف المستخدم لمفتاح الحفظ ، والذي يمكن أن يكون أي مفتاح (مستخدم session_id ، تسجيل دخول فريد ، إلخ).
أشعل النار
في مشروعنا ، لم تكن آلية حفظ واستعادة الحالات من الصندوق مناسبة لنا ، حيث قمنا بتخزين أوضاع الجهاز في قاعدة البيانات ويمكن تغييرها بواسطة وظيفة لا تعرف شيئًا عن الماكينة.
اضطررت إلى تثبيت الحالة التي تم تلقيها من قاعدة البيانات ، وإجراء بعض الخطوات الأولية التي ، عند بدء تشغيل الجهاز ، استلمت الحالة من قاعدة البيانات ، وقمت بتعيينها بالقوة ، وعندها فقط ألقيت الحدث ، رمز مثال يفعل ما يلي:
 stateMachine .getStateMachineAccessor() .doWithAllRegions(access -> { access.resetStateMachine(new DefaultStateMachineContext<>({ResetState}, null, null, null, null)); }); stateMachine.start(); stateMachine.sendEvent({NewEventFromResetState}); 


سننظر في تنفيذ الخدمة في كل طريقة:
  @Override public boolean reserved(final String userId, final String productId) { final StateMachine<PurchaseState, PurchaseEvent> stateMachine = stateMachineFactory.getStateMachine(); stateMachine.getExtendedState().getVariables().put("PRODUCT_ID", productId); stateMachine.sendEvent(RESERVE); try { persister.persist(stateMachine, userId); } catch (final Exception e) { e.printStackTrace(); return false; } return true; } 

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

تتشابه طريقتان المتبقيتان:
  @Override public boolean cancelReserve(final String userId) { final StateMachine<PurchaseState, PurchaseEvent> stateMachine = stateMachineFactory.getStateMachine(); try { persister.restore(stateMachine, userId); stateMachine.sendEvent(RESERVE_DECLINE); } catch (Exception e) { e.printStackTrace(); return false; } return true; } @Override public boolean buy(final String userId) { final StateMachine<PurchaseState, PurchaseEvent> stateMachine = stateMachineFactory.getStateMachine(); try { persister.restore(stateMachine, userId); stateMachine.sendEvent(BUY); } catch (Exception e) { e.printStackTrace(); return false; } return true; } 

سنقوم أولاً باستعادة حالة الجهاز للمستخدم userId لمستخدم معين ، ثم نلقي حدثًا يتوافق مع طريقة api.
لاحظ أن productId لا يظهر بالفعل في الطريقة ، لقد أضفناه إلى سياق الجهاز وسنحصل عليه بعد استعادة الجهاز من النسخة الاحتياطية.
في تنفيذ الإجراء ، سوف نحصل على معرف المنتج من سياق الجهاز وسنعرض رسالة تقابل الانتقال في السجل ، على سبيل المثال ، سأقدم الكود المحجوز:
 public class ReservedAction implements Action<PurchaseState, PurchaseEvent> { @Override public void execute(StateContext<PurchaseState, PurchaseEvent> context) { final String productId = context.getExtendedState().get("PRODUCT_ID", String.class); System.out.println("   " + productId + " ."); } } 

لا يسعنا إلا أن نذكر المستمع ، الذي يقدم خارج الصندوق عددًا لا بأس به من النصوص التي يمكنك تعليقها ، انظر لنفسك:
 public class PurchaseStateMachineApplicationListener implements StateMachineListener<PurchaseState, PurchaseEvent> { @Override public void stateChanged(State<PurchaseState, PurchaseEvent> from, State<PurchaseState, PurchaseEvent> to) { if (from.getId() != null) { System.out.println("   " + from.getId() + "   " + to.getId()); } } @Override public void stateEntered(State<PurchaseState, PurchaseEvent> state) { } @Override public void stateExited(State<PurchaseState, PurchaseEvent> state) { } @Override public void eventNotAccepted(Message<PurchaseEvent> event) { System.out.println("   " + event); } @Override public void transition(Transition<PurchaseState, PurchaseEvent> transition) { } @Override public void transitionStarted(Transition<PurchaseState, PurchaseEvent> transition) { } @Override public void transitionEnded(Transition<PurchaseState, PurchaseEvent> transition) { } @Override public void stateMachineStarted(StateMachine<PurchaseState, PurchaseEvent> stateMachine) { System.out.println("Machine started"); } @Override public void stateMachineStopped(StateMachine<PurchaseState, PurchaseEvent> stateMachine) { } @Override public void stateMachineError(StateMachine<PurchaseState, PurchaseEvent> stateMachine, Exception exception) { } @Override public void extendedStateChanged(Object key, Object value) { } @Override public void stateContext(StateContext<PurchaseState, PurchaseEvent> stateContext) { } } 

المشكلة الوحيدة هي أن هذه واجهة ، مما يعني أنك بحاجة إلى تنفيذ جميع هذه الطرق ، لكن بما أنك لا تحتاجها جميعًا ، فسيتم تعليق بعضها فارغًا ، وستقول التغطية إن هذه الطرق لا تشملها الاختبارات.
هنا في أداة التثبيت ، يمكننا تعليق أي مقاييس في أحداث مختلفة تمامًا عن الجهاز (على سبيل المثال ، لا تتم عمليات الدفع ، وغالبًا ما ينتقل الجهاز إلى نوع من حالة PAYMENT_FAIL ، نستمع إلى التحولات ، وإذا كانت الآلة في حالة خاطئة - نكتب ، في سجل فردي ، أو قاعدة أو استدعاء الشرطة ، أيا كان).
أشعل النار
توجد حالة حدثآلة في lisener-e ، ولكن مع فارق بسيط ، عندما يكون لديك استثناء وتتعامل معه بشكل مباشر ، الجهاز لا يعتبر أن هناك خطأ ، تحتاج إلى التحدث بوضوح في الصيد
stateMachine.setStateMachineError (استثناء) وتمرير خطأ.

كتحقق مما فعلناه ، سنقوم بتنفيذ حالتين:
  • 1. الحجز والرفض اللاحق لعملية الشراء. سوف نرسل الطلب إلى URI "/ reserve" ، مع المعلمات userId = 007 ، productId = 10001 ، وبعد ذلك يكون الطلب "/ Cancel" مع المعلمة userId = 007 ، سيكون إخراج وحدة التحكم كما يلي:
    Machine started
    10001 .
    NEW RESERVED
    Machine started
    10001
    RESERVED CANCEL_RESERVED
  • 2. الحجز والشراء الناجح:
    Machine started
    10001 .
    NEW RESERVED
    Machine started
    10001
    RESERVED PURCHASE_COMPLETE

استنتاج


في الختام ، سأقدم مثالًا على اختبار الإطار ، وأعتقد أن كل شيء سيصبح واضحًا من الشفرة ، وستحتاج فقط إلى الاعتماد على جهاز الاختبار ، ويمكنك التحقق من التكوين بشكل مُعلن.
  @Test public void testWhenReservedCancel() throws Exception { StateMachine<PurchaseState, PurchaseEvent> machine = factory.getStateMachine(); StateMachineTestPlan<PurchaseState, PurchaseEvent> plan = StateMachineTestPlanBuilder.<PurchaseState, PurchaseEvent>builder() .defaultAwaitTime(2) .stateMachine(machine) .step() .expectStates(NEW) .expectStateChanged(0) .and() .step() .sendEvent(RESERVE) .expectState(RESERVED) .expectStateChanged(1) .and() .step() .sendEvent(RESERVE_DECLINE) .expectState(CANCEL_RESERVED) .expectStateChanged(1) .and() .build(); plan.test(); } @Test public void testWhenPurchaseComplete() throws Exception { StateMachine<PurchaseState, PurchaseEvent> machine = factory.getStateMachine(); StateMachineTestPlan<PurchaseState, PurchaseEvent> plan = StateMachineTestPlanBuilder.<PurchaseState, PurchaseEvent>builder() .defaultAwaitTime(2) .stateMachine(machine) .step() .expectStates(NEW) .expectStateChanged(0) .and() .step() .sendEvent(RESERVE) .expectState(RESERVED) .expectStateChanged(1) .and() .step() .sendEvent(BUY) .expectState(PURCHASE_COMPLETE) .expectStateChanged(1) .and() .build(); plan.test(); } 

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

PS


تعمل سيارتنا على أساس منتج ، وحتى الآن لم نواجه أي مشاكل تشغيلية ، فهناك ميزة يمكن أن نستخدم فيها الغالبية العظمى من مكونات الجهاز الحالي عند تطبيق واحدة جديدة (Guard و Some Actions مثالية فقط)

تعليق


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

مراجع


مصادر دوكا

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


All Articles