Skip to content

8. RxJava في بيئة Swing

8.1. Introduction

سنعود هنا إلى تطبيق Swing الذي تم عرضه في الفقرة 2.

  

للعمل مع RxJava في بيئة Swing، سنستخدم مكتبة RxSwing التي تضيف إلى RxJava فئات وواجهات مفيدة في بيئة Swing. ولهذا الغرض، فإن ملف Gradle الخاص بمثال Swing هو كما يلي:

  

buildscript {
    repositories {
        mavenCentral()
    }
}
apply plugin: 'java'
jar {
    baseName = 'exemples-01'
    version = '0.0.1-SNAPSHOT'
}
repositories {
    mavenCentral()
}
dependencies {
    compile('io.reactivex:rxswing:0.25.0')
    compile('io.reactivex:rxjava:1.1.3')
    compile('com.fasterxml.jackson.core:jackson-databind:2.7.3')
}
task wrapper(type: Wrapper) {
    gradleVersion = '2.9'
}
  • السطر 15: التبعية لـ RxSwing؛

لن نستخدم سوى كائن واحد خاص بـ RxSwing: المجدول [SwingScheduler.getInstance()] الذي يقوم بتنفيذ/مراقبة العناصر القابلة للمراقبة على مؤشر ترابط حلقة الأحداث في Swing. وسنستخدمه حصريًّا لمراقبة العناصر القابلة للمراقبة التي تُنفَّذ على خيوط أخرى غير خيط حلقة الأحداث. ولنتذكر بنية التطبيق المثال:

Image

  • تحتوي طبقة الخدمة غير المتزامنة على طرق تُرجع قيمًا قابلة للمراقبة. نقوم بتنفيذ هذه القيم القابلة للمراقبة في خيوط مختلفة عن خيط حلقة الأحداث. وبالتالي، لا تكون واجهة المستخدم الرسومية جامدة. بل يمكنها الاستجابة لطلبات المستخدم. وأبرز هذه الاستجابات هو السماح للمستخدم بالنقر على زر [Annuler] لإيقاف عملية غير متزامنة تستغرق وقتًا طويلاً. ولكي يتمكن من القيام بذلك، يجب ألا تكون الواجهة الرسومية متجمدة (frozen
  • تسعى طبقة Swing إلى استغلال النتائج التي ترسلها العمليات غير المتزامنة واستخدامها لتحديث واجهة المستخدم الرسومية. لكن هذا لا يمكن أن يتم إلا في مؤشر ترابط حلقة الأحداث. ولتحقيق ذلك، يتم مراقبة هذه النتائج في المجدول [SwingScheduler.getInstance()]؛

وبالتالي، في كود إدارة أحداث الواجهة الرسومية، يتم التفاعل مع الطبقة غير المتزامنة [rxService] بالشكل التالي:


Observable obs=rxService.doSomething(...).subscribeOn(Schedulers.computation()).observeOn(SwingScheduler.getInstance()) ;

حيث يمكن استبدال المجدول [Schedulers.computation()] بمجدول آخر وفقًا لحالات الاستخدام.

يُرجى من القارئ إعادة قراءة الفقرة 2. فقد أصبح لديه الآن المعرفة الكافية لفهمها تمامًا.

8.2. هيكل الكود

يُنفذ الرمز البنية التالية:

Image

مشروع IntelliJ IDEA الذي ينفذ هذه البنية هو التالي:

  
  • تنفذ الحزمة [rxswing.service] طبقات الخدمة المتزامنة (IService، Service) وغير المتزامنة (IRxService، RxService
  • تقوم الحزمة [rxswing.ui] بتنفيذ واجهة Swing؛

8.3. تشغيل المشروع

لتشغيل المشروع في IntelliJ IDEA، اتبع الخطوات التالية:

 

8.4. الخدمة المتزامنة

Image

  

تقدم طبقة الخدمة المتزامنة الواجهة التالية: [IService]:


package dvp.rxswing.service;

public interface IService {
  // أرقام عشوائية في النطاق [a,b]
  // يتم توليد n عددًا من الأرقام، حيث n نفسه رقم عشوائي في النطاق [minCount, maxCount]
  // يتم توليد الأعداد بعد انتظار مدته delay مللي ثانية،
  // حيث [delay] هو نفسه عدد عشوائي في النطاق [minDelay, maxDelay]
  public ServiceResponse getAleas(int a, int b, int minCount, int maxCount, int minDelay, int maxDelay);
}

نوع استجابة الخدمة [ServiceResponse] هو كما يلي:


package dvp.rxswing.service;

import java.util.List;

public class ServiceResponse {

  // فترة انتظار الخدمة
  private int delay;
  // أرقام عشوائية
  private List<Integer> aleas;
  // مؤشر ترابط التنفيذ
  private String executedOn;

  // المنشئات

  public ServiceResponse() {
      // خيط التنفيذ
    executedOn = Thread.currentThread().getName();
  }

  public ServiceResponse(int delay, List<Integer> aleas) {
      // منشئ محلي
    this();
    // عمليات تهيئة أخرى
    this.delay = delay;
    this.aleas = aleas;
  }

  // وظائف الاسترجاع والتعيين
...
}

يتم تنفيذ الواجهة [IService] بواسطة الفئة [Service] التالية:


package dvp.rxswing.service;

import java.util.*;

public class Service implements IService {

  @Override
  public ServiceResponse getAleas(int a, int b, int minCount, int maxCount, int minDelay, int maxDelay) {
    // أرقام عشوائية في النطاق [a,b]
    // يتم توليد n عددًا، حيث n نفسه عدد عشوائي في النطاق [minCount, maxCount]
    // يتم توليد الأعداد بعد انتظار مدته delay مللي ثانية،
    // حيث [delay] هو نفسه عدد عشوائي في النطاق [minDelay, maxDelay]

    // بعض عمليات التحقق
    List<String> messages = new ArrayList<>();
    int erreur = 0;
    if (a < 0) {
      messages.add("Le nombre a de l'intervalle [a,b] de génération doit être supérieur à 0");
      erreur |= 2;
    }
    if (a >= b) {
      messages.add("Dans l'intervalle [a,b] de génération, on doit avoir a< b");
      erreur |= 4;
    }
    if (minCount < 0) {
      messages.add("Le nombre min de l'intervalle [min,count] du nombre de valeurs générées doit être supérieur à 0");
      erreur |= 16;
    }
    if (minCount > maxCount) {
      messages.add("Dans l'intervalle [min,count] du nombre de valeurs générées, on doit avoir min<= max");
      erreur |= 32;
    }
    if (minDelay < 0) {
      messages.add("Le nombre min de l'intervalle [min,count] du délai d'attente doit être supérieur à 0");
      erreur |= 64;
    }
    if (minCount > maxCount) {
      messages.add("Dans l'intervalle [min,count] du délai d'attente, on doit avoir min<= max");
      erreur |= 128;
    }
    if (maxDelay > 5000) {
      messages.add("L'attente en millisecondes avant la génération des nombres doit être dans l'intervalle [0,5000]");
      erreur |= 256;
    }
    // أخطاء؟
    if (!messages.isEmpty()) {
      throw new AleasException(String.join(" [---] ", messages), erreur);
    }
    // مولد الأرقام العشوائية
    Random random = new Random();
    // في انتظار؟
    int delay = minDelay + random.nextInt(maxDelay - minDelay + 1);
    if (delay > 0) {
      try {
        Thread.sleep(delay);
      } catch (InterruptedException e) {
        throw new AleasException(String.format("[%s : %s]", e.getClass().getName(), e.getMessage()), 1024);
      }
    }
    // توليد النتيجة
    int count = minCount + random.nextInt(maxCount - minCount + 1);
    List<Integer> nombres = new ArrayList<>();
    for (int i = 0; i < count; i++) {
      nombres.add(a + random.nextInt(b - a + 1));
    }
    // إرجاع النتيجة
    return new ServiceResponse(delay,nombres);
  }

}

فئة الاستثناء [AleasException] المستخدمة من قبل الخدمة هي كما يلي:


package dvp.rxswing.service;

public class AleasException extends RuntimeException {

    private static final long serialVersionUID = 1L;
    // رمز الخطأ
  private int code;

  // منشئات
  public AleasException() {
  }

  public AleasException(String detailMessage, int code) {
    super(detailMessage);
    this.code = code;
  }

  public AleasException(Throwable throwable, int code) {
    super(throwable);
    this.code = code;
  }

  public AleasException(String detailMessage, Throwable throwable, int code) {
    super(detailMessage, throwable);
    this.code = code;
  }

  // مُستردات وقيم التعيين
...
}
  • السطر 3: توسع فئة [RuntimeException]. لذا فهي استثناء غير خاضع للرقابة؛
  • السطر 7: تضيف إلى فئتها الأم رمز خطأ (0 = لا يوجد خطأ)؛

8.5. الخدمة غير المتزامنة

Image

  

تقدم طبقة الخدمة غير المتزامنة الواجهة التالية [IRxService]:


package dvp.rxswing.service;

import dvp.rxswing.ui.UiResponse;
import rx.Observable;

public interface IRxService {
  // أرقام عشوائية في النطاق [a,b]
  // يتم توليد n عددًا، حيث n نفسه عدد عشوائي في النطاق [minCount, maxCount]
  // يتم توليد الأعداد بعد انتظار مدته delay مللي ثانية،
  // حيث [delay] هو نفسه عدد عشوائي في النطاق [minDelay, maxDelay]
  public Observable<UiResponse> getAleas(int a, int b, int minCount, int maxCount, int minDelay, int maxDelay, UiResponse uiResponse);
}
  • السطر 11: أصبحت الآن قيمة قابلة للمراقبة تُرجعها الطريقة [getAleas] الخاصة بالخدمة؛

تُرجع الطريقة [getAleas] استجابة من النوع [UiResponse] موجهة إلى الطبقة [Ui]. وهذا النوع هو كما يلي:


package dvp.rxswing.ui;

import dvp.rxswing.service.ServiceResponse;

import java.text.SimpleDateFormat;
import java.util.Calendar;

public class UiResponse {

  // معرف العميل
  private int idClient;
  // استجابة الخدمة
  private ServiceResponse serviceResponse;
  // اسم مؤشر ترابط المراقبة
  private String observedOn;
  // وقت الطلب
  private String requestAt;
  // وقت الرد
  private String responseAt;

  // المنشئات

  public UiResponse() {
      // مؤشر المراقبة
    observedOn = Thread.currentThread().getName();
    // وقت الطلب
    requestAt = getTimeStamp();
  }

  // الطرق الخاصة

  private String getTimeStamp() {
    return new SimpleDateFormat("hh:mm:ss:SSS").format(Calendar.getInstance().getTime());
  }

  // أدوات الاسترجاع والتعيين
...
}
  • توجد الأرقام العشوائية في حقل السطر 13؛
  • أما الحقول الأخرى فهي مخصصة لتحديد خيوط التنفيذ والمراقبة للمتغير القابل للمراقبة في الخدمة غير المتزامنة، بالإضافة إلى أوقات إرسال الطلب إلى الخدمة وتلقي الاستجابة؛

يتم تنفيذ الواجهة غير المتزامنة بواسطة الفئة [RxService] التالية:


package dvp.rxswing.service;

import dvp.rxswing.ui.UiResponse;
import rx.Observable;

public class RxService implements IRxService {

  // خدمة متزامنة
  private IService service;

  // منشئ
  public RxService(IService service) {
    this.service = service;
  }

  @Override
  public Observable<UiResponse> getAleas(int a, int b, int minCount, int maxCount, int minDelay, int maxDelay, UiResponse uiResponse) {
      // يتم إنشاء كائن قابل للمراقبة يُصدر القيمة التي تُرجعها الخدمة المتزامنة
    return Observable.create(subscriber -> {
      try {
          // استدعاء متزامن
        uiResponse.setServiceResponse(service.getAleas(a, b, minCount, maxCount, minDelay, maxDelay));
        // يتم تمرير النتيجة إلى المراقب
        subscriber.onNext(uiResponse);
      } catch (Exception e) {
          // يتم تمرير الخطأ إلى المراقب
        subscriber.onError(e);
      } finally {
          // يتم إخطار المراقب بأن عمليات الإرسال قد انتهت
        subscriber.onCompleted();
      }
    });
  }
}
  • الأسطر 12-14: يتم إنشاء الفئة [RxService] الخاصة بالخدمة غير المتزامنة انطلاقًا من مثيل للواجهة المتزامنة [IService
  • الأسطر 20-33: إنشاء المراقب، وهو نتيجة الطريقة [getAleas
  • السطر 22: يتم استدعاء الطريقة المتزامنة [service.getAleas]. ويتم تضمين نتيجتها من النوع [ServiceResponse] في الكائن من النوع [UiResponse] المراد توفيره لطبقة [swing]. وقد تم تمرير هذا الكائن في البداية ضمن معلمات استدعاء الطريقة (المعلمة الأخيرة، السطر 17)؛
  • السطر 24: يتم إرسال الاستجابة [UiResponse] إلى المراقب (الطبقة [swing]). لا يحتوي الكائن [UiResponse] فقط على المعلومات التي أنشأتها الخدمة المتزامنة في السطر 22. بل يحتوي أيضًا على معلومات أخرى أنشأتها الطريقة المستدعية للطريقة [getAleas] في السطر 17. ولهذا السبب، قامت هذه الطريقة المستدعية بتمرير الكائن [UiResponse] كمعلمة إلى الطريقة [getAleas] (المعلمة الأخيرة، السطر 17)؛
  • السطر 30: لا ننسى الإشارة إلى نهاية عمليات الإرسال. لدينا هنا متغير لا يُرسل سوى قيمة واحدة: وهي القيمة التي تُرجعها الخدمة المتزامنة؛
  • السطر 27: يتم إخطار المراقب بأي خطأ محتمل؛

8.6. الواجهة الرسومية

Image

  
  • تم إنشاء الواجهة الرسومية باستخدام IDE [Netbeans] الذي يحتوي على محرر رسومي جيد. وقد أنشأ هذا المحرر الملف [AbstractJFrameAleas.form] الذي لا يمكن تشغيله إلا بواسطة هذا IDE؛
  • كما تم إنشاء الفئة [AbstractJFrameAleas] بواسطة محرر واجهة المستخدم الرسومية في NetBeans. ثم تمت إعادة هيكلتها على النحو التالي: يتم معالجة أحداث واجهة المستخدم الرسومية التي أردنا إدارتها في الفئة [AbstractJFrameAleas] بواسطة طرق مجردة تم تنفيذها في الفئة الفرعية [JFrameAleasEvents]. وفي النهاية،
    • تتولى الفئة المجردة [AbstractJFrameAleas] مهمة إنشاء واجهة المستخدم الرسومية وعرضها؛
    • تتولى الفئة الفرعية [JFrameAleasEvents] إدارة أحداث تلك الواجهة؛

مكونات واجهة المستخدم الرسومية لعلامة التبويب [Request] هي التالية:

 
رقم
النوع
الاسم
الدور
1
JTabbedPane
jTabbedPane1
حاوية علامات تبويب. تحتوي على علامتي تبويب (JPanel) [jPanelRequest] للاستعلام، و[jPanelresponse] للرد؛
2
JTextField
jTextFieldNbValeurs
عدد الطلبات التي يجب إرسالها إلى خدمة الأرقام العشوائية. في حالة الخدمة غير المتزامنة التي يتم تنفيذها على المجدول [Schedulers.io]، ستتشارك هذه الطلبات معالجًا واحدًا؛
3
JTextField
jTextFieldA
الطرف a من الفاصل الزمني [a,b]
4
JTextField
jTextFieldB
الطرف ب من الفاصل [a,b]
5
JTextField
jTextFieldMinCount
الطرف minCount من النطاق [minCount, maxCount]
6
JTextField
jTextFieldMaxCount
القطب maxCount من النطاق [minCount, maxCount]
7
JTextField
jTextFieldMinDelay
القطب minDelay من النطاق [minDelay, maxDelay]
8
JTextField
jTextFieldMaxDelay
المحطة maxDelay من النطاق [minDelay, maxDelay]
9
JCheckBox
jCheckBoxRxSwing
إذا تم تحديد المربع، يتم إرسال الطلبات إلى الواجهة غير المتزامنة. وإلا، يتم إرسالها إلى الواجهة المتزامنة
10
JComboBox
jComboBoxSchedulers
في حالة الطلبات غير المتزامنة، سيتم تنفيذها باستخدام المجدول المختار هنا
11
JButton
jButtonGenerate
يبدأ تنفيذ الطلبات في الخدمة المتزامنة أو غير المتزامنة

مكونات واجهة المستخدم الرسومية لعلامة التبويب [Response] هي كما يلي:

 
رقم
النوع
الاسم
الدور
1
JLabel
jLabelDuree
المدة الإجمالية لتنفيذ الطلبات بالمللي ثانية
2
JLabel
jLabelNbReponses
العدد الإجمالي للردود التي تمت ملاحظتها (قد يختلف عن عدد الاستعلامات، حيث يمكن أن يوفر كل استعلام عدة قيم للملاحظة)
3
JList
jListNumbers
عرض القيم الملاحظة (المستلمة)
4
JButton
jButtonAnnuler
إلغاء الطلبات قيد التنفيذ

8.7. إنشاء مثيل لواجهة المستخدم الرسومية

  

تدير الفئة [JFrameAleasEvents] أحداث واجهة المستخدم الرسومية، ولا سيما النقر على الزر [Générer]. وهي فئة قابلة للتنفيذ تبدأ في السياق التالي:


public class JFrameAleasEvents extends AbstractJFrameAleas {

    private static final long serialVersionUID = 1L;
    // خدمة التوليد المتزامن
    private IService service;
    // خدمة التوليد غير المتزامن
    private IRxService rxService;

    // المدخلات
    private int nbRequests;
    private int a;
    private int b;
    private int minDelay;
    private int maxDelay;
    private int minCount;
    private int maxCount;

    // رسائل الخطأ
    private final String jLabelNbValuesErrorText = "Tapez un nombre entier >=1";
    private final String jLabelCountErrorText = "minCount doit être >=0 et maxCount>=minCount ";
    private final String jLabelDelayErrorText = "minDelay doit être >=0 et maxDelay>=minDelay et  maxDelay<=5000";
    private final String jLabelIntervalErrorText = "a doit être >=0 et b>=a ";

    // الاشتراكات في العناصر القابلة للمراقبة
    protected List<Subscription> subscriptions = new ArrayList<Subscription>();
    // بداية ونهاية التنفيذ
    private long debut;
    // أداة التعيين jSON
    private ObjectMapper jsonMapper;
    // نموذج الردود
    private DefaultListModel<String> model;

    // المنشئ
    public JFrameAleasEvents() {
        // الأصل
        super();
        // محلي
        initJFrame();
        // الخدمات
        service = new Service();
        rxService = new RxService(service);
        // المُعيّن jSON
        jsonMapper = new ObjectMapper();
    }

    private void initJFrame() {
        // إخفاء رسائل الخطأ
        jLabelCountError.setText("");
        jLabelDelayError.setText("");
        jLabelIntervalError.setText("");
        jLabelNbValuesError.setText("");
        // إخفاء النصوص افتراضيًا
        jTextFieldA.setText("100");
        jTextFieldB.setText("200");
        jTextFieldMinCount.setText("5");
        jTextFieldMaxCount.setText("10");
        jTextFieldMinDelay.setText("100");
        jTextFieldMaxDelay.setText("500");
        jTextFieldNbValeurs.setText("10");
        jLabelDuree.setText("");
        // نموذج الردود
        model = new DefaultListModel<>();
        jListNumbers.setModel(model);
        // عدد النوى
        System.out.printf("La JVM a détecté [%s] coeurs sur votre machine%n", Runtime.getRuntime().availableProcessors());
    }

    public static void main(String args[]) {
        try {
            UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
        } catch (UnsupportedLookAndFeelException | ClassNotFoundException | InstantiationException
                | IllegalAccessException e) {
            System.out.println(e);
            System.exit(0);
        }

        /* إنشاء النموذج وعرضه */
        java.awt.EventQueue.invokeLater(() -> {
            new JFrameAleasEvents().setVisible(true);
        });
    }
  • السطر 1: الفئة [JFrameAleasEvents] تمتد من الفئة [AbstractJFrameAleas] التي تمتد بدورها من فئة Swing [JFrame]. وبالتالي، فإن الفئة [JFrameAleasEvents] هي نافذة Swing؛
  • الأسطر 68-75: الطريقة [main] التي سيتم تنفيذها؛
  • السطر 70: يحدد شكل ومظهر واجهة المستخدم الرسومية؛
  • السطر 79: يتم استدعاء منشئ الفئة [JFrameAleasEvents]: سيتم إنشاء الواجهة الرسومية وتهيئتها. وبمجرد الانتهاء من ذلك، يتم إظهارها؛
  • الأسطر 34-44: المنشئ؛
  • السطر 36: سيؤدي استدعاء منشئ الفئة الأم إلى تهيئة واجهة المستخدم الرسومية. في هذه المرحلة، تكون الواجهة كما صممها المطور. وهي غير مرئية بعد؛
  • السطر 38: يتم تهيئة بعض مكونات واجهة المستخدم الرسومية؛
  • السطر 40: إنشاء مثيل للخدمة المتزامنة؛
  • السطر 41: إنشاء مثيل للخدمة غير المتزامنة؛

8.8. تنفيذ الطلبات المتزامنة

يؤدي النقر على الزر [Générer] إلى تنفيذ الطريقة التالية [doGenerate]:


    @Override
    protected void doGenerate() {
        // هل البيانات صحيحة؟
        if (!isPageValid()) {
            return;
        }
        // rx أم لا؟
        if (jCheckBoxRxSwing.isSelected()) {
            // الاستعلامات غير المتزامنة
            doGenerateWithRxService();
        } else {
            // طلبات متزامنة
            doGenerateWithService();
        }
}
  • الأسطر 4-6: يتم التحقق من صحة المدخلات التي أدخلها المستخدم. لن نعلق على الأسلوب [isPageValid]. فهو أساسي؛
  • السطر 8: يتم اختبار حالة مربع الاختيار RxSwing؛
  • السطر 13: يتم تنفيذ الاستعلامات بشكل متزامن؛

الطريقة [doGenerateWithService] هي كما يلي:


    // التوليد المتزامن
    private void doGenerateWithService() {
        // بدء الانتظار
        beginWaiting();
        try {
            for (int i = 0; i < nbRequests; i++) {
                // تحضير الرد
                UiResponse uiResponse = new UiResponse();
                // رقم العميل
                uiResponse.setIdClient(i);
                // استدعاء متزامن
                uiResponse.setServiceResponse(service.getAleas(a, b, minCount, maxCount, minDelay, maxDelay));
                // وقت الرد
                uiResponse.setResponseAt();
                // تحديث نموذج JList بالردود المستلمة
                model.add(0, jsonMapper.writeValueAsString(uiResponse));
                // تحديث عدد الردود
                jLabelNbReponses.setText(String.valueOf(Integer.parseInt(jLabelNbReponses.getText()) + 1));
            }
        } catch (JsonProcessingException | RuntimeException e) {
            JOptionPane.showMessageDialog(this, getInfoForThrowable("L'erreur suivante s'est produite", e), "Informations",
                    JOptionPane.PLAIN_MESSAGE);
        }
        // نهاية الانتظار
        endWaiting();
}
  • السطر 12: استدعاء متزامن لخدمة توليد الأرقام العشوائية؛
  • يتم تنفيذ الطريقة [doGenerateWithService] بالكامل في مؤشر ترابط حلقة الأحداث (event loop) في Swing. وطالما أن الطريقة لم تنتهِ بعد، فإن واجهة المستخدم الرسومية لا تعالج أي حدث جديد. فهي متجمدة (frozen). وهكذا، على سبيل المثال، لن تظهر أبدًا تحديثات واجهة المستخدم الرسومية في السطرين 16 و18. ولن تظهر إلا بقيمها النهائية، وذلك عند انتهاء تنفيذ جميع الطلبات؛

الطريقة [beginWaiting] (السطر 4) هي كما يلي:


    private void beginWaiting() {
        // أزرار
        jButtonGenerate.setVisible(false);
        jButtonCancel.setVisible(true);
        // مؤشر الانتظار
        jTabbedPane1.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
        jButtonCancel.setCursor(Cursor.getDefaultCursor());
        // إعادة تعيين الردود
        model.clear();
        // اشتراكات Rx
        subscriptions.clear();
        // عرض قائمة الردود
        jTabbedPane1.setSelectedIndex(1);
        jLabelNbReponses.setText("0");
        jLabelDuree.setText("");
        // بدء التنفيذ
        debut = new Date().getTime();
}
  • السطر 3: الزر [Générer] مخفي. وهذا يخلق حدثًا لن يتم تنفيذه هو الآخر إلا عند انتهاء تنفيذ جميع الاستعلامات. ولذلك لا نراه مخفيًا أبدًا لأن الطريقة [endWaiting] في السطر 25 من الطريقة [doGenerateWithService] تعيده إلى العرض؛
  • السطر 13: نختار علامة التبويب [Response] لمشاهدة وصول الردود. ومرة أخرى، لن يتم تنفيذ هذا الحدث إلا عند انتهاء تنفيذ جميع الاستعلامات، حيث سنرى عندئذٍ جميع الردود دفعة واحدة، في حين كنا نرغب في رؤيتها تصل واحدة تلو الأخرى؛

من الواضح أن الواجهة المتزامنة تعاني من أوجه قصور. يتم التغلب على هذه الأوجه بفضل الواجهة غير المتزامنة.

8.9. تنفيذ الطلبات غير المتزامنة

فيما يلي كود تنفيذ الطلبات غير المتزامنة:


private void doGenerateWithRxService() {
        // بدء الانتظار
        beginWaiting();
        // سيتم الحصول على الأرقام العشوائية في شكل متغير قابل للمراقبة
        Observable<UiResponse> observable = Observable.empty();
        // جدولة تنفيذ المراقبات المختلفة
        Scheduler[] schedulers = { Schedulers.io(), Schedulers.computation(), Schedulers.newThread(),
                Schedulers.trampoline(), Schedulers.immediate() };
        Scheduler scheduler = schedulers[jComboBoxSchedulers.getSelectedIndex()];
        // تكوين القيم القابلة للمراقبة
        for (int i = 0; i < nbRequests; i++) {
            // إعداد الاستجابة
            UiResponse uiResponse = new UiResponse();
            uiResponse.setIdClient(i);
            // تم تكوين المراقب ليتم تنفيذه على الجدول الزمني الذي اختاره المستخدم
            // ثم تجميع المراقب الذي تم الحصول عليه مع المراقب الكلي
            observable = observable.mergeWith(
                    rxService.getAleas(a, b, minCount, maxCount, minDelay, maxDelay, uiResponse).subscribeOn(scheduler));
        }
        // المراقب
        observable = observable.observeOn(SwingScheduler.getInstance());
        // حتى الآن، لم نقم سوى بإجراء التهيئة
        // لم يتم إرسال أي طلب حتى الآن إلى الخدمة المتزامنة لتوليد الأرقام العشوائية
        // نقوم بالاشتراك في المراقب - وهذا ما سيؤدي إلى استدعاء الخدمة المتزامنة لتوليد الأرقام العشوائية
        try {
            // لا يوجد هنا سوى اشتراك واحد — والنتيجة هي اشتراك
            subscriptions.add(observable.subscribe(
                    // إشعار بالإرسال
                    uiResponse -> {
                        // يتم تحديث واجهة المستخدم بالاستجابة
                        // هذا ممكن لأن المراقبة تتم في مؤشر ترابط واجهة المستخدم
                        updateUi(uiResponse);
                    } ,
                    // إشعار بخطأ
                    th -> {
                        // حالة خطأ - يتم عرضها
                        String message = getInfoForThrowable("L'erreur suivante s'est produite", th);
                        JOptionPane.showMessageDialog(this, message, "Informations", JOptionPane.PLAIN_MESSAGE);
                        // إلغاء الطلبات
                        doCancel();
                    } ,
                    // إشعار [onCompleted]
                    // نهاية فترة الانتظار
                    this::endWaiting));
        } catch (Throwable th) {
            // حالة استثناء عامة - يتم عرضها
            String message = getInfoForThrowable("L'erreur suivante s'est produite", th);
            JOptionPane.showMessageDialog(this, message, "Informations", JOptionPane.PLAIN_MESSAGE);
            // يتم إلغاء الطلبات
            doCancel();
        }
    }
  • السطر 3: يتم تعديل واجهة المستخدم الرسومية لإظهار أن عملية قد تستغرق وقتًا طويلاً جارية حاليًا؛
  • السطر 5: يتم إنشاء متغير قابل للمراقبة فارغ. وهذا المتغير هو الذي ستراقبه الطبقة [swing
  • السطر 7: جدول المجدولات المحتملة؛
  • السطر 9: أتحنا للمستخدم إمكانية اختيار المجدول الذي سيتم تنفيذ الاستعلامات عليه. ونسترد المجدول الذي اختاره؛
  • الأسطر 11-19: كل استعلام يُرجع كائنًا قابلاً للمراقبة يتم تجميع عناصره (mergeWith) (السطر 17) في الكائن القابل للمراقبة الموجود في السطر 5؛
  • الأسطر 13-14: يتم إنشاء الكائن [UiResponse]. تجدر الإشارة إلى أن هذا الكائن هو في الوقت نفسه معلمة إدخال للطريقة [RxService.getAleas] ونتيجتها (الأسطر 17-18)؛
  • السطر 14: يتم تعريف كل طلب برقمه المسمى هنا [idClient]. وهذا ضروري لأن ترتيب استلام الردود في بيئة غير متزامنة قد يختلف عن ترتيب إرسال الطلبات. ويتيح [idClient] معرفة الطلب الذي تنتمي إليه الردود؛
  • السطران 17-18: يتم إجراء الطلب غير المتزامن [rxService.getAleas]. ويتم تنفيذه على المجدول الذي اختاره المستخدم. ويتم تجميع نتيجته من النوع Observable<UiResponse> مع القيمة القابلة للمراقبة في السطر 5. يجب أن ندرك جيدًا أن الطريقة [rxService.getAleas] تُنفَّذ هنا وتُرجع كائنًا قابلًا للمراقبة. لكن هذا لا يعني أنه تم الحصول على أرقام عشوائية. في الواقع، لا يُنفَّذ الكائن القابل للمراقبة إلا عند الاشتراك فيه. وهذا لم يحدث بعد؛
  • السطر 21: هذه هي التعليمات المهمة: نطلب أن تتم مراقبة العناصر الصادرة عن القيمة القابلة للمراقبة في السطر 5 على مؤشر ترابط واجهة المستخدم (Ui). نستخدم هنا جدولة خاصة بمكتبة RxSwing؛
  • الأسطر 25-51: يتم الاشتراك في العنصر القابل للمراقبة في السطر 5. والآن فقط، سيتم طلب الأرقام العشوائية من الخدمة المتزامنة لتوليد هذه الأرقام. ويكمن الجوهر في التعليمات الواردة في الأسطر 29-33. أما الباقي فيتولى بشكل أساسي معالجة حالات الأخطاء وإشعار [onCompleted] الخاص بالمراقب؛
  • السطور 28-44: يجب أن نتذكر أننا طلبنا مراقبة العملية الموجودة في السطر 5 على مؤشر ترابط واجهة المستخدم (Ui). لذا، يتم تنفيذ الكود الموجود في السطور 28-44 في مؤشر ترابط واجهة المستخدم (Ui
  • الأسطر 29-33: يتم معالجة الإشعار [onNext] الخاص بالمراقب. يتم استلام نوع [UiResponse] صادر عن العملية المراقبة. وهذا يمثل نتيجة إحدى الطلبات غير المتزامنة. يتم تحديث واجهة المستخدم الرسومية بهذه الاستجابة؛
  • الأسطر 34-41: تتم معالجة الإشعار [onError] الخاص بالمُراقَب. يتم عرض مربع حوار يوضح الخطأ (الأسطر 37-38) ثم يتم إلغاء الطلبات (السطر 40)؛
  • الأسطر 42-44: يتم معالجة الإشعار [onCompleted] الخاص بالمراقب. يتم تحديث واجهة المستخدم الرسومية لإظهار أن الخدمة المطلوبة قد اكتملت. كان من الممكن أيضًا كتابة السطر 44 بالطريقة التالية
 ()->{endWaiting();}

وقد فضلنا هنا استخدام مرجع الأسلوب؛

  • الأسطر 45-51: بعض الاستثناءات لا تمر عبر الأسطر 34-41. ويحدث ذلك عند إرسال عدد كبير جدًا من الطلبات. فبعد تجاوز حد معين يعتمد على بيئة العمل وقت التنفيذ، تظهر رسالة [StackOverflowError] التي يتم اعتراضها بواسطة الأسطر 45-51؛
  • السطر 27: ينتج الاشتراك نوعًا [Subscription] الذي يُضاف إلى قائمة الاشتراكات. وستحتوي هذه القائمة هنا على عنصر واحد فقط؛

السطر 32: يتم تحديث واجهة المستخدم الرسومية باستخدام الطريقة التالية [updateUi]:


    private void updateUi(UiResponse uiResponse) {
        // وقت الرد
        uiResponse.setResponseAt();
        // مؤشر المراقبة
        uiResponse.setObservedOn();
        // عدد الردود
        jLabelNbReponses.setText(String.valueOf(Integer.parseInt(jLabelNbReponses.getText()) + 1));
        // مدة التنفيذ
        jLabelDuree.setText(String.valueOf(new Date().getTime() - debut));
        // إضافة السلسلة jSON من الرد إلى نموذج الردود JList
        try {
            model.add(0, jsonMapper.writeValueAsString(uiResponse));
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
}

نلاحظ هنا أن مكونات واجهة المستخدم الرسومية يتم تحديثها (الأسطر 7 و9 و12). ولتحقيق ذلك، يجب أن تكون العملية جارية بالضرورة في مؤشر ترابط واجهة المستخدم (حلقة الأحداث).

الطريقة [endWaiting] هي كما يلي:


    private void endWaiting() {
        // زر [Générer] مرئي
        jButtonGenerate.setVisible(true);
        // زر [Annuler] مخفي
        jButtonCancel.setVisible(false);
        // مؤشر الانتظار مخفي
        jTabbedPane1.setCursor(Cursor.getDefaultCursor());
        // علامة تبويب «الردود» محددة
        jTabbedPane1.setSelectedIndex(1);
        // آخر تحديث
        jLabelDuree.setText(String.valueOf(new Date().getTime() - debut));
}

يتم استدعاء الطريقة [doCancel] عند حدوث خطأ في تنفيذ الطلبات غير المتزامنة أو عند قيام المستخدم بالنقر على الزر [Annuler]. وفيما يلي شفرة هذه الطريقة:


// الاشتراكات في العناصر القابلة للمراقبة
    private List<Subscription> subscriptions = new ArrayList<Subscription>();
....

    @Override
    protected void doCancel() {
        // نهاية الانتظار
        endWaiting();
        // في حالة الاشتراكات
        if (jCheckBoxRxSwing.isSelected() && subscriptions != null) {
            subscriptions.forEach(Subscription::unsubscribe);
            //subscriptions.forEach(s -> s.unsubscribe());
        }
    }

  • السطر 2: [subscriptions] هي قائمة باشتراك واحد؛
  • السطر 11: يتم إلغاء جميع الاشتراكات؛
  • السطر 12: كتابة أخرى للسطر 11. تنتظر الطريقة [forEach] هنا مثيلًا من النوع Consumer<Subscription> (انظر الفقرة 4.4

لنعد إلى كود الطريقة [doGenerateWithService]: يمكن تقسيمه إلى خطوتين:

  1. مرحلة تكوين العناصر القابلة للمراقبة. ويتم ذلك في مؤشر الترابط الخاص بالمتصل بالطريقة [doGenerateWithService]، أي مؤشر الترابط الخاص بواجهة المستخدم (Ui
  2. الاشتراك الذي سيؤدي إلى تنفيذ العناصر القابلة للمراقبة؛

إذا كان أحد المجدولين ([Schedulers.computation(), Scheduler.io(), Schedulers.newThread()]) هو المسؤول عن جدولة العمليات القابلة للمراقبة، فستُنفَّذ هذه العمليات خارج مؤشر ترابط واجهة المستخدم (UI). وستتنافس مؤشرات الترابط المختلفة هذه على نواة (أو نوى) الجهاز. ونظرًا لأن الاستعلامات عمليات تستغرق وقتًا طويلاً (عدة مئات من الميلي ثانية)، فإن الأسلوب [doGenerateWithService] الذي يتم تنفيذه في مؤشر ترابط واجهة المستخدم (Ui) سينتهي قبل أن ترسل الاستعلامات ردودها. ولكن هذه الطريقة كانت قد نُفِّذت عند حدوث حدث النقر على الزر [Générer]. وبمجرد معالجة هذا الحدث، سيتمكن مؤشر ترابط واجهة المستخدم من الانتقال إلى معالجة الأحداث التالية. وهناك العديد منها. وهكذا، فقد قامت الطريقة [beginWaiting] بوضع العديد منها:


    private void beginWaiting() {
        // الأزرار
        jButtonGenerate.setVisible(false);
        jButtonCancel.setVisible(true);
        // مؤشر الانتظار
        jTabbedPane1.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
        jButtonCancel.setCursor(Cursor.getDefaultCursor());
        // مسح الردود
        model.clear();
        // اشتراكات Rx
        subscriptions.clear();
        // عرض قائمة الردود
        jTabbedPane1.setSelectedIndex(1);
        jLabelNbReponses.setText("0");
        jLabelDuree.setText("");
        // بدء التنفيذ
        debut = new Date().getTime();
}

تؤثر جميع أسطر هذا الكود تقريبًا على واجهة المستخدم الرسومية. ولا يتم هذا التحديث على الفور: حيث يتم وضع الأحداث في قائمة انتظار حلقة الأحداث. وبمجرد معالجة حدث النقر على الزر [Générer]، يتم تنفيذ هذه الأحداث بالترتيب ويمكن للمستخدم ملاحظة تغير واجهة المستخدم الرسومية:

  • يتم عرض علامة التبويب [Response] (السطر 13) ويتم ربطها بمؤشر انتظار (السطر 6)
  • يتم عرض زره [Annuler] (السطر 4) ويمكن للمستخدم النقر عليه؛
  • يتم إفراغ حقل JList الخاص بالإجابات (السطر 9)؛
  • يعرض JLabel الخاص بعدد الإجابات الرقم 0؛
  • يعرض JLabel الخاص بمدة التنفيذ سلسلة فارغة؛

طوال فترة تنفيذ الاستعلامات، يحصل مؤشر الترابط الخاص بـ UI على وصول منتظم إلى المعالج. وبذلك يمكنه معالجة الأحداث المعلقة. ومن بين هذه الأحداث، هناك تلك التي تم تعيينها بواسطة الطريقة [updateUi]:


    private void updateUi(UiResponse uiResponse) {
        // وقت الرد
        uiResponse.setResponseAt();
        // سلسلة المراقبة
        uiResponse.setObservedOn();
        // عدد الردود
        jLabelNbReponses.setText(String.valueOf(Integer.parseInt(jLabelNbReponses.getText()) + 1));
        // مدة التنفيذ
        jLabelDuree.setText(String.valueOf(new Date().getTime() - debut));
        // إضافة السلسلة jSON من الرد إلى نموذج JList للردود
        try {
            model.add(0, jsonMapper.writeValueAsString(uiResponse));
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
}

عندما يكون خيط واجهة المستخدم (Ui) هو الذي يتولى زمام الأمور:

  • يتم تحديث قيمة JLabel الخاصة بعدد الردود (السطر 7)؛
  • يتم تحديث JLabel الخاص بمدة التنفيذ (السطر 9)؛
  • يتم تحديث JList الخاص بالاستجابات من خلال نموذجه (السطر 12)؛

وبذلك يرى المستخدم تقدم تنفيذ الطلبات. بالإضافة إلى ذلك، يمكنه إلغاء هذه الطلبات عبر الزر [Annuler]. وهنا تكمن أهمية وجود خدمات غير متزامنة أمام الطبقة [swing]، وتعد RxJava تقنية مفضلة لتنفيذها.

وأخيرًا، تجدر الإشارة إلى أنه إذا اختار المستخدم أحد برامج الجدولة [Schedulers.immediate(), Schedulers.trampoline()]، فإن العناصر القابلة للمراقبة تُنفَّذ على نفس مؤشر الترابط الذي يعمل عليه المستدعي، أي مؤشر ترابط واجهة المستخدم. وبذلك نعود إلى التشغيل المتزامن.

وقد تم عرض النتائج التي تم الحصول عليها باستخدام المجدولات المختلفة في الفقرات 2.8.1 و2.8.2 و2.8.3 و2.8.4.