Skip to content

8. RxJava در محیط Swing

8.1. Introduction

در اینجا، به برنامه Swing ارائه‌شده در بخش ۲ بازمی‌گردیم.

  

برای کار با 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'
}
  • خط ۱۵: وابستگی به RxSwing؛

ما تنها از یک شیء اختصاصی برای RxSwing استفاده خواهیم کرد: برنامه‌ریز [SwingScheduler.getInstance()] که قابل‌مشاهده‌ها را روی نخ چرخه رویداد Swing اجرا و مشاهده می‌کند. ما از آن منحصراً برای مشاهدهٔ قابل‌مشاهده‌هایی استفاده خواهیم کرد که روی نخ‌های غیر از نخ چرخهٔ رویداد اجرا می‌شوند. بیایید معماری برنامهٔ نمونه را مرور کنیم:

Image

  • لایه سرویس ناهمزمان متدهایی را فراهم می‌کند که observableها را برمی‌گردانند. ما این observableها را در نخ‌های (threads) غیر از نخ رویدادپرداز (event loop thread) اجرا می‌کنیم. این کار تضمین می‌کند که رابط کاربری گرافیکی (GUI) قفل نشود و بتواند به ورودی کاربر پاسخ دهد. مشهورترین مثال، اجازه دادن به کاربر برای کلیک کردن روی یک دکمه ([Annuler]) برای لغو یک عملیات غیرهمزمان است که بیش از حد طول می‌کشد. برای اینکه این کار انجام شود، رابط کاربری گرافیکی باید به سادگی منجمد شود (frozen
  • لایه Swing باید نتایج بازگردانده‌شده توسط عملیات‌های غیرهمزمان را پردازش کرده و از آن‌ها برای به‌روزرسانی رابط کاربری گرافیکی استفاده کند. با این حال، این کار فقط در داخل نخ حلقه رویداد (event loop thread) قابل انجام است. برای دستیابی به این هدف، این نتایج در برنامه‌ریز (scheduler) [SwingScheduler.getInstance()] نظارت می‌شوند؛

بنابراین، در کد رسیدگی به رویدادهای رابط کاربری گرافیکی، تعامل با لایه غیرهمزمان [rxService] به شکل زیر است:


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

که در آن برنامه‌ریز [Schedulers.computation()] بسته به مورد استفاده می‌تواند با برنامه‌ریز دیگری جایگزین شود.

از خواننده دعوت می‌شود که بار دیگر به پاراگراف ۲ مراجعه کند. اکنون آنها دانش لازم برای درک کامل آن را دارند.

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] خود یک عدد تصادفی در بازه [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] خود یک عدد تصادفی در بازه [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;
  }

  // گیرنده و تنظیم‌کننده
...
}
  • خط ۳: این کلاس را گسترش می‌دهد. بنابراین یک استثنای رسیدگی‌نشده است؛
  • خط ۷: یک کد خطا به کلاس والد خود اضافه می‌کند (۰ = بدون خطا)؛

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] خود یک عدد تصادفی در بازه [minDelay, maxDelay] است
  public Observable<UiResponse> getAleas(int a, int b, int minCount, int maxCount, int minDelay, int maxDelay, UiResponse uiResponse);
}
  • خط ۱۱: متد [getAleas] سرویس اکنون یک observable را بازمی‌گرداند؛

متد [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());
  }

  // گیرنده و تنظیم‌کننده
...
}
  • اعداد تصادفی در فیلد روی خط ۱۳ قرار دارند؛
  • سایر فیلدها برای مشخص کردن نخ‌های اجرا و مشاهده برای observable سرویس ناهمزمان، و همچنین زمان‌های درخواست انجام‌شده به سرویس و پاسخ دریافتی استفاده می‌شوند؛

رابط غیرهم‌زمان توسط کلاس زیر، [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) {
      // ما یک Observable ایجاد می‌کنیم که مقدار بازگشتی سرویس همگام را منتشر می‌کند
    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();
      }
    });
  }
}
  • خطوط ۱۲–۱۴: کلاس [RxService] سرویس ناهمزمان از یک نمونه از رابط همزمانی [IService] ساخته می‌شود؛
  • خطوط ۲۰–۳۳: ساختنی observable، نتیجه متد [getAleas];
  • خط ۲۲: متد همگام [service.getAleas] فراخوانی می‌شود. نتیجه آن، از نوع [ServiceResponse]، در شی از نوع [UiResponse] که باید به لایه [swing] ارائه شود، گنجانده می‌شود. این شیء در ابتدا در پارامترهای فراخوانی متد (آخرین پارامتر، خط 17) ارسال شده بود؛
  • خط ۲۴: پاسخ [UiResponse] به ناظر (لایه [swing]) ارسال می‌شود. شیء [UiResponse] تنها شامل اطلاعاتی که توسط سرویس همگام در خط ۲۲ تولید شده است نیست. بلکه شامل اطلاعات دیگری نیز هست که توسط متد فراخوانی‌کننده متد [getAleas] در خط ۱۷ تولید شده است. به همین دلیل این متد فراخوانی، شیء [UiResponse] را به عنوان پارامتر به متد [getAleas] (آخرین پارامتر، خط 17) پاس کرده است؛
  • خط ۳۰: نباید فراموش کنیم که پایان انتقال‌ها را سیگنال دهیم. در اینجا یک مشاهد‌ه‌پذیر داریم که تنها یک مقدار صادر می‌کند: مقداری که توسط سرویس همگام بازگردانده می‌شود؛
  • خط ۲۷: هرگونه خطا به ناظر گزارش می‌شود؛

8.6. رابط کاربری گرافیکی

Image

  
  • رابط کاربری گرافیکی با استفاده از IDE [Netbeans] که ویرایشگر گرافیکی خوبی دارد، ساخته شده است. این ویرایشگر فایل [AbstractJFrameAleas.form] را تولید کرده است که فقط توسط این IDE قابل استفاده است؛
  • کلاس [AbstractJFrameAleas] نیز توسط ویرایشگر گرافیکی NetBeans تولید شد. سپس به صورت زیر بازسازی شد: رویدادهای رابط کاربری (GUI) که می‌خواستیم مدیریت کنیم، در کلاس [AbstractJFrameAleas] توسط متدهای انتزاعی پیاده‌سازی‌شده در کلاس فرزند [JFrameAleasEvents] پردازش می‌شوند. در نهایت،
    • کلاس انتزاعی [AbstractJFrameAleas] مسئول ساخت و نمایش رابط کاربری گرافیکی است؛
    • کلاس فرزند [JFrameAleasEvents] مسئول رسیدگی به رویدادهای آن است؛

اجزای رابط کاربری گرافیکی زبانه [Request] به شرح زیر هستند:

 
خیر.
نوع
نام
نقش
۱
JTabbedPane
jTabbedPane1
یک کانتینر زبانه دار. شامل دو زبانه (JPanel) برای درخواست و ([jPanelRequest]) برای پاسخ؛
2
JTextField
jTextFieldNbValeurs
تعداد درخواست‌هایی که باید به سرویس عدد تصادفی ارسال شوند. در صورتی که سرویس غیرهمزمان روی زمان‌بندی‌کننده [Schedulers.io] اجرا شود، این درخواست‌ها یک پردازنده را به اشتراک خواهند گذاشت؛
3
JTextField
jTextFieldA
نقطه پایانی a از بازه [a,b]
۴
JTextField
jTextFieldB
ترمینال b از بازه [a,b]
۵
JTextField
jTextFieldMinCount
ترمینال minCount در محدوده [minCount, maxCount]
۶
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] به شرح زیر است:

 
خیر.
نوع
نام
نقش
۱
JLabel
jLabelDuree
زمان اجرای کل پرس‌وجوها به میلی‌ثانیه
۲
JLabel
jLabelNbReponses
تعداد کل پاسخ‌های مشاهده‌شده (ممکن است با تعداد درخواست‌ها متفاوت باشد، زیرا هر درخواست ممکن است چندین مقدار برای مشاهده بازگرداند)
۳
JList
jListNumbers
نمایش مقادیر مشاهده‌شده (دریافت‌شده)
4
JButton
jButtonAnnuler
لغو درخواست‌هایی که در حال اجرا هستند

8.7. مثال‌سازی رابط کاربری گرافیکی

  

کلاس [JFrameAleasEvents] رویدادهای رابط کاربری گرافیکی (GUI) را مدیریت می‌کند، از جمله کلیک روی دکمه [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);
        });
    }
  • خط ۱: کلاس [JFrameAleasEvents] از کلاس [AbstractJFrameAleas] که خود از کلاس Swing، [JFrame]، ارث می‌برد، گسترش می‌یابد. بنابراین، کلاس [JFrameAleasEvents] یک پنجره Swing است؛
  • خطوط ۶۸–۷۵: متد [main] که قرار است اجرا شود؛
  • خط ۷۰: ظاهر و حس رابط کاربری گرافیکی را تنظیم می‌کند؛
  • خط ۷۹: سازنده کلاس [JFrameAleasEvents] فراخوانی می‌شود: رابط کاربری گرافیکی ساخته و اولیه می‌شود. پس از انجام این کار، قابل مشاهده می‌شود؛
  • خطوط ۳۴–۴۴: کانتراکتور؛
  • خط ۳۶: فراخوانی کَنستراکتور والد، رابط کاربری گرافیکی را راه‌اندازی می‌کند. در این مرحله، این رابط دقیقاً همان‌طور است که توسعه‌دهنده طراحی کرده است. این رابط هنوز قابل مشاهده نیست؛
  • خط ۳۸: برخی از اجزای رابط کاربری گرافیکی مقداردهی اولیه می‌شوند؛
  • خط ۴۰: نمونه‌سازی سرویس همگام؛
  • خط ۴۱: نمونه‌سازی سرویس همگام؛

8.8. اجرای درخواست‌های همگام

کلیک روی دکمه [Générer] باعث اجرای متد زیر [doGenerate] می‌شود:


    @Override
    protected void doGenerate() {
        // ورودی‌های معتبر؟
        if (!isPageValid()) {
            return;
        }
        // rx یا نه؟
        if (jCheckBoxRxSwing.isSelected()) {
            // درخواست‌های ناهمزمان
            doGenerateWithRxService();
        } else {
            // درخواست‌های همزمان
            doGenerateWithService();
        }
}
  • خطوط ۴–۶: ورودی کاربر از نظر اعتبار بررسی می‌شود. ما در مورد متد [isPageValid] نظری نمی‌دهیم. این متد ساده است؛
  • خط ۸: وضعیت تیک‌باکس RxSwing بررسی می‌شود؛
  • خط ۱۳: پرس‌وجوها به‌صورت همگام اجرا می‌شوند؛

متد [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();
}
  • خط ۱۲: فراخوانی همزمان سرویس تولیدکننده اعداد تصادفی؛
  • اجرای متد [doGenerateWithService] به طور کامل در داخل نخ حلقه رویداد Swing انجام می‌شود. تا زمانی که متد به پایان نرسد، رابط کاربری گرافیکی هیچ رویداد جدیدی را پردازش نمی‌کند. این رابط منجمد (frozen) می‌شود (frozen). بنابراین، برای مثال، به‌روزرسانی‌های رابط کاربری گرافیکی در خطوط ۱۶ و ۱۸ هرگز نمایش داده نخواهند شد. آنها تنها پس از اتمام اجرای تمام پرس‌وجوها با مقادیر نهایی خود قابل مشاهده خواهند بود؛

متد [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] پنهان است. این یک رویداد را فعال می‌کند که به همین ترتیب، تنها پس از اتمام اجرای تمام پرس‌وجوها قابل اجرا خواهد بود. بنابراین، این دکمه هرگز واقعاً پنهان نمی‌شود، زیرا متد [endWaiting] در خط ۲۵ از متد [doGenerateWithService] آن را دوباره نمایش می‌دهد؛
  • خط ۱۳: ما زبانه [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();
        }
    }
  • خط ۳: رابط کاربری گرافیکی برای نشان دادن اینکه یک عملیات بالقوه زمان‌بر در حال انجام است، تغییر داده می‌شود؛
  • خط ۵: یک observable خالی ایجاد می‌شود. این observable است که توسط لایه [swing] مشاهده خواهد شد؛
  • خط ۷: آرایهٔ برنامه‌ریزهای ممکن؛
  • خط ۹: ما به کاربر این امکان را داده‌ایم که برنامه‌ریز مورد نظر خود را برای اجرای پرس‌وجوها انتخاب کند. ما برنامه‌ریز انتخابی او را بازیابی می‌کنیم؛
  • خطوط ۱۱–۱۹: هر پرس‌وجو یک observable را بازمی‌گرداند که عناصر آن (mergeWith) (خط ۱۷) در observable خط ۵ تجمیع می‌شوند؛
  • خطوط ۱۳–۱۴: شیء [UiResponse] ساخته می‌شود. توجه داشته باشید که این شیء هم پارامتر ورودی متد [RxService.getAleas] و هم نتیجه آن است (خطوط ۱۷–۱۸)؛
  • خط ۱۴: هر درخواست با شماره خود شناسایی می‌شود که در اینجا به آن [idClient] گفته می‌شود. این امر ضروری است زیرا در یک محیط ناهمزمان، ترتیبی که پاسخ‌ها دریافت می‌شوند ممکن است با ترتیبی که درخواست‌ها ارسال می‌شوند متفاوت باشد. [idClient] نشان می‌دهد که پاسخ مربوط به کدام درخواست است؛
  • خطوط 17–18: درخواست ناهمزمان انجام می‌شود: [rxService.getAleas]. این درخواست روی زمان‌بندی‌کننده‌ای که توسط کاربر انتخاب شده است، اجرا می‌شود. نتیجه آن، از نوع `Observable<UiResponse>`, با مشاهده‌پذیر خط 5 ادغام می‌شود. مهم است که درک کنیم متد [rxService.getAleas] در اینجا اجرا شده و یک observable را بازمی‌گرداند. با این حال، این بدان معنا نیست که اعداد تصادفی تولید شده‌اند. در واقع، یک observable تنها زمانی اجرا می‌شود که به آن مشترک (subscribe) شود. این هنوز اتفاق نیفتاده است؛
  • خط ۲۱: این دستور کلیدی است: ما مشخص می‌کنیم که مشاهده عناصر صادرشده توسط observable در خط ۵ باید روی نخ رابط کاربری (UI thread) انجام شود. در اینجا، ما از یک زمان‌بندی‌کننده (scheduler) مخصوص کتابخانه RxSwing استفاده می‌کنیم؛
  • خطوط ۲۵–۵۱: ما در خط ۵ به observable مشترک می‌شویم. تنها اکنون است که اعداد تصادفی از سرویس تولید عدد تصادفی همگام درخواست خواهند شد. بخش کلیدی در دستورالعمل‌های خطوط ۲۹–۳۳ است. بقیه کد اساساً موارد خطا و اعلان [onCompleted] را از observable مدیریت می‌کند؛
  • خطوط 28–44: به یاد داشته باشید که ما در خط 5 روی نخ UI درخواست مشاهده فرآیند را داده‌ایم. بنابراین، کد در خطوط 28–44 روی نخ UI اجرا می‌شود؛
  • خطوط ۲۹–۳۳: ما اعلان [onNext] قابل مشاهده را مدیریت می‌کنیم. ما مقداری از نوع [UiResponse] را که توسط فرآیند مشاهده‌شده صادر شده است، دریافت می‌کنیم. این نتیجه یکی از درخواست‌های ناهمزمان است. ما رابط کاربری گرافیکی را با این پاسخ به‌روزرسانی می‌کنیم؛
  • خطوط ۳۴–۴۱: اعلان [onError] از مشاهده‌شونده پردازش می‌شود. یک کادر گفتگو که خطا را نمایش می‌دهد، نشان داده می‌شود (خطوط ۳۷–۳۸)، سپس درخواست‌ها لغو می‌شوند (خط ۴۰)؛
  • خطوط ۴۲–۴۴: ما اعلان [onCompleted] را از مشاهده‌شونده مدیریت می‌کنیم. رابط کاربری گرافیکی را به‌روزرسانی می‌کنیم تا نشان دهیم سرویس درخواست‌شده تکمیل شده است. خط ۴۴ می‌توانست به شکل زیر نیز نوشته شود
 ()->{endWaiting();}

در اینجا، ما استفاده از ارجاع متد را انتخاب کرده‌ایم؛

  • خطوط ۴۵–۵۱: برخی خطاها از خطوط ۳۴–۴۱ عبور نمی‌کنند. این امر زمانی رخ می‌دهد که درخواست‌های زیادی ارسال شود. به محض اینکه یک حد معین—که به محیط زمان اجرا بستگی دارد—عبور کند، یک [StackOverflowError] تولید و توسط خطوط ۴۵–۵۱ رهگیری می‌شود؛
  • خط ۲۷: اشتراک نوع [Subscription] را تولید می‌کند که به فهرست اشتراک‌ها اضافه می‌شود. این فهرست در اینجا تنها شامل یک مورد خواهد بود؛

خط ۳۲: رابط کاربری گرافیکی با استفاده از روش زیر [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();
        }
}

در اینجا می‌توانیم ببینیم که اجزای رابط کاربری گرافیکی به‌روزرسانی می‌شوند (خطوط ۷، ۹، ۱۲). برای اینکه این امر ممکن شود، کد باید در نخ رابط کاربری (حلقه‌ی رویداد) در حال اجرا باشد.

متد [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);
            //QZXW2HTMLCc3Vic2NyaXB0aW9ucy.mb3JFYWNoZQX(s -> s.unsubscribe());
        }
    }

  • خط ۲: [subscriptions] یک لیست از اشتراک است؛
  • خط ۱۱: همه اشتراک‌ها لغو می‌شوند؛
  • خط ۱۲: نسخه‌ای دیگر از خط ۱۱. متد [forEach] در اینجا یک نمونه از نوع Consumer<Subscription> را انتظار دارد (به بخش ۴.۴ مراجعه کنید)؛

بیایید به کد متد [doGenerateWithService] بازگردیم: این متد را می‌توان به دو مرحله تقسیم کرد:

  1. مرحله پیکربندی قابل مشاهده. این مرحله در نخ فراخواننده متد [doGenerateWithService]، یعنی نخ رابط کاربری (UI thread)، انجام می‌شود؛
  2. اشتراکی که اجرای مشاهده‌پذیرها را تحریک می‌کند؛

اگر قابل‌مشاهده‌ها توسط یکی از زمان‌بندی‌کننده‌های [Schedulers.computation(), Scheduler.io(), Schedulers.newThread()] برنامه‌ریزی شوند، آن‌ها خارج از نخ رابط کاربری اجرا خواهند شد. این نخ‌های مختلف برای هسته‌های ماشین رقابت خواهند کرد. از آنجا که این درخواست‌ها عملیات زمان‌بری هستند (چند صد میلی‌ثانیه)، متد [doGenerateWithService] که در نخ رابط کاربری اجرا می‌شود، پیش از بازگشت پاسخ‌های درخواست‌ها به پایان می‌رسد. با این حال، این متد توسط رویداد کلیک روی دکمه [Générer] فراخوانده شده بود. پس از پردازش این رویداد، نخ رابط کاربری (UI) قادر خواهد بود به پردازش رویدادهای بعدی بپردازد. چندین رویداد وجود دارد. متد [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();
}

تقریباً هر خط از این کد بر رابط کاربری گرافیکی تأثیر می‌گذارد. این به‌روزرسانی بلافاصله انجام نمی‌شود: رویدادها در صف حلقه رویداد (event loop) قرار می‌گیرند. پس از پردازش رویداد کلیک روی دکمه [Générer]، این رویدادها به ترتیب اجرا می‌شوند و کاربر می‌تواند تغییر رابط کاربری گرافیکی را مشاهده کند:

  • زبانه‌ی [Response] نمایش داده می‌شود (خط ۱۳) و یک نشانگر انتظار به آن اختصاص داده می‌شود (خط ۶)
  • دکمه [Annuler] آن نمایش داده می‌شود (خط ۴) و کاربر قادر خواهد بود روی آن کلیک کند؛
  • فلش پاسخ 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();
        }
}

وقتی نخ رابط کاربری فعال است:

  • متغیر JLabel برای تعداد پاسخ‌ها به‌روزرسانی می‌شود (خط ۷)؛
  • JLabel برای زمان اجرا به‌روزرسانی می‌شود (خط ۹)؛
  • متغیر زمان اجرای JList برای پاسخ‌ها از طریق قالب آن (خط ۱۲) به‌روزرسانی می‌شود؛

این امکان را برای کاربر فراهم می‌کند تا پیشرفت پرس‌وجوها را مشاهده کند. علاوه بر این، آنها می‌توانند با استفاده از دکمه [Annuler] آنها را لغو کنند. این مزیت کلیدی داشتن سرویس‌های غیرهمزمان در مقابل لایه [swing] است و RxJava فناوری منتخب برای پیاده‌سازی آنهاست.

در نهایت، باید توجه داشت که اگر کاربر یکی از زمان‌بندهای [Schedulers.immediate(), Schedulers.trampoline()] را انتخاب کند، عملیات قابل مشاهده (observables) بر روی همان رشته‌ای که فراخوانی را انجام داده است، یعنی رشته رابط کاربری (UI thread)، اجرا می‌شوند. این امر عملاً به عملکرد همگام (synchronous) بازمی‌گردد.

نتایج حاصل از برنامه‌ریزهای مختلف در بخش‌های 2.8.1، 2.8.2، 2.8.3 و 2.8.4 نشان داده شده است.