Skip to content

20. برنامه‌نویسی ناهمزمان با RxJava

مستندات قابل مطالعه: [Introduction à RxJava. Application aux environnements Swing et Android.]

در این فصل، به فصل 17.6 بازمی‌گردیم، جایی که یک برنامهٔ کلاینت/سرور با معماری زیر ساختیم:

برخی از اقدامات کاربر در رابط کاربری Swing در [1]، اقداماتی را تا سطح پایگاه داده در [3] از طریق یک شبکه در HTTP [2] تحریک می‌کنند. به همین دلیل، پاسخ به اقدام کاربر ممکن است زمان متغیری ببرد. مفید خواهد بود که یک نشانگر بارگذاری در رابط کاربری قرار داده شود، با گزینه‌ای برای لغو عملیات در صورتی که بیش از حد طول بکشد. در فصل 17.6، هر اقدام کاربر که نیاز به تبادل اطلاعات با سرور دارد، همگام است. رویدادپرداز اجرا شده توسط کد تا دریافت پاسخ کامل نمی‌شود. در این مدت، رابط کاربری گرافیکی منجمد است: به اقدامات جدید کاربر پاسخ نمی‌دهد. این اقدامات صرفاً در صف قرار می‌گیرند تا پس از اتمام روال پردازش رویداد در حال اجرا، پردازش شوند. بنابراین، اگر دکمه لغو نمایش داده می‌شد، کاربر می‌توانست روی آن کلیک کند اما تا زمانی که عملیات جاری به پایان نرسیده بود، هیچ اتفاقی نمی‌افتاد. در نتیجه، دکمه لغو هیچ کاربردی نداشت.

برای اینکه کلیک روی دکمه لغو تأثیری داشته باشد، عملیات جاری باید کامل شده باشد. برای دستیابی به این هدف، باید عملیات بالقوه زمان‌بر را به صورت غیرهمزمان آغاز کند:

  • پردازشگر رویداد، عملیات زمان‌بر را آغاز می‌کند اما منتظر نتیجه آن نمی‌ماند و کنترل را به نخ UI که رویدادهای رابط کاربری گرافیکی (GUI) را مدیریت می‌کند، بازمی‌گرداند. عملیات زمان‌بر روی نخی متفاوت از نخ UI اجرا می‌شود، که به این معنی است که نخ دوم مسدود نمی‌شود؛
  • اگر کاربر قبل از پایان عملیات طولانی‌مدت، دکمه لغو را کلیک کند، نخ UI که در حالت بیکار است، می‌تواند این رویداد را مدیریت کند. سپس می‌توان عملیات طولانی‌مدت را با نادیده گرفتن نتیجه آن متوقف کرد؛
  • اگر عملیات زمان‌بر لغو نشده باشد، دریافت پاسخ یک رویداد را در نخ UI فعال می‌کند. اگر این نخ در حالت بیکاری باشد، کد مرتبط با این رویداد را اجرا خواهد کرد که پاسخ را پردازش می‌کند؛

رابط کاربری مانند قبل عمل خواهد کرد. اگر زمان پاسخ‌دهی سرور سریع باشد، کاربر هیچ تفاوتی را متوجه نخواهد شد. اگر این تأخیرها قابل توجه باشند، کاربر دکمه لغو را مشاهده خواهد کرد و قادر خواهد بود عملیات در حال اجرا را متوقف کند.

کتابخانه [Rx] برنامه‌نویسی ناهمزمان را امکان‌پذیر می‌سازد. مزیت اصلی آن در این است که به محیط‌های متعددی (جاوا، .NET، JS، ...) و اینکه دانشی که در یک محیط کسب می‌شود، می‌تواند به‌راحتی به محیط دیگر منتقل شود. ما از فصل ۲ سند [Introduction à RxJava. Application aux environnements Swing et Android] استفاده خواهیم کرد. خوانندگان تشویق می‌شوند آن را مطالعه کنند. در ادامه، از کدی که از مثال‌های آن فصل گرفته شده است، استفاده خواهیم کرد.

ما معماری برنامه را به شرح زیر توسعه خواهیم داد:

  • در [1]، ما یک لایه [RxJava] را بین لایه [swing] و لایه [métier] قرار می‌دهیم. متدهای این لایه اکنون به صورت غیرهمزمان فراخوانی خواهند شد؛

ما در چند مرحله پیش خواهیم رفت:

  • مرحله ۱: لایه [metier, DAO] در حال حاضر یک رابط همزمان به لایه [ui] ارائه می‌دهد. ما آن را به یک لایه غیرهمزمان، [RxJava, metier, DAO]، تبدیل خواهیم کرد؛
  • مرحله ۲: ما برنامه کنسول همگام را به یک برنامه جدید منتقل می‌کنیم که همچنان همگام باقی می‌ماند اما از رابط ناهمزمان [RxJava, metier, DAO] استفاده می‌کند؛
  • مرحله ۳: ما برنامه Swing همگام را به یک برنامه Swing غیرهمگام منتقل خواهیم کرد؛

20.1. مرحله ۱

ما در حال تبدیل لایه همگام فعلی [metier, DAO] به یک لایه غیرهمگام [RxJava, metier, DAO] هستیم.

20.1.1. ایجاد

ما با پروژه Maven از فصل 17.4 شروع می‌کنیم که آن را در NetBeans باز خواهیم کرد:

ما این پروژه را، [1] (کپی و پیست)، در یک پروژه جدید، [elections-rxjava-metier-dao-security-webjson] [2]، کپی می‌کنیم.

20.1.2. پیکربندی Maven

ما فایل [pom.xml] را در پروژه جدید به‌روزرسانی می‌کنیم تا وابستگی به کتابخانه [RxJava] را اضافه کنیم:


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>istia.st.elections</groupId>
  <artifactId>elections-metier-dao-security-rxjava-webjson</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <description>Client jUnit du serveur web / jSON</description>
  <name>elections-metier-dao-security-rxjava-webjson</name>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <java.version>1.8</java.version>
  </properties>

  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.2.7.RELEASE</version>
  </parent>

  <dependencies>
    <!-- اسپرینگ -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-web</artifactId>
    </dependency>
    <!-- jSON کتابخانه‌ای که توسط Spring استفاده می‌شود -->
    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-core</artifactId>
    </dependency>
    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-databind</artifactId>
    </dependency>
    <!-- کامپوننتی که توسط Spring استفاده می‌شود RestTemplate -->
    <dependency>
      <groupId>org.apache.httpcomponents</groupId>
      <artifactId>httpclient</artifactId>
    </dependency>
    <!-- گوگل گوآوا -->
    <dependency>
      <groupId>com.google.guava</groupId>
      <artifactId>guava</artifactId>
      <version>16.0.1</version>
      <scope>test</scope>
    </dependency>
    <!--کتابخانهٔ لاگ‌گیری -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-logging</artifactId>
    </dependency>
    <!-- آزمون Spring Boot -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>
    <!-- Spring Boot -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot</artifactId>
      <scope>test</scope>
    </dependency>
    <!-- https://mvnrepository.com/artifact/io.reactivex/rxjava -->
    <dependency>
      <groupId>io.reactivex</groupId>
      <artifactId>rxjava</artifactId>
      <version>1.2.0</version>
    </dependency>

  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>2.18.1</version>
      </plugin>
    </plugins>
  </build>
</project>
  • خطوط ۶۵–۷۰: وابستگی به کتابخانه RxJava را اضافه کرده‌ایم؛

20.1.3. پیاده‌سازی غیرهم‌زمان لایه [métier]

برای پیاده‌سازی لایه [RxJava, métier]، ما در حال افزودن یک رابط غیرهمزمان [IRxElectionsMetier] [1] و پیاده‌سازی آن [RxElectionsMetier] [2] به پروژه هستیم:

  

رابط [IRxElectionsMetier] رابط غیرهمزمان برای لایه [RxJava, métier] است. کد آن به شرح زیر است:


package elections.security.client.metier;

import elections.security.client.entities.ListeElectorale;
import elections.security.client.entities.User;
import rx.Observable;

public interface IRxElectionsMetier {

  //احراز هویت
  Observable<Void> authenticate(User user);

  // دریافت فهرست نامزدها
  Observable<ListeElectorale[]> getListesElectorales(User user);

  // تعداد کرسی‌های قابل پر شدن
  Observable<Integer> getNbSiegesAPourvoir(User user);

  //آستانهٔ انتخاباتی
  Observable<Double> getSeuilElectoral(User user);

  //ثبت نتایج
  Observable<Void> recordResultats(User user, ListeElectorale[] listesElectorales);

  // محاسبه کرسی‌ها
  Observable<ListeElectorale[]> calculerSieges(User user, ListeElectorale[] listesElectorales);

}

رابط [IRxElectionsMetier] متدهای رابط [IElectionsMetier] را در خود جای داده است، اما در حالی که یک متد M از رابط [IElectionsMetier] نتیجه‌ای از نوع T برمی‌گرداند، متد M در رابط [IRxElectionsMetier] نتیجه‌ای از نوع Observable<T> را بازمی‌گرداند. نوع [Observable] توسط کتابخانه RxJava ارائه می‌شود. نوع Observable&lt;T&gt; متد [subscribe] را فراهم می‌کند که نوع T را به‌صورت ناهمزمان بازیابی می‌کند. سه رویداد با این متد مرتبط هستند:

  • onSuccess(T result)، که اعلام می‌کند نتیجه‌ای از نوع T در دسترس است. عملیات ناهمزمان ممکن است چندین نتیجه بازگرداند؛
  • onError(Throwable th)، که نشان می‌دهد عملیات ناهمزمان با خطایی مواجه شده است؛
  • onCompleted()، که اعلام می‌کند عملیات ناهمزمان به پایان رسیده است؛

تا زمانی که متد [Observable.subscribe] فراخوانی نشود، عملیات ناهمزمان مرتبط با قابل مشاهده (observable) آغاز نمی‌گردد. کدی که متد M از اینترفیس [IRxElectionsMetier] را فراخوانی می‌کند، نتیجه مورد انتظار T را دریافت نمی‌کند، بلکه یک نوع Observable<T> دریافت می‌کند که بعداً به آن اجازه می‌دهد با فراخوانی متد [Observable.subscribe]، نتیجه T را به دست آورد.

پیاده‌سازی [RxElectionsMetier] از رابط [IRxElectionsMetier] به شرح زیر است:


package elections.security.client.metier;

import elections.security.client.entities.ListeElectorale;
import elections.security.client.entities.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import rx.Observable;

@Component
public class RxElectionsMetier implements IRxElectionsMetier {

  @Autowired
  private IElectionsMetier metier;

  @Override
  public Observable<Void> authenticate(User user) {
    ...
  }

  @Override
  public Observable<ListeElectorale[]> getListesElectorales(User user) {
    return Observable.create(subscriber -> {
      try {
        // فراخوانی همزمان متد که با پاسخی برای مشترک دنبال می‌شود
        subscriber.onNext(metier.getListesElectorales(user));
        //پایان مشاهده‌پذیر اعلام می‌شود
        subscriber.onCompleted();
      } catch (Exception e) {
        // استثناء منتقل می‌شود
        subscriber.onError(e);
      }
    });
  }

  @Override
  public Observable<Integer> getNbSiegesAPourvoir(User user) {
    ...
  }

  @Override
  public Observable<Double> getSeuilElectoral(User user) {
    ...
  }

  @Override
  public Observable<Void> recordResultats(User user, ListeElectorale[] listesElectorales) {
    ...
  }

  @Override
  public Observable<ListeElectorale[]> calculerSieges(User user, ListeElectorale[] listesElectorales) {
    ...
  }
}
  • خطوط ۱۲–۱۳: تزریق اسپرینگ لایه کسب‌وکار همگام؛
  • خطوط ۲۰–۳۴: ما متد [getListesElectorales] را بررسی خواهیم کرد که به جای بازگرداندن نوع [ListeElectorale[]]، نوع [Observable<ListeElectorale[]>] را بازمی‌گرداند؛
  • خطوط 22–32: متد استاتیک [Observable.create] امکان ایجاد یک Observable از نوع [Subscriber] را فراهم می‌کند. نوع [Subscriber] نمایانگر یک مشترک برای جریان‌های نتیجه‌ای است که توسط فرآیند مشاهده‌شده (Observable) تولید می‌شوند. این نوع سه متد را فراهم می‌کند:
    • [Subscriber.onNext] (خط 25) برای دریافت یک نتیجه از فرآیند مشاهده‌شده؛
    • [Subscriber.onError] (خط ۳۰) برای دریافت یک استثنا از فرآیند مشاهده‌شده. پس از یک استثنا، نوع [Observable] دیگر نتایجی را منتشر نمی‌کند؛
    • [Subscriber.onCompleted] (خط 27) برای دریافت سیگنال پایان انتشار از فرآیند مشاهده‌شده. در اینجا، فرآیند مشاهده‌شده تنها یک عنصر را منتشر می‌کند. توجه داشته باشید که اگر یک استثنا رخ دهد، این سیگنال منتشر نمی‌شود. این رفتار پیش‌فرض Observables است: انتشار یک استثنا همچنین پایان انتشارها را سیگنال می‌دهد. مشترکان از این موضوع آگاه هستند؛
  • خطوط ۲۲–۳۴: متد [Observable.create] یک نوع [Observable.OnSubscribe] را به‌عنوان پارامتر می‌پذیرد. این نوع یک رابط تابعی است. این مفهوم در جاوا ۸ معرفی شد و به رابطی با یک متد واحد اشاره دارد. در اینجا، متد واحد رابط [Observable.OnSubscribe] به شرح زیر است:
T call(Subscriber<T> subscriber)

برای پیاده‌سازی یک رابط تابعی تک‌متدی m(param1, param2, ..., paramn)، می‌توان از سینتکس ساده‌شده زیر استفاده کرد:

(param1, param2, ..., paramn) -> { code de la méthode m}

این کاری است که در خطوط 22–34 انجام می‌شود:

  • [subscriber] پارامتر متد [Observable.OnSubscribe.call] است؛
  • خطوط ۲۳–۳۲: کدی که باید به متد [call] اختصاص داده شود؛
  • خط ۲۵: فهرست‌های رأی‌دهندگان به‌صورت همگام از لایه [métier] که در خط ۱۳ تزریق شده است، درخواست می‌شود. بنابراین منتظر نتیجه خواهیم بود. هنگامی که نتیجه دریافت شد، به متد [onNext] مشترک ارسال می‌شود؛
  • خط ۲۸: در صورت بروز خطا، استثنا به متد مشترک [onError] ارسال می‌شود؛
  • خط ۳۱: ما فقط منتظر یک نتیجه هستیم. پس از به دست آمدن آن (فهرست‌های انتخاباتی یا یک استثنا)، به مشترک اطلاع داده می‌شود که فرآیند مشاهده‌شده تولید نتایج را به پایان رسانده است؛

توجه به این نکته مهم است که متد [RxElectionsMetier] نوع Observable<ListeElectorale[]> را برمی‌گرداند و نه خود نوع ListeElectorale[] را. کد فراخوانی باید متد Observable&lt;ListeElectorale[]&gt;.**subscribe** را فراخوانی کند تا کد در خطوط ۲۳–۳۳ اجرا شود و فهرست‌های انتخاباتی از طریق خط ۲۵ بازگردانده شوند.

کد سایر متدها مشابه است:


package elections.security.client.metier;

import elections.security.client.entities.ListeElectorale;
import elections.security.client.entities.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import rx.Observable;

@Component
public class RxElectionsMetier implements IRxElectionsMetier {

  @Autowired
  private IElectionsMetier metier;

  @Override
  public Observable<Void> authenticate(User user) {
    return Observable.create(subscriber -> {
      try {
        // فراخوانی متد همگام
        metier.authenticate(user);
        // نشان‌دهنده پایان مشاهده‌پذیر
        subscriber.onCompleted();
      } catch (Exception e) {
        // استثناء منتقل می‌شود
        subscriber.onError(e);
      }
    });

  }

  @Override
  public Observable<ListeElectorale[]> getListesElectorales(User user) {
    return Observable.create(subscriber -> {
      try {
        // فراخوانی متد همگام که به دنبال آن پاسخی به مشترک ارسال می‌شود
        subscriber.onNext(metier.getListesElectorales(user));
        // نشان‌دهنده پایان مشاهده‌پذیر
        subscriber.onCompleted();
      } catch (Exception e) {
        // استثناء منتقل می‌شود
        subscriber.onError(e);
      }
    });
  }

  @Override
  public Observable<Integer> getNbSiegesAPourvoir(User user) {
    return Observable.create(subscriber -> {
      try {
        // فراخوانی متد همگام، سپس پاسخ به مشترک
        subscriber.onNext(metier.getNbSiegesAPourvoir(user));
        // پایان مشاهده‌پذیر را علامت‌گذاری می‌کند
        subscriber.onCompleted();
      } catch (Exception e) {
        // استثناء منتقل می‌شود
        subscriber.onError(e);
      }
    });
  }

  @Override
  public Observable<Double> getSeuilElectoral(User user) {
    return Observable.create(subscriber -> {
      try {
        // متد همگام را فراخوانی می‌کند، سپس به مشترک پاسخ می‌دهد
        subscriber.onNext(metier.getSeuilElectoral(user));
        // پایان مشاهده‌پذیر را اعلام می‌کند
        subscriber.onCompleted();
      } catch (Exception e) {
        // استثناء منتقل می‌شود
        subscriber.onError(e);
      }
    });
  }

  @Override
  public Observable<Void> recordResultats(User user, ListeElectorale[] listesElectorales) {
    return Observable.create(subscriber -> {
      try {
        // فراخوانی متد همگام
        metier.recordResultats(user, listesElectorales);
        // نشان‌دهنده پایان مشاهده‌پذیر
        subscriber.onCompleted();
      } catch (Exception e) {
        // استثناء منتقل می‌شود
        subscriber.onError(e);
      }
    });
  }

  @Override
  public Observable<ListeElectorale[]> calculerSieges(User user, ListeElectorale[] listesElectorales) {
    return Observable.create(subscriber -> {
      try {
        // فراخوانی متد همگام که به دنبال آن پاسخی به مشترک ارسال می‌شود
        subscriber.onNext(metier.calculerSieges(user, listesElectorales));
        // نشان‌دهنده پایان مشاهده‌پذیر
        subscriber.onCompleted();
      } catch (Exception e) {
        // استثناء منتقل می‌شود
        subscriber.onError(e);
      }
    });
  }
}
  • خطوط ۲۰ و ۸۱: متد [onNext] مشترک فراخوانی نمی‌شود زیرا مشترک انتظار هیچ نتیجه‌ای را ندارد؛

20.1.4. JUnit لایه [métier] را بررسی می‌کند

  

20.1.4.1. Test01

ما به تست واحد [Test01] که در بخش 17.4.4 مورد بحث قرار گرفت، بازمی‌گردیم. این تست برای انجام فراخوانی‌های همزمان به رابط [IElectionsMetier] طراحی شده بود. ما آن را طوری تغییر می‌دهیم که تماس‌های همگام با رابط جدید [IRxElectionsMetier] برقرار کند. در واقع، امکان برقراری تماس‌های همگام با یک رابط ناهمزمان RxJava نیز وجود دارد. کد به شکل زیر درمی‌آید:


package elections.security.client.metier.junit;

import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import elections.security.client.config.MetierConfig;
import elections.security.client.entities.ElectionsException;
import elections.security.client.entities.ListeElectorale;
import elections.security.client.entities.User;
import elections.security.client.metier.IRxElectionsMetier;
import rx.observables.BlockingObservable;

@SpringApplicationConfiguration(classes = MetierConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class Test01 {

  //لایه [electionsMetier]
  @Autowired
  private IRxElectionsMetier electionsMetier;

  // jSON نقشه بردار
  private final ObjectMapper mapper = new ObjectMapper();

  // کاربران
  static private User admin;
  static private User user;
  static private User unknown;

  @BeforeClass
  public static void initTest() {
    admin = new User("admin", "admin");
    user = new User("user", "user");
    unknown = new User("x", "y");
  }

  @Test()
  public void checkUserUser() {
    ElectionsException se = null;
    try {
      BlockingObservable.from(electionsMetier.authenticate(user)).firstOrDefault(null);
    } catch (ElectionsException e) {
      se = e;
    }
    Assert.assertNotNull(se);
    Assert.assertEquals("403 Forbidden", se.getErreurs().get(0));
  }

  @Test()
  public void checkUserUnknown() {
    ElectionsException se = null;
    try {
      BlockingObservable.from(electionsMetier.authenticate(unknown)).firstOrDefault(null);
    } catch (ElectionsException e) {
      se = e;
    }
    Assert.assertNotNull(se);
    Assert.assertEquals("401 Unauthorized", se.getErreurs().get(0));
  }

  @Test()
  public void checkUserAdmin() {
    ElectionsException se = null;
    try {
      BlockingObservable.from(electionsMetier.authenticate(admin)).firstOrDefault(null);
    } catch (ElectionsException e) {
      se = e;
    }
    Assert.assertNull(se);
  }

  /**
   * vérification 1 : méthode de calcul des sièges on fixe en dur les listes
   */
  @Test
  public void calculSieges1() {
    // جدول فهرست‌های نامزد ۷گانه ایجاد می‌شود
    ListeElectorale[] listes = new ListeElectorale[7];
    listes[0] = new ListeElectorale("A", 32000, 0, false);
    listes[1] = new ListeElectorale("B", 25000, 0, false);
    listes[2] = new ListeElectorale("C", 16000, 0, false);
    listes[3] = new ListeElectorale("D", 12000, 0, false);
    listes[4] = new ListeElectorale("E", 8000, 0, false);
    listes[5] = new ListeElectorale("F", 4500, 0, false);
    listes[6] = new ListeElectorale("G", 2500, 0, false);
    // ما تعداد کرسی‌ها را برای هر فهرست محاسبه می‌کنیم
    listes = BlockingObservable.from(electionsMetier.calculerSieges(admin, listes)).first();
    //نتایج تأیید می‌شوند
    Assert.assertEquals(2, listes[0].getSieges());
    Assert.assertFalse(listes[0].isElimine());
    Assert.assertEquals(2, listes[1].getSieges());
    Assert.assertFalse(listes[1].isElimine());
    Assert.assertEquals(1, listes[2].getSieges());
    Assert.assertFalse(listes[2].isElimine());
    Assert.assertEquals(1, listes[3].getSieges());
    Assert.assertFalse(listes[3].isElimine());
    Assert.assertEquals(0, listes[4].getSieges());
    Assert.assertFalse(listes[4].isElimine());
    Assert.assertEquals(0, listes[5].getSieges());
    Assert.assertTrue(listes[5].isElimine());
    Assert.assertEquals(0, listes[6].getSieges());
    Assert.assertTrue(listes[6].isElimine());
  }

  /**
   * vérification 2 : méthode de calcul des sièges on demande les listes à la couche [metier] puis on fixe en dur les
   * voix
   */
  @Test
  public void calculSieges2() {
    //جدول ۷ فهرست نامزدها ایجاد می‌شود
    ListeElectorale[] listes = BlockingObservable.from(electionsMetier.getListesElectorales(admin)).first();
    //آرا به‌صورت کد سخت‌کد شده هستند
    listes[0].setVoix(32000);
    listes[1].setVoix(25000);
    listes[2].setVoix(16000);
    listes[3].setVoix(12000);
    listes[4].setVoix(8000);
    listes[5].setVoix(4500);
    listes[6].setVoix(2500);
    //تعداد کرسی‌های کسب‌شده توسط هر فهرست محاسبه می‌شود
    listes = BlockingObservable.from(electionsMetier.calculerSieges(admin, listes)).first();
    //نتایج بررسی می‌شوند
    Assert.assertEquals(2, listes[0].getSieges());
    Assert.assertFalse(listes[0].isElimine());
    Assert.assertEquals(2, listes[1].getSieges());
    Assert.assertFalse(listes[1].isElimine());
    Assert.assertEquals(1, listes[2].getSieges());
    Assert.assertFalse(listes[2].isElimine());
    Assert.assertEquals(1, listes[3].getSieges());
    Assert.assertFalse(listes[3].isElimine());
    Assert.assertEquals(0, listes[4].getSieges());
    Assert.assertFalse(listes[4].isElimine());
    Assert.assertEquals(0, listes[5].getSieges());
    Assert.assertTrue(listes[5].isElimine());
    Assert.assertEquals(0, listes[6].getSieges());
    Assert.assertTrue(listes[6].isElimine());
  }

  /**
   * vérification 3 méthode de calcul des sièges on provoque une exception
   */
  @Test(expected = ElectionsException.class)
  public void calculSieges3() {
    //جدولی ایجاد می‌شود که شامل ۲۴ فهرست نامزد است، هر کدام با ۱ رأی
    ListeElectorale[] listes = new ListeElectorale[25];
    // تمام ۲۵ فهرست دارای تعداد آرای یکسان (۴ درصد) خواهند بود
    for (int i = 0; i < listes.length; i++) {
      listes[i] = new ListeElectorale("Liste" + (i + 1), 1, 0, false);
    }
    //تخصیص کرسی‌ها – معمولاً این باید منجر به ElectionsException شود
    // با آستانهٔ انتخاباتی ۵ درصد
    BlockingObservable.from(electionsMetier.calculerSieges(admin, listes)).first();
  }

  /**
   * enregistrement des résultats de l'élection
   *
   * @throws JsonProcessingException
   */
  @Test
  public void ecritureResultatsElections() throws JsonProcessingException {
    // ما جدول فهرست‌های هفت نامزد را ایجاد می‌کنیم
    ListeElectorale[] listes = BlockingObservable.from(electionsMetier.getListesElectorales(admin)).first();
    // رأی‌ها به‌صورت کد سخت درج شده‌اند
    listes[0].setVoix(32000);
    listes[1].setVoix(25000);
    listes[2].setVoix(16000);
    listes[3].setVoix(12000);
    listes[4].setVoix(8000);
    listes[5].setVoix(4500);
    listes[6].setVoix(2500);
    // ما تعداد کرسی‌های کسب‌شده توسط هر فهرست را محاسبه می‌کنیم
    listes = BlockingObservable.from(electionsMetier.calculerSieges(admin, listes)).first();
    //نتایج را نمایش دهید
    for (int i = 0; i < listes.length; i++) {
      System.out.println(mapper.writeValueAsString(listes[i]));
    }
    //نتایج در پایگاه داده ذخیره می‌شوند
    BlockingObservable.from(electionsMetier.recordResultats(admin, listes)).firstOrDefault(null);
    //نتایج تأیید می‌شوند
    listes = BlockingObservable.from(electionsMetier.getListesElectorales(admin)).first();
    //نتایج نمایش داده می‌شوند
    for (int i = 0; i < listes.length; i++) {
      System.out.println(mapper.writeValueAsString(listes[i]));
    }
    Assert.assertEquals(2, listes[0].getSieges());
    Assert.assertFalse(listes[0].isElimine());
    Assert.assertEquals(2, listes[1].getSieges());
    Assert.assertFalse(listes[1].isElimine());
    Assert.assertEquals(1, listes[2].getSieges());
    Assert.assertFalse(listes[2].isElimine());
    Assert.assertEquals(1, listes[3].getSieges());
    Assert.assertFalse(listes[3].isElimine());
    Assert.assertEquals(0, listes[4].getSieges());
    Assert.assertFalse(listes[4].isElimine());
    Assert.assertEquals(0, listes[5].getSieges());
    Assert.assertTrue(listes[5].isElimine());
    Assert.assertEquals(0, listes[6].getSieges());
    Assert.assertTrue(listes[6].isElimine());
  }
}

بیایید تغییرات را بررسی کنیم:

  • خط ۴۸: متد استاتیک [BlockingObservable.from(Observable).first]:
    • مشترک پارامتر قابل مشاهده [from] می‌شود؛
    • اجرای کد مرتبط با مشاهده‌پذیر را تحریک می‌کند؛
    • منتظر دریافت اولین نتیجه است. بنابراین این یک عملیات همگام است؛

ما در اینجا از متد [firstOrDefault(null)] استفاده می‌کنیم زیرا مشاهده‌پذیر [metier.authenticate] هنگام اجرا، نتیجه‌ای بر نمی‌گرداند. بنابراین نتیجه متد [firstOrDefault(null)]، مقدار null خواهد بود، مقداری که در اینجا استفاده نمی‌شود؛

ما این الگو را در سراسر باقی کد هر زمان که بخواهیم لایه [métier] را فراخوانی کنیم، تکرار می‌کنیم.

تست واحد [Test01] باید با موفقیت اجرا شود:

 

کار انجام‌دادنی: بررسی کنید که تست [Test01] با موفقیت اجرا شود.


20.1.4.2. Test02

ما در حال اصلاح تست [Test01] هستیم تا اکنون با فراخوانی‌های غیرهم‌زمان به متدهای آن، رابط غیرهم‌زمان [IRxElectionsMetier] را تست کند.

بیایید به یک تست اولیه نگاه کنیم:


  // سِمافور همگام‌سازی رشته‌ها
  private CountDownLatch latch;

  // -----------------------------------
  private ElectionsException checkUserUserException;

  @Test()
  public void checkUserUser() throws InterruptedException {
    // سِمافور روی ۱ تنظیم شده است
    latch = new CountDownLatch((1));
    // عملیات ناهمزمان
    electionsMetier.authenticate(user).subscribeOn(Schedulers.io())
            .subscribe((result) -> {
            },
                    (th) -> {
                      checkUserUserException = (ElectionsException) th;
                      latch.countDown();
                    },
                    () -> {
                      latch.countDown();
                    });
    // انتظار سِمافور
    latch.await();
    //بررسی نتایج
    Assert.assertNotNull(checkUserUserException);
    Assert.assertEquals("403 Forbidden", checkUserUserException.getErreurs().get(0));
}
  • خط ۲: یک سِمافور ابزاری است که برای همگام‌سازی رشته‌ها با یکدیگر استفاده می‌شود. رشته‌ها جریان‌های اجرایی هستند که به‌طور موازی اجرا می‌شوند. برای اجرای یک وظیفه T1، نخ [Thread1] ممکن است به اتمام وظیفه T2، که توسط نخ [Thread2] اجرا می‌شود، نیاز داشته باشد. بنابراین منتظر می‌ماند تا رشته [Thread2] سیگنالی را برای آن ارسال کند که نشان دهد وظیفه T2 به پایان رسیده است. راه‌های مختلفی برای مدیریت این همگام‌سازی بین دو رشته وجود دارد. روش مورد استفاده در اینجا به شرح زیر است:
    • خط ۱۰: نخ [Thread1] یک نیم‌فور با مقدار ۱ ایجاد می‌کند؛
    • خط ۱۲: نخ [Thread1] یک نخ [Thread2] را ایجاد و راه‌اندازی می‌کند. این کار با استفاده از سینتکس زیر انجام می‌شود:

electionsMetier.authenticate(user).subscribeOn(Schedulers.io())

متد [Observable.subscribeOn] نخِ (thread) مورد نظر را که فرآیند مشاهده‌شده روی آن اجرا خواهد شد، مشخص می‌کند. پارامتر برای [subscribeOn] یک استخر نخ است. کتابخانه RxJava چندین مورد از این استخرها را ارائه می‌دهد که برای شرایط مختلف مناسب هستند. استخر [Schedulers.io()] استخر توصیه‌شده برای عملیات شبکه است؛

  • (ادامه)
    • خطوط ۱۲–۱۳: عملیات

electionsMetier.authenticate(user).subscribeOn(Schedulers.io()).subscribe(...)

عملیات همگام را که در [authenticate(user)] قابل مشاهده (observable) محصور شده است، اجرا می‌کند. با این حال، از آنجا که این عملیات همگام بر روی یک تار رشته‌ای متفاوت از تار رشته‌ای [Thread1] اجرا می‌شود، تار رشته‌ای دوم منتظر پاسخ از متد [subscribe] نمی‌ماند و به دستور بعدی می‌رود؛

  • (ادامه)
    • خط ۲۳: نخ [Thread1] متوقف شده و منتظر می‌شود تا سِمافور روی ۰ قرار گیرد (در حال حاضر ۱ است)؛
  • خطوط ۱۳–۲۱: متد [subscribe] سه تابع لامبدا را به‌عنوان پارامتر می‌پذیرد:
    • اولین مورد، [(result)->{...}]، هر بار که observable [authenticate(user)] نتیجه‌ای [result] صادر می‌کند، فراخوانی می‌شود. در اینجا یک observable [authenticate(user)] داریم که کاری انجام می‌دهد اما هیچ نتیجه‌ای صادر نمی‌کند. بنابراین لامبدا [(result)->{}] هرگز فراخوانی نخواهد شد. به همین دلیل کد آن در اینجا خالی است: [{}];
    • دومی، [(th)->{...}]، یک پارامتر از نوع [Throwable] را می‌پذیرد. این تابع زمانی فراخوانی می‌شود که اجرای مشاهده‌پذیر با یک خطا مواجه می‌شود. در اینجا، ما پارامتر [Throwable th] را به شرح زیر مدیریت می‌کنیم:
      • خط ۱۶: آن را در یک فیلد از کلاس تست با نوع [ElectionsException] ذخیره می‌کنیم، زیرا تنها observable در حال اجرا این نوع استثنا را پرتاب می‌کند؛
      • خط ۱۷: برای نشان دادن اینکه نخ [Thread2] کار خود را به پایان رسانده است، سِمافور را روی ۰ تنظیم می‌کنیم؛
    • سومین مورد، [()->{...}]، زمانی فراخوانی می‌شود که مشاهده‌پذیر دیگر عنصری برای ارسال نداشته باشد. ما این رویداد را به شرح زیر مدیریت می‌کنیم:
      • خط ۲۰: ما شمارندهٔ ایمنی را روی ۰ تنظیم می‌کنیم تا نشان دهیم که نخ [Thread2] کار خود را به پایان رسانده است؛

توجه داشته باشید که اگر خطایی رخ دهد، لامبای سوم فراخوانی نمی‌شود. به همین دلیل بود که ما مجبور شدیم در خط 17 نیز سیمفور را روی 0 تنظیم کنیم؛

  • خط ۲۵: وقتی به این خط می‌رسیم، مشاهده‌پذیر کار خود را به پایان رسانده است. سپس می‌توانیم همان بررسی‌های موجود در تست [Test01] را انجام دهیم؛

بیایید تست دیگری را بررسی کنیم:


// -----------------------------------
  private ElectionsException calculSieges1Exception;
  private ListeElectorale[] listesCalculSieges1;

  @Test
  public void calculSieges1() throws InterruptedException {
    // ایجاد آرایهٔ ۷ فهرست نامزد
    ListeElectorale[] listes = new ListeElectorale[7];
    listes[0] = new ListeElectorale("A", 32000, 0, false);
    listes[1] = new ListeElectorale("B", 25000, 0, false);
    listes[2] = new ListeElectorale("C", 16000, 0, false);
    listes[3] = new ListeElectorale("D", 12000, 0, false);
    listes[4] = new ListeElectorale("E", 8000, 0, false);
    listes[5] = new ListeElectorale("F", 4500, 0, false);
    listes[6] = new ListeElectorale("G", 2500, 0, false);
    // سِمافور روی ۱ تنظیم شده است
    latch = new CountDownLatch((1));
    // عملیات ناهمزمان    
    //محاسبه تعداد صندلی‌ها برای هر فهرست
    electionsMetier.calculerSieges(admin, listes).subscribeOn(Schedulers.io())
            .subscribe((result) -> {
              listesCalculSieges1 = result;
            },
                    (th) -> {
                      calculSieges1Exception = (ElectionsException) th;
                      latch.countDown();
                    },
                    () -> {
                      latch.countDown();
                    });
    // انتظار برای نیمافور
    latch.await();
    //نتایج بررسی می‌شوند
    Assert.assertNull(calculSieges1Exception);
    Assert.assertEquals(2, listesCalculSieges1[0].getSieges());
    Assert.assertFalse(listesCalculSieges1[0].isElimine());
    Assert.assertEquals(2, listesCalculSieges1[1].getSieges());
    Assert.assertFalse(listesCalculSieges1[1].isElimine());
    Assert.assertEquals(1, listesCalculSieges1[2].getSieges());
    Assert.assertFalse(listesCalculSieges1[2].isElimine());
    Assert.assertEquals(1, listesCalculSieges1[3].getSieges());
    Assert.assertFalse(listesCalculSieges1[3].isElimine());
    Assert.assertEquals(0, listesCalculSieges1[4].getSieges());
    Assert.assertFalse(listesCalculSieges1[4].isElimine());
    Assert.assertEquals(0, listesCalculSieges1[5].getSieges());
    Assert.assertTrue(listesCalculSieges1[5].isElimine());
    Assert.assertEquals(0, listesCalculSieges1[6].getSieges());
    Assert.assertTrue(listesCalculSieges1[6].isElimine());
  }
  • خطوط ۲۰–۳۰: اجرای ناهمزمان قابل مشاهده [electionsMetier.calculerSieges(admin, listes)];
  • خطوط 21–23: اجرای observable یک نوع [ListeElectorale[]] را بازمی‌گرداند که در یک فیلد از کلاس تست، خط 3، ذخیره می‌شود؛
  • خطوط ۳۴–۴۸: این بررسی‌ها از تست [Test01] گرفته شده‌اند که بررسی خط ۳۴ به آن اضافه شده تا اطمینان حاصل شود که هیچ استثنایی رخ نداده است؛

تست کامل [Test02] در مطالب دوره موجود است.


وظیفه: تست [Test02] را اجرا کنید و بررسی کنید که با موفقیت انجام شده است.


20.1.4.3. Test03

آزمون [Test03] همان کاری را انجام می‌دهد که آزمون [Test01] انجام می‌دهد: این آزمون رابط [IRxElectionsMetier] را با استفاده از فراخوانی‌های همزمان به آن رابط آزمایش می‌کند. این یک نسخهٔ کپی از تست [Test02] است، با دو تفاوت:

  • متغیرهای قابل مشاهده دیگر در رشته‌ای غیر از رشته‌ای که آزمون‌ها را اجرا می‌کند، اجرا نمی‌شوند. هنگامی که نخ [Thread1] متد [subscribe] یک Observable را اجرا می‌کند، این امر یک عملیات HTTP به سمت سرور را نیز در نخ [Thread1] آغاز می‌کند. سپس کل متد [subscribe] همگام می‌شود؛
  • از آنجایی که اکنون تنها یک نخ وجود دارد، همگام‌سازی نخ دیگر لازم نیست و نیم‌صندوق ناپدید می‌شود؛

در اینجا دو مثال آزمایشی آورده شده است:


  // -----------------------------------
  private ElectionsException checkUserUserException;

  @Test()
  public void checkUserUser() throws InterruptedException {
    // عملیات همگام
    electionsMetier.authenticate(user)
            .subscribe((result) -> {
            },
                    (th) -> {
                      checkUserUserException = (ElectionsException) th;
                    },
                    () -> {
                    });
    // تأیید نتایج
    Assert.assertNotNull(checkUserUserException);
    Assert.assertEquals("403 Forbidden", checkUserUserException.getErreurs().get(0));
}
  • خط ۷: به طور پیش‌فرض، متد [electionsMetier.authenticate(user).subscribe] در نخ کد فراخوانی‌کننده اجرا می‌شود. بنابراین این یک عملیات همگام است؛

  // -----------------------------------
  private ElectionsException calculSieges1Exception;
  private ListeElectorale[] listesCalculSieges1;

  @Test
  public void calculSieges1() throws InterruptedException {
    //جدول ۷ فهرست نامزدها ایجاد می‌شود
    ListeElectorale[] listes = new ListeElectorale[7];
    listes[0] = new ListeElectorale("A", 32000, 0, false);
    listes[1] = new ListeElectorale("B", 25000, 0, false);
    listes[2] = new ListeElectorale("C", 16000, 0, false);
    listes[3] = new ListeElectorale("D", 12000, 0, false);
    listes[4] = new ListeElectorale("E", 8000, 0, false);
    listes[5] = new ListeElectorale("F", 4500, 0, false);
    listes[6] = new ListeElectorale("G", 2500, 0, false);
    //عملیات همگام    
    // محاسبه تعداد کرسی‌ها برای هر فهرست
    electionsMetier.calculerSieges(admin, listes)
            .subscribe((result) -> {
              listesCalculSieges1 = result;
            },
                    (th) -> {
                      calculSieges1Exception = (ElectionsException) th;
                    },
                    () -> {
                    });
    // تأیید نتایج
    Assert.assertNull(calculSieges1Exception);
    Assert.assertEquals(2, listesCalculSieges1[0].getSieges());
    Assert.assertFalse(listesCalculSieges1[0].isElimine());
    Assert.assertEquals(2, listesCalculSieges1[1].getSieges());
    Assert.assertFalse(listesCalculSieges1[1].isElimine());
    Assert.assertEquals(1, listesCalculSieges1[2].getSieges());
    Assert.assertFalse(listesCalculSieges1[2].isElimine());
    Assert.assertEquals(1, listesCalculSieges1[3].getSieges());
    Assert.assertFalse(listesCalculSieges1[3].isElimine());
    Assert.assertEquals(0, listesCalculSieges1[4].getSieges());
    Assert.assertFalse(listesCalculSieges1[4].isElimine());
    Assert.assertEquals(0, listesCalculSieges1[5].getSieges());
    Assert.assertTrue(listesCalculSieges1[5].isElimine());
    Assert.assertEquals(0, listesCalculSieges1[6].getSieges());
    Assert.assertTrue(listesCalculSieges1[6].isElimine());
  }

وظیفه: تست [Test03] را اجرا کنید و بررسی کنید که با موفقیت انجام می‌شود.


20.2. مرحله ۲

اکنون برنامه کنسول همگام فصل 17.5 را به برنامه‌ای تبدیل خواهیم کرد که همچنان همگام است اما از رابط ناهمزمان [RxJava, metier, DAO] استفاده می‌کند؛

ما با پروژه [elections-console-metier-dao-security-webjson] [1] از فصل 17.5 شروع می‌کنیم که آن را در پروژه‌ای جدید به نام [elections-console-rxjava- metier-dao-security-webjson] [2] کپی می‌کنیم:

  • به [3-4]؛ در پروژه جدید، وابستگی به لایه همگام قدیمی [métier] را حذف می‌کنیم؛
  • در [5-9]، وابستگی به لایهٔ نوی غیرهمزمان [métier] اضافه شده است؛
  • در [10-14]، کلاس [ElectionsConsole] به [ElectionsConsole01] تغییر نام داده شده است؛

به همین ترتیب، کلاس [BootElectionsConsole] به [BootElectionsConsole01] تغییر نام داده شده است:

 

کد فعلی کلاس [BootElectionsConsole01] به شرح زیر است:


package elections.security.client.boot;

import elections.security.client.console.IElectionsUI;


public class BootElectionsConsole01 extends AbstractBootElections{
    public static void main(String[] arguments) {
        new BootElectionsConsole01().run();
    }

    @Override
    protected IElectionsUI getUI() {
        return ctx.getBean("electionsConsole",IElectionsUI.class);
    }
}
  • خط ۱۳: از آنجا که نام کلاس [ElectionsConsole] به [ElectionsConsole01] تغییر یافته است، اکنون باید بنویسید:

        return ctx.getBean("electionsConsole01",IElectionsUI.class);

بیایید به کد کلاس [ElectionsConsole01] بازگردیم:


@Component
public class ElectionsConsole01 implements IElectionsUI {

    @Autowired
    private IElectionsMetier electionsMetier;

  @Autowired
  private User admin;
  
    @Override
    public void run() {
        //فهرست‌های رقیب
        ListeElectorale[] listes;
        // وارد کردن داده‌ها
        try (Scanner clavier = new Scanner(System.in)) {
         //فهرست‌های رقیب از لایه [metier] درخواست می‌شوند
         listes = electionsMetier.getListesElectorales(admin);
            ...
        //محاسبه تعداد کرسی‌ها
        listes=electionsMetier.calculerSieges(admin,listes);
        //نتایج ثبت می‌شوند
        electionsMetier.recordResultats(admin,listes);
        ...
}

اگر مثال تست [Test01] در بخش 20.1.4.1 را دنبال کنیم، خطوط 5، 17، 20 و 22 به شرح زیر تغییر خواهند کرد:


@Component
public class ElectionsConsole01 implements IElectionsUI {

  @Autowired
  private IRxElectionsMetier electionsMetier;

  @Autowired
  private User admin;

  @Override
  public void run() {
    //فهرست‌های رقیب
    ListeElectorale[] listes;
    //ورود داده‌ها
    try (Scanner clavier = new Scanner(System.in)) {
      //درخواست فهرست‌های رقیب از لایه [metier]
      listes = BlockingObservable.from(electionsMetier.getListesElectorales(admin)).first();
      ...
    // کرسی‌ها محاسبه می‌شوند
    listes = BlockingObservable.from(electionsMetier.calculerSieges(admin, listes)).first();
    //نتایج ثبت می‌شوند
    BlockingObservable.from(electionsMetier.recordResultats(admin, listes));
    ...
  }

مأموریت: پروژه را برای اجرای کلاس [BootElectionsConsole01] با سه پارامتر [SS, Heures travaillées, Jours travaillés] پیکربندی کنید و بررسی کنید که اجرای پروژه با پیکربندی انجام‌شده، نتایج مورد انتظار را تولید می‌کند.



وظیفه: پروژه را برای اجرای جفت [BootElectionsConsole02, ElectionsConsole02] پیکربندی کنید، جایی که کلاس [ElectionsConsole02] بر اساس الگوی تست [Test02] از بخش 20.1.4.2 نوشته شده است.



وظیفه: پروژه را برای اجرای جفت [BootElectionsConsole03, ElectionsConsole03] پیکربندی کنید، جایی که کلاس [ElectionsConsole03] بر اساس الگوی تست [Test03] در بخش 20.1.4.3 نوشته شده است.


20.3. مرحله ۳

اکنون به پورت کردن برنامه Swing به یک محیط غیرهم‌زمان می‌پردازیم.

ما با کپی کردن پروژه [elections-swing-metier-dao-security-webjson] [1] از فصل 17.6 به یک پروژه جدید [elections-swing-rxjava-metier-dao-security-webjson] [2] شروع می‌کنیم:

  • در [3, 4]، وابستگی به لایه همزمان [console] را حذف می‌کنیم؛
  • در [5-9]، وابستگی به لایه کنسول غیرهمزمان اضافه می‌کنیم؛

لایه [swing] فراخوانی‌های واقعاً غیرهمزمان به لایه [métier] انجام خواهد داد. وقتی یک متد در لایه دوم فراخوانی شود، دو نخ وجود خواهد داشت:

  • رشته UI که رویدادها را مدیریت می‌کند؛
  • یک تِرد ورودی/خروجی که فراخوانی HTTP را به سرور اجرا خواهد کرد؛

در طول فراخوانی ناهمزمان، باید یک تصویر بارگذاری و یک دکمه لغو نمایش دهیم. ما این کار را در اینجا انجام نمی‌دهیم؛ این موضوع به عنوان یک بهبود برای برنامه به شما پیشنهاد خواهد شد. تغییرات در دو کلاسی اعمال می‌شوند که فراخوانی‌هایی به لایه [métier] انجام می‌دهند:

 

20.3.1. پیکربندی Maven

در اینجا از کتابخانه [RxSwing] استفاده خواهیم کرد که ویژگی‌هایی را به کتابخانه [RxJava] اضافه می‌کند که فقط در محیط Swing در دسترس هستند. برای این کار، فایل [pom.xml] را به شرح زیر اصلاح می‌کنیم:


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>istia.st.elections</groupId>
  <artifactId>elections-swing-rxjava-metier-dao-security-webjson</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <name>elections-swing-rxjava-metier-dao-security-webjson</name>
  <description>couche swing asynchrone du client web / jSON</description>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <java.version>1.8</java.version>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
  </properties>

  <dependencies>
    <!--RxSwing -->
    <!-- https://mvnrepository.com/artifact/io.reactivex/rxswing -->
    <dependency>
      <groupId>io.reactivex</groupId>
      <artifactId>rxswing</artifactId>
      <version>0.27.0</version>
    </dependency>
    <!-- لایه‌های پایین‌تر -->
    <dependency>
      <groupId>istia.st.elections</groupId>
      <artifactId>elections-console-rxjava-metier-dao-security-webjson</artifactId>
      <version>0.0.1-SNAPSHOT</version>
    </dependency>
  </dependencies>

</project>

20.3.2. کلاس [ElectionsConnectForm]

در یک عملیات غیرهمزمان، کلاس [ElectionsConnectForm] به صورت زیر در می‌آید:


package elections.security.client.swing;

import elections.security.client.console.IElectionsUI;
import elections.security.client.entities.User;

import java.awt.Dimension;
import java.awt.Toolkit;
import javax.swing.SwingUtilities;

import elections.security.client.metier.IRxElectionsMetier;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import rx.schedulers.Schedulers;
import rx.schedulers.SwingScheduler;

@Component
public class ElectionsConnectForm extends AbstractElectionsConnectForm implements IElectionsUI {

  private static final long serialVersionUID = 1L;

  //مرجع لایه غیرهمزمان [métier]
  @Autowired
  private IRxElectionsMetier metier;

  // کاربر واردشده
  private User user;

  // فرم اصلی
  @Autowired
  private ElectionsMainForm electionsMainForm;

  // جلسه UI
  @Autowired
  private UiSession uiSession;

  @Override
  protected void doConnect() {
    if (isPageValid()) {
      //احراز هویت کاربر
      metier.authenticate(user).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance()).subscribe(
        // بدون پاسخ
        (result) -> {
        },
        // پردازش استثنا
        (th) -> {
          // خطا ثبت شد
          String info = getInfoForException("Les erreurs suivantes se sont produites :", th);
          // نمایش اطلاعات
          jTextPaneErreurs.setText(info);
          jTextPaneErreurs.setCaretPosition(0);

        },
        // احراز هویت کامل شد
        () -> {
          // کاربر در جلسه ذخیره می‌شود
          uiSession.setUser(user);
          // نمایه ورود پنهان می‌شود
          setVisible(false);
          //نمای اصلی نمایش داده می‌شود
          electionsMainForm.run();
        });
    }
  }

  // ابتدایی‌سازی‌ها
  @Override
  protected void init() {
    ...
  }

  @Override
  public void run() {
    //رابط کاربری گرافیکی نمایش داده می‌شود
    SwingUtilities.invokeLater(new Runnable() {
      public void run() {
        init();
        setVisible(true);
      }
    });
  }

  private boolean isPageValid() {
    ...
  }

  private String getInfoForException(String message, Throwable ex) {
    ...
  }

}
  • خطوط ۳۶–۶۳: متد [doConnect] زمانی اجرا می‌شود که کاربر گزینه منوی [Connexion] را انتخاب می‌کند:
 

همه چیز در خط ۴۰ است:


      metier.authenticate(user).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance()).subscribe(...)
  • فرآیندی که مشاهده می‌شود [metier.authenticate(user)] است؛
  • این روی یک نخ I/O برداشته‌شده از استخر [Schedulers.io()] اجرا خواهد شد؛
  • این رویداد در نخ UI مشاهده خواهد شد که رویدادها را از رابط Swing [observeOn(SwingScheduler.getInstance())] مدیریت می‌کند. این نخ از طریق متد [SwingScheduler.getInstance()] به دست می‌آید، که در آن [SwingScheduler] کلاسی است که توسط کتابخانه [RxSwing] ارائه می‌شود. این امر الزامی است. هنگامی که نتیجه عملیات غیرهمزمان به دست آمد، اغلب برای تغییر عناصر رابط کاربری Swing استفاده می‌شود. با این حال، رابط کاربری Swing فقط در داخل نخ UI قابل تغییر است؛ در غیر این صورت، یک استثنا پرتاب می‌شود. بنابراین خطوط ۴۱ تا ۶۱ باید در نخ UI اجرا شوند. این امر در اینجا توسط متد [observeOn(SwingScheduler.getInstance())] تضمین می‌شود؛

بیایید دربارهٔ بقیهٔ کد نظر دهیم:

  • خطوط ۴۲–۴۳: این خطوط برای مطابقت با نحوی متد [subscribe] قرار دارند. آنها هرگز اجرا نخواهند شد زیرا فرآیند [metier.authenticate(user)] هیچ نتیجه‌ای بر نمی‌گرداند؛
  • خطوط ۳۵–۵۲: هنگامی که یک استثنا دریافت می‌شود، نمایش داده می‌شود؛
  • خطوط ۵۴–۶۱: زمانی که فرآیند [metier.authenticate(user)] پایان خروجی خود را اعلام می‌کند، اجرا می‌شوند؛

20.3.3. کلاس [ElectionsMainForm]

 

20.3.3.1. ابتدایی‌سازی رابط کاربری گرافیکی


package elections.security.client.swing;

import elections.security.client.console.IElectionsUI;
import elections.security.client.entities.ListeElectorale;
import elections.security.client.entities.User;
import elections.security.client.metier.IRxElectionsMetier;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import rx.schedulers.Schedulers;
import rx.schedulers.SwingScheduler;

import javax.swing.*;
import java.awt.*;
import java.util.ArrayList;
import java.util.List;

@Component
public class ElectionsMainForm extends AbstractElectionsMainForm implements IElectionsUI {

  private static final long serialVersionUID = 1L;

  // اشاره به لایه غیرهمزمان [métier]
  @Autowired
  private IRxElectionsMetier metier;

  // UI جلسه
  @Autowired
  private UiSession uiSession;

  // کاربر وارد شده است
  private User user;

  // قالب‌های لیست JList
  private DefaultListModel<String> modèleNomsVoix = null;
  private DefaultListModel<String> modèleRésultats = null;

  // فهرست‌های رقیب
  private ListeElectorale[] listes;

  // فهرست‌های واردشده توسط کاربر
  private final List<ListeElectorale> listesSaisies = new ArrayList<>();
  private ListeElectorale[] tListesSaisies;

  // ابتدایی‌سازی‌ها
  @Override
  protected void init() {
    //تولید مؤلفه توسط کلاس والد
    super.init();
    //وضعیت فرم
    Utilitaires.setEnabled(new JLabel[]{jLabelAjouter, jLabelCalculer, jLabelEnregistrer, jLabelSupprimer}, false);
    Utilitaires.setEnabled(
            new JMenuItem[]{jMenuItemAjouter, jMenuItemCalculer, jMenuItemEnregistrer, jMenuItemSupprimer}, false);
    // متمرکز کردن پنجره
    Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
    Dimension frameSize = getSize();
    if (frameSize.height > screenSize.height) {
      frameSize.height = screenSize.height;
    }
    if (frameSize.width > screenSize.width) {
      frameSize.width = screenSize.width;
    }
    setLocation((screenSize.width - frameSize.width) / 2, (screenSize.height - frameSize.height) / 2);
    // کاربر وارد شده است
    user = uiSession.getUser();
    // ابتکاری‌های محلی
    modèleNomsVoix = new DefaultListModel<>();
    jListNomsVoix.setModel(modèleNomsVoix);
    modèleRésultats = new DefaultListModel<>();
    jListResultats.setModel(modèleRésultats);
    // فهرست‌ها از لایه [métier] درخواست می‌شوند
    metier.getListesElectorales(user).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance())
            .subscribe(
                    // پاسخ
                    listesElectorales -> {
                      // فهرست‌ها ذخیره شده‌اند
                      listes = listesElectorales;
                    },
                    // استثناء
                    (th) -> showException(th),
                    //پایان قابل مشاهده
                    () -> {
                      // مرحله بعدی
                      doInitStep2();
                    });
  }
...
  • خط ۴۶: متد [init] زمانی اجرا می‌شود که پنجرهٔ مربوطه در آستانهٔ نمایش قرار دارد. هدف آن راه‌اندازی اجزای [1-3] فهرست‌شده در زیر است:
 
  • خطوط ۷۱–۸۵: لیست‌های نامزد به صورت غیرهم‌زمان درخواست می‌شوند (کامپوننت [1]);
  • خط ۷۱: فرآیند مشاهده‌شده، [metier.getListesElectorales(user)] است. این فرآیند روی یک نخ I/O ([subscribeOn(Schedulers.io())]) اجرا می‌شود و روی نخ UI ([observeOn(SwingScheduler.getInstance()]) مشاهده می‌شود؛
  • خطوط ۷۴–۷۷: نتیجه‌ای که توسط فرآیند مشاهده‌شده بازگردانده می‌شود، در فیلد [listes] در خط ۳۸ ذخیره می‌شود؛
  • خط ۷۹: هر استثنا با روش زیر مدیریت می‌شود:

  private void showException(Throwable th) {
    // نمایش خطا
    jTextPaneMessages.setText(getInfoForException("Les erreurs suivantes se sont produites : ", th));
    jTextPaneMessages.setCaretPosition(0);
}
  • خطوط ۸۱–۸۴: در پایان فرآیند مشاهده‌شده، خطوط ۸۱–۸۴ اجرا می‌شوند. این خطوط در صورت وقوع استثنا اجرا نمی‌شوند. متد [doInitStep2] مرحلهٔ ۲ راه‌اندازی را به شرح زیر مدیریت می‌کند:

  private void doInitStep2() {
    // ارتباط نام فهرست‌ها با منوی کشویی jComboBoxNomsListes
    for (int i = 0; i < listes.length; i++) {
      jComboBoxNomsListes.addItem(String.format("%s - %s", listes[i].getId(), listes[i].getNom()));
    }
    // تعداد صندلی‌های قابل پر شدن
    metier.getNbSiegesAPourvoir(user).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance())
            .subscribe(
                    // پاسخ
                    nbSiegesAPourvoir -> {
                      // برچسب مرتبط با این اطلاعات مقداردهی اولیه شده است
                      jLabelSAP.setText(jLabelSAP.getText() + nbSiegesAPourvoir);
                    },
                    // استثناء
                    (th) -> showException(th),
                    // پایان مشاهده‌پذیر
                    () -> {
                      // مرحله بعدی
                      doInitStep3();
                    });
}
  • خطوط ۳–۵: نتیجه مرحله قبلی برای پر کردن لیست کشویی با نام‌های لیست‌های نامزدها استفاده می‌شود؛
  • خطوط ۷–۲۰: تعداد صندلی‌های مورد نیاز به‌صورت ناهمزمان درخواست می‌شود؛
  • خط ۷: فرآیند مشاهده‌شده [metier.getNbSiegesAPourvoir(user)] است. این فرآیند روی یک نخ I/O ([subscribeOn(Schedulers.io())]) اجرا می‌شود و روی نخ‌های UI و [observeOn(SwingScheduler.getInstance()] مشاهده می‌شود؛
  • خطوط ۱۰–۱۳: نتیجه بازگردانده شده توسط فرآیند برای به‌روزرسانی رابط کاربری گرافیکی استفاده می‌شود؛
  • خط ۱۵: هرگونه استثنا نمایش داده می‌شود؛
  • خطوط 17–20: با دریافت سیگنال پایان از مشاهده‌گر، فرآیند به مرحله 3 فرآیند راه‌اندازی می‌رود؛

مرحلهٔ ۳ از فرایند راه‌اندازی توسط کد زیر انجام می‌شود:


  private void doInitStep3() {
    // آستانهٔ انتخاباتی
    metier.getSeuilElectoral(user).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance())
            .subscribe(
                    //پاسخ
                    seuilElectoral -> {
                      // برچسب مرتبط با این اطلاعات مقداردهی اولیه شده است
                      jLabelSE.setText(jLabelSE.getText() + seuilElectoral);
                    },
                    // استثناء
                    (th) -> showException(th),
                    // پایان مشاهده‌پذیر
                    () -> {
                    });
}
  • خطوط ۳–۴: آستانه انتخاباتی به‌صورت ناهمزمان درخواست می‌شود؛
  • خط ۳: فرآیند مشاهده‌شده [metier.getSeuilElectoral(user)] است. این فرآیند روی یک نخ I/O با شناسه [subscribeOn(Schedulers.io())] اجرا می‌شود و روی نخ‌های UI و [observeOn(SwingScheduler.getInstance()] مشاهده می‌شود؛
  • خطوط ۶–۹: نتیجه بازگردانده شده توسط فرآیند برای به‌روزرسانی رابط کاربری گرافیکی استفاده می‌شود؛
  • خط ۱۱: هرگونه استثنا نمایش داده می‌شود؛
  • خطوط ۱۳–۱۴: با دریافت سیگنال پایان از مشاهده‌گر، هیچ اقدامی انجام نمی‌شود: فرآیند راه‌اندازی رابط کاربری گرافیکی کامل شده است؛

20.3.3.2. محاسبه تعداد کرسی‌های کسب‌شده توسط فهرست‌های مختلف

هدف متد [doCalculer] محاسبه تعداد کرسی‌های کسب‌شده توسط فهرست‌های مختلف است:


  @Override
  protected void doCalculer() {
    tListesSaisies = listesSaisies.toArray(new ListeElectorale[0]);
    // محاسبه صندلی‌ها
    String info = null;
    metier.calculerSieges(user, tListesSaisies).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance())
            .subscribe(
                    // پردازش نتیجه
                    result -> consumeResultSieges(result),
                    // مدیریت استثنا
                    th -> showException(th),
                    // پایان قابل مشاهده
                    () -> {
                    }
            );
}
  • خطوط ۶–۱۵: تعداد کرسی‌های به دست آمده توسط فهرست‌های مختلف به صورت غیرهم‌زمان محاسبه می‌شود؛
  • خط ۶: فرآیند مشاهده‌شده [metier.calculerSieges(user, tListesSaisies)] است. این فرآیند روی یک نخ I/O ([subscribeOn(Schedulers.io())]) اجرا می‌شود و روی نخ‌های UI و [observeOn(SwingScheduler.getInstance()] مشاهده می‌شود؛
  • خط ۹: نتیجه‌ای که توسط فرآیند بازگردانده می‌شود توسط متد [consumeResultSieges] استفاده می‌شود؛
  • خط ۱۱: هر استثنا نمایش داده می‌شود؛
  • خطوط ۱۳–۱۴: پس از دریافت سیگنال پایان از مشاهده‌شونده، هیچ اقدامی انجام نمی‌شود؛

خط ۹: متد [consumeResultSieges] از نتیجه بازگردانده‌شده توسط فرآیند مشاهده‌شده استفاده می‌کند، فهرست‌های کاندیدا با فیلدهای [sieges, elimine] به‌روزرسانی‌شده:


  private void consumeResultSieges(ListeElectorale[] tListesSaisies) {
    // نتیجه ذخیره شد
    this.tListesSaisies = tListesSaisies;
    // نمایش نتایج
    modèleRésultats.clear();
    for (int i = 0; i < tListesSaisies.length; i++) {
      modèleRésultats.addElement(tListesSaisies[i].toString());
    }
    // به‌روزرسانی وضعیت فرم
    Utilitaires.setEnabled(new JLabel[]{jLabelEnregistrer}, true);
    Utilitaires.setEnabled(new JLabel[]{jLabelCalculer}, false);
    Utilitaires.setEnabled(new JMenuItem[]{jMenuItemEnregistrer}, true);
    Utilitaires.setEnabled(new JMenuItem[]{jMenuItemCalculer}, false);
    jTextPaneMessages.setText("Calcul terminé");
}
  • خطوط ۴–۱۴: نتیجهٔ به‌دست‌آمده برای به‌روزرسانی رابط کاربری گرافیکی استفاده می‌شود؛

20.3.3.3. ثبت نتایج انتخابات

نتایج انتخابات با استفاده از روش زیر، [doEnregistrer]، ثبت می‌شوند:


  @Override
  protected void doEnregistrer() {
    //درخواست ذخیره در لایه [métier]
    metier.recordResultats(user, tListesSaisies).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance())
            .subscribe(
                    //پردازش نتیجه – در اینجا هیچ‌چیز وجود ندارد
                    (param) -> {
                    },
                    // پردازش استثنا
                    (th) -> showException(th),
                    //پایان قابل مشاهده
                    () -> {
                      // به‌روزرسانی فرم
                      Utilitaires.setEnabled(new JLabel[]{jLabelEnregistrer}, false);
                      Utilitaires.setEnabled(new JMenuItem[]{jMenuItemEnregistrer}, false);
                      jTextPaneMessages.setText("Enregistrement des résultats réalisé");
                    }
            );
}
  • خطوط ۴–۱۷: نتایج انتخابات به‌صورت ناهمزمان ثبت می‌شوند؛
  • خط ۴: فرآیند مشاهده‌شده [metier.recordResultats(user, tListesSaisies)] است. این فرآیند روی یک نخ ورودی/خروجی ([subscribeOn(Schedulers.io())]) اجرا می‌شود و روی نخ UI ([observeOn(SwingScheduler.getInstance()]) مشاهده می‌شود؛
  • خطوط ۷–۸: این خطوط هرگز اجرا نخواهند شد زیرا فرآیند مشاهده‌شده هیچ نتیجه‌ای بازنمی‌گرداند؛
  • خط ۱۰: هر استثنا نمایش داده می‌شود؛
  • خطوط 14–16: پس از دریافت سیگنال پایان از مشاهده‌شونده، رابط کاربری گرافیکی به‌روزرسانی می‌شود؛

وظیفه: بررسی اینکه برنامه Swing کار می‌کند. سپس رابط کاربری گرافیکی و کد را طوری تغییر دهید که در حین یک عملیات ناهمزمان با سرور وب / jSON، یک تصویر بارگذاری همراه با گزینه‌ای برای لغو عملیات جاری نمایش داده شود.