Skip to content

8. RxJava في بيئة Swing

8.1. مقدمة

هنا، سنعيد النظر في تطبيق 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

  • توفر طبقة الخدمة غير المتزامنة طرقًا تُرجع العناصر القابلة للمراقبة. نُنفذ هذه العناصر القابلة للمراقبة في خيوط أخرى غير خيط حلقة الأحداث. بهذه الطريقة، تظل واجهة المستخدم الرسومية (GUI) سريعة الاستجابة. ويمكنها الاستجابة لمدخلات المستخدم. وأبرز مثال على ذلك هو السماح للمستخدم بالنقر على زر [إلغاء] (Cancel) لمقاطعة عملية غير متزامنة تستغرق وقتًا طويلاً. لكي يعمل هذا، يجب تجميد واجهة المستخدم الرسومية (GUI
  • تحتاج طبقة 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 {
  // random numbers in the [a,b] interval
  // n numbers are generated with n itself a random number in the interval [minCount, maxCount]
  // numbers are generated after a delay of milliseconds,
  // where [delay] is itself a random number in the interval [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 {
 
  // service waiting time
  private int delay;
  // random numbers
  private List<Integer> aleas;
  // execution thread
  private String executedOn;
 
  // manufacturers
 
  public ServiceResponse() {
      // execution thread
    executedOn = Thread.currentThread().getName();
  }
 
  public ServiceResponse(int delay, List<Integer> aleas) {
      // local builder
    this();
    // other initializations
    this.delay = delay;
    this.aleas = aleas;
  }
 
  // getters and setters
...
}

يتم تنفيذ واجهة [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) {
    // random numbers in the [a,b] interval
    // n numbers are generated with n itself a random number in the interval [minCount, maxCount]
    // numbers are generated after a delay of milliseconds,
    // where [delay] is itself a random number in the interval [minDelay, maxDelay]
 
    // some checks
    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;
    }
    // mistakes?
    if (!messages.isEmpty()) {
      throw new AleasException(String.join(" [---] ", messages), erreur);
    }
    // random number generator
    Random random = new Random();
    // waiting?
    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);
      }
    }
    // result generation
    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 result
    return new ServiceResponse(delay,nombres);
  }
 
}

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


package dvp.rxswing.service;
 
public class AleasException extends RuntimeException {
 
    private static final long serialVersionUID = 1L;
    // error code
  private int code;
 
  // manufacturers
  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;
  }
 
  // getters and setters
...
}
  • السطر 3: يوسع فئة [RuntimeException]. وبالتالي فهو استثناء غير معالج؛
  • السطر 7: يضيف رمز خطأ إلى فئته الأصلية (0 = لا يوجد خطأ)؛

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

Image

  

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


package dvp.rxswing.service;
 
import dvp.rxswing.ui.UiResponse;
import rx.Observable;
 
public interface IRxService {
  // random numbers in the [a,b] interval
  // n numbers are generated with n itself a random number in the interval [minCount, maxCount]
  // numbers are generated after a delay of milliseconds,
  // where [delay] is itself a random number in the interval [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 {
 
  // customer id
  private int idClient;
  // service response
  private ServiceResponse serviceResponse;
  // observation thread name
  private String observedOn;
  // query time
  private String requestAt;
  // response time
  private String responseAt;
 
  // manufacturers
 
  public UiResponse() {
      // observation thread
    observedOn = Thread.currentThread().getName();
    // query time
    requestAt = getTimeStamp();
  }
 
  // private methods
 
  private String getTimeStamp() {
    return new SimpleDateFormat("hh:mm:ss:SSS").format(Calendar.getInstance().getTime());
  }
 
  // getters and setters
...
}
  • الأرقام العشوائية موجودة في الحقل في السطر 13؛
  • أما الحقول الأخرى فهي موجودة لتحديد خيوط التنفيذ والمراقبة الخاصة بـ"المراقب" (observable) للخدمة غير المتزامنة، بالإضافة إلى الطوابع الزمنية للطلب المقدم إلى الخدمة والاستجابة المستلمة؛

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


package dvp.rxswing.service;
 
import dvp.rxswing.ui.UiResponse;
import rx.Observable;
 
public class RxService implements IRxService {
 
  // synchronous service
  private IService service;
 
  // manufacturer
  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) {
      // we create an observable emitting the value rendered by the synchronous service
    return Observable.create(subscriber -> {
      try {
          // synchronous call
        uiResponse.setServiceResponse(service.getAleas(a, b, minCount, maxCount, minDelay, maxDelay));
        // the result is passed on to the observer
        subscriber.onNext(uiResponse);
      } catch (Exception e) {
          // we pass the error to the observer
        subscriber.onError(e);
      } finally {
          // the observer is informed that the emissions are finished
        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

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

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

 
رقم
النوع
الاسم
الدور
1
JTabbedPane
jTabbedPane1
حاوية ذات علامات تبويب. تحتوي على علامتي تبويب (JPanel): [jPanelRequest] للطلب و[jPanelResponse] للاستجابة؛
2
JTextField
jTextFieldNbValues
عدد الطلبات التي سيتم إرسالها إلى خدمة الأرقام العشوائية. في حالة تشغيل الخدمة غير المتزامنة على المجدول [Schedulers.io]، ستتشارك هذه الطلبات في معالج واحد؛
3
JTextField
jTextFieldA
نقطة النهاية a من الفاصل [a,b]
4
JTextField
jTextFieldB
نقطة النهاية b من الفترة [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
يبدأ تنفيذ الطلبات إلى الخدمة المتزامنة أو غير المتزامنة

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

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

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

  

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


public class JFrameAleasEvents extends AbstractJFrameAleas {
 
    private static final long serialVersionUID = 1L;
    // synchronous generation service
    private IService service;
    // asynchronous generation service
    private IRxService rxService;
 
    // seizures
    private int nbRequests;
    private int a;
    private int b;
    private int minDelay;
    private int maxDelay;
    private int minCount;
    private int maxCount;
 
    // error messages
    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 ";
 
    // subscriptions to observables
    protected List<Subscription> subscriptions = new ArrayList<Subscription>();
    // start-end of execution
    private long debut;
    // mapper jSON
    private ObjectMapper jsonMapper;
    // answer model
    private DefaultListModel<String> model;
 
    // manufacturer
    public JFrameAleasEvents() {
        // parent
        super();
        // local
        initJFrame();
        // services
        service = new Service();
        rxService = new RxService(service);
        // mapper jSON
        jsonMapper = new ObjectMapper();
    }
 
    private void initJFrame() {
        // hide error messages
        jLabelCountError.setText("");
        jLabelDelayError.setText("");
        jLabelIntervalError.setText("");
        jLabelNbValuesError.setText("");
        // hide texts by default
        jTextFieldA.setText("100");
        jTextFieldB.setText("200");
        jTextFieldMinCount.setText("5");
        jTextFieldMaxCount.setText("10");
        jTextFieldMinDelay.setText("100");
        jTextFieldMaxDelay.setText("500");
        jTextFieldNbValeurs.setText("10");
        jLabelDuree.setText("");
        // answer model
        model = new DefaultListModel<>();
        jListNumbers.setModel(model);
        // number of hearts
        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);
        }
 
        /* Create and display the form */
        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. تنفيذ الطلبات المتزامنة

يؤدي النقر على زر [Generate] إلى تشغيل طريقة [doGenerate] التالية:


    @Override
    protected void doGenerate() {
        // saisies valides ?
        if (!isPageValid()) {
            return;
        }
        // rx ou pas ?
        if (jCheckBoxRxSwing.isSelected()) {
            // requêtes asynchrones
            doGenerateWithRxService();
        } else {
            // requêtes synchrones
            doGenerateWithService();
        }
}
  • الأسطر 4–6: نتحقق من صحة إدخال المستخدم. لن نعلق على طريقة [isPageValid]. فهي بسيطة؛
  • السطر 8: نتحقق من حالة مربع الاختيار RxSwing؛
  • السطر 13: ننفذ الطلبات بشكل متزامن؛

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


    // synchronous generation
    private void doGenerateWithService() {
        // start waiting
        beginWaiting();
        try {
            for (int i = 0; i < nbRequests; i++) {
                // response preparation
                UiResponse uiResponse = new UiResponse();
                // customer no
                uiResponse.setIdClient(i);
                // synchronous call
                uiResponse.setServiceResponse(service.getAleas(a, b, minCount, maxCount, minDelay, maxDelay));
                // response time
                uiResponse.setResponseAt();
                // update the JList model with the responses received
                model.add(0, jsonMapper.writeValueAsString(uiResponse));
                // update number of responses
                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);
        }
        // end waiting
        endWaiting();
}
  • السطر 12: استدعاء متزامن لخدمة توليد الأرقام العشوائية؛
  • يتم تنفيذ الأسلوب [doGenerateWithService] بالكامل داخل مؤشر ترابط حلقة أحداث Swing. حتى يتم الانتهاء من الأسلوب، لا تعالج واجهة المستخدم الرسومية أي أحداث جديدة. فهي متجمدة. وبالتالي، على سبيل المثال، لن تظهر تحديثات واجهة المستخدم الرسومية في السطرين 16 و 18 أبدًا. لن تكون مرئية إلا بقيمها النهائية، وسيحدث هذا في نهاية تنفيذ جميع الطلبات؛

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


    private void beginWaiting() {
        // buttons
        jButtonGenerate.setVisible(false);
        jButtonCancel.setVisible(true);
        // wait slider
        jTabbedPane1.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
        jButtonCancel.setCursor(Cursor.getDefaultCursor());
        // raz answers
        model.clear();
        // rx subscriptions
        subscriptions.clear();
        // the response view is displayed
        jTabbedPane1.setSelectedIndex(1);
        jLabelNbReponses.setText("0");
        jLabelDuree.setText("");
        // start of execution
        debut = new Date().getTime();
}
  • السطر 3: زر [Generate] مخفي. يؤدي هذا إلى إنشاء حدث لا يمكن تنفيذه إلا بعد انتهاء تشغيل جميع الطلبات. ومع ذلك، لا نراه مخفيًا أبدًا، لأن الطريقة [endWaiting] في السطر 25 من الطريقة [doGenerateWithService] تعرضه مرة أخرى؛
  • السطر 13: نختار علامة التبويب [Response] لرؤية وصول الردود. مرة أخرى، لن يتم تنفيذ هذا الحدث إلا بعد انتهاء جميع الطلبات، وعندها سنرى جميع الردود دفعة واحدة، في حين أننا أردنا رؤيتها تصل واحدة تلو الأخرى؛

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

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

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


private void doGenerateWithRxService() {
        // début attente
        beginWaiting();
        // on va obtenir les nombres aléatoires sous la forme d'un observable
        Observable<UiResponse> observable = Observable.empty();
        // Schéduler d'exécution des différents observables
        Scheduler[] schedulers = { Schedulers.io(), Schedulers.computation(), Schedulers.newThread(),
                Schedulers.trampoline(), Schedulers.immediate() };
        Scheduler scheduler = schedulers[jComboBoxSchedulers.getSelectedIndex()];
        // configuration des observables
        for (int i = 0; i < nbRequests; i++) {
            // préparation réponse
            UiResponse uiResponse = new UiResponse();
            uiResponse.setIdClient(i);
            // l'observable est configuré pour s'exécuter sur le schéduler choisi par l'utilisateur
            // puis cumul de l'observable obtenu à l'observable du tout
            observable = observable.mergeWith(
                    rxService.getAleas(a, b, minCount, maxCount, minDelay, maxDelay, uiResponse).subscribeOn(scheduler));
        }
        // observateur
        observable = observable.observeOn(SwingScheduler.getInstance());
        // pour l'instant, on a juste fait de la configuration
        // aucune requête n'a encore été faite au service synchrone de génération des nombres aléatoires
        // on s'abonne à l'observable - c'est ce qui va provoquer l'appel au service synchrone de génération des nombres aléatoires
        try {
            // il n'y a ici qu'un abonnement - le résultat est une souscription
            subscriptions.add(observable.subscribe(
                    // notification d'émission
                    uiResponse -> {
                        // on met à jour l'Ui avec la réponse
                        // ceci est possible car l'observation a lieu dans le thread de l'Ui
                        updateUi(uiResponse);
                    } ,
                    // notification d'erreur
                    th -> {
                        // cas d'erreur - on l'affiche
                        String message = getInfoForThrowable("L'erreur suivante s'est produite", th);
                        JOptionPane.showMessageDialog(this, message, "Informations", JOptionPane.PLAIN_MESSAGE);
                        // annulation requêtes
                        doCancel();
                    } ,
                    // notification [onCompleted]
                    // fin de l'attente
                    this::endWaiting));
        } catch (Throwable th) {
            // cas d'exception + générale - on l'affiche
            String message = getInfoForThrowable("L'erreur suivante s'est produite", th);
            JOptionPane.showMessageDialog(this, message, "Informations", JOptionPane.PLAIN_MESSAGE);
            // on annule les requêtes
            doCancel();
        }
    }
  • السطر 3: يتم تحديث واجهة المستخدم الرسومية (GUI) للإشارة إلى أن عملية قد تستغرق وقتًا طويلاً قيد التنفيذ؛
  • السطر 5: يتم إنشاء متغير قابل للمراقبة فارغ. سيتم مراقبة هذا المتغير من قبل طبقة [Swing
  • السطر 7: مصفوفة المجدولات المحتملة؛
  • السطر 9: لقد منحنا المستخدم القدرة على اختيار المجدول الذي سيتم تنفيذ الاستعلامات عليه. نقوم باسترداد المجدول الذي اختاره؛
  • الأسطر 11-19: يعرض كل طلب عنصرًا قابلًا للمراقبة يتم دمج عناصره (mergeWith) (السطر 17) في العنصر القابل للمراقبة من السطر 5؛
  • السطور 13-14: يتم إنشاء كائن [UiResponse]. تذكر أن هذا الكائن هو معلمة الإدخال لطريقة [RxService.getAleas] ونتيجتها في الوقت نفسه (السطور 17-18)؛
  • السطر 14: يتم تحديد كل طلب برقم، يُشار إليه هنا بـ [idClient]. وهذا ضروري لأن ترتيب استلام الردود في بيئة غير متزامنة قد يختلف عن ترتيب إرسال الطلبات. يسمح لنا [idClient] بتحديد الطلب الذي ينتمي إليه الرد؛
  • السطران 17-18: يتم إجراء الطلب غير المتزامن [rxService.getRandom]. ويتم تنفيذه على المجدول الذي يختاره المستخدم. ويتم دمج نتيجته، من النوع Observable<UiResponseمع المراقب من السطر 5. ومن المهم ملاحظة أن الطريقة [rxService.getAleas] يتم تنفيذها هنا وتُرجع مراقبًا. ومع ذلك، هذا لا يعني أنه تم إنشاء أرقام عشوائية. في الواقع، لا يتم تنفيذ المراقب إلا عند الاشتراك فيه. وهذا لم يحدث بعد؛
  • السطر 21: هذه هي التعليمات الأساسية: نحدد أن العناصر التي تصدرها القابلة للمراقبة في السطر 5 يجب مراقبةها على مؤشر ترابط واجهة المستخدم. هنا، نستخدم جدولة خاصة بمكتبة RxSwing؛
  • الأسطر 25–51: نشترك في المراقب من السطر 5. الآن فقط سيتم طلب الأرقام العشوائية من الخدمة المتزامنة التي تولدها. الجزء الأساسي موجود في التعليمات في الأسطر 29–33. أما الباقي فيتعامل بشكل أساسي مع حالات الخطأ وإشعار [onCompleted] من المراقب؛
  • الأسطر 28-44: تذكر أننا طلبنا مراقبة العملية من السطر 5 في مؤشر ترابط واجهة المستخدم. لذلك، يتم تشغيل الكود في الأسطر 28-44 في مؤشر ترابط واجهة المستخدم؛
  • الأسطر 29–33: نتعامل مع إشعار [onNext] الخاص بـ observable. نتلقى نوع [UiResponse] صادرًا عن العملية المراقبة. هذه هي نتيجة أحد الطلبات غير المتزامنة. نقوم بتحديث واجهة المستخدم باستخدام هذا الرد؛
  • الأسطر 34–41: نتعامل مع إشعار [onError] الخاص بـ observable. نعرض مربع حوار يوضح الخطأ (الأسطر 37–38) ثم نلغي الطلبات (السطر 40)؛
  • الأسطر 42–44: نتعامل مع إشعار [onCompleted] الخاص بالمراقب. نقوم بتحديث واجهة المستخدم الرسومية لإظهار أن الخدمة المطلوبة قد اكتملت. كان من الممكن أيضًا كتابة السطر 44 على النحو التالي
 ()->{endWaiting();}

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

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

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


    private void updateUi(UiResponse uiResponse) {
        // response time
        uiResponse.setResponseAt();
        // observation thread
        uiResponse.setObservedOn();
        // number of responses
        jLabelNbReponses.setText(String.valueOf(Integer.parseInt(jLabelNbReponses.getText()) + 1));
        // running time
        jLabelDuree.setText(String.valueOf(new Date().getTime() - debut));
        // add the jSON response string to the JList response template
        try {
            model.add(0, jsonMapper.writeValueAsString(uiResponse));
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
}

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

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


    private void endWaiting() {
        // generate] button visible
        jButtonGenerate.setVisible(true);
        // hidden [Cancel] button
        jButtonCancel.setVisible(false);
        // hidden wait cursor
        jTabbedPane1.setCursor(Cursor.getDefaultCursor());
        // selected answers tab
        jTabbedPane1.setSelectedIndex(1);
        // duration updated one last time
        jLabelDuree.setText(String.valueOf(new Date().getTime() - debut));
}

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


// les souscriptions aux observables
    private List<Subscription> subscriptions = new ArrayList<Subscription>();
....
 
    @Override
    protected void doCancel() {
        // fin attente
        endWaiting();
        // dans le cas de souscriptions
        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]، أي مؤشر ترابط واجهة المستخدم؛
  2. الاشتراك الذي سيؤدي إلى تشغيل العناصر القابلة للمراقبة؛

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


    private void beginWaiting() {
        // buttons
        jButtonGenerate.setVisible(false);
        jButtonCancel.setVisible(true);
        // waiting cursor
        jTabbedPane1.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
        jButtonCancel.setCursor(Cursor.getDefaultCursor());
        // raz answers
        model.clear();
        // rx subscriptions
        subscriptions.clear();
        // the response view is displayed
        jTabbedPane1.setSelectedIndex(1);
        jLabelNbReponses.setText("0");
        jLabelDuree.setText("");
        // start of execution
        debut = new Date().getTime();
}

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

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

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


    private void updateUi(UiResponse uiResponse) {
        // response time
        uiResponse.setResponseAt();
        // observation thread
        uiResponse.setObservedOn();
        // number of responses
        jLabelNbReponses.setText(String.valueOf(Integer.parseInt(jLabelNbReponses.getText()) + 1));
        // running time
        jLabelDuree.setText(String.valueOf(new Date().getTime() - debut));
        // add the jSON response string to the JList response template
        try {
            model.add(0, jsonMapper.writeValueAsString(uiResponse));
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
}

عندما يكون مؤشر ترابط واجهة المستخدم نشطًا:

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

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

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

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