Skip to content

20. البرمجة غير المتزامنة مع RxJava

وثيقة للقراءة: [Introduction à RxJava. Application aux environnements Swing et Android.]

في هذا الفصل، نعود إلى الفصل 17.6 حيث قمنا بإنشاء تطبيق عميل/خادم بالبنية التالية:

تؤدي بعض إجراءات المستخدم على واجهة Swing في [1] إلى تشغيل إجراءات في قاعدة البيانات في [3] عبر شبكة HTTP [2]. وبسبب ذلك، قد يستغرق الرد على إجراء المستخدم وقتًا أطول أو أقصر. سيكون من الجيد وضع مؤشر انتظار على واجهة المستخدم مع خيار لإلغاء العملية التي تم تشغيلها إذا استغرقت وقتًا طويلاً. في الفصل 17.6، كل إجراء يقوم به المستخدم ويتطلب تبادل المعلومات مع الخادم هو إجراء متزامن. لا ينتهي معالج الأحداث الذي ينفذه الكود إلا عند استلام الرد. طوال هذا الوقت، تكون الواجهة الرسومية متجمدة: فهي لا تستجيب للإجراءات الجديدة التي يقوم بها المستخدم. يتم ببساطة وضع هذه الإجراءات في قائمة انتظار ليتم معالجتها عند انتهاء مدير الأحداث الذي يتم تنفيذه حاليًا. وبالتالي، إذا تم عرض زر إلغاء، يمكن للمستخدم النقر عليه ولكن لن يحدث شيء ما لم تنته العملية الجارية. عندئذٍ لن يكون لزر الإلغاء أي فائدة.

لكي يكون للنقر على زر الإلغاء أي تأثير، يجب أن تنتهي العملية الجارية. ولذلك، يجب أن تطلق العملية التي قد تستغرق وقتًا طويلاً بشكل غير متزامن:

  • يقوم مدير الأحداث بتشغيل العملية الطويلة ولكنه لا ينتظر نتيجتها ويعيد السيطرة إلى مؤشر الترابط الخاص بـ UI الذي يدير أحداث واجهة المستخدم الرسومية. يتم تشغيل العملية الطويلة على مؤشر ترابط مختلف عن مؤشر ترابط UI، مما لا يؤدي إلى تعطيل هذا الأخير؛
  • إذا نقر المستخدم على زر الإلغاء قبل انتهاء العملية الطويلة، يمكن لسلسلة UI غير المشغولة معالجة هذا الحدث. يمكن عندئذٍ التخلي عن العملية الطويلة بتجاهل نتيجتها؛
  • إذا لم يتم إلغاء العملية الطويلة، فإن وصول الرد سيؤدي إلى حدوث حدث في مؤشر الترابط الخاص بـ UI. وسيقوم هذا المؤشر، إذا كان غير مشغول، بتنفيذ الكود المرتبط بهذا الحدث الذي سيستفيد من الرد؛

ستعمل واجهة المستخدم كما في السابق. إذا كانت أوقات استجابة الخادم سريعة، فلن يلاحظ المستخدم الفرق. إذا كانت ملحوظة، فسيظهر للمستخدم زر إلغاء وسيكون بإمكانه إيقاف العملية الجارية.

تسمح مكتبة [Rx] بالبرمجة غير المتزامنة. تكمن أهميتها الكبيرة في أنها تم تكييفها مع العديد من البيئات (Java، .NET، JS، ...) وأن إتقانها في بيئة ما يمكن نقله بسهولة إلى بيئة أخرى. سنعتمد هنا على الفصل 2 من الوثيقة [Introduction à RxJava. Application aux environnements Swing et Android]. ندعو القارئ إلى قراءتها. وفيما يلي، نستعرض شفرة مأخوذة من الأمثلة الواردة في هذا الفصل.

سنقوم بتطوير بنية التطبيق بالطريقة التالية:

  • في [1]، نقوم بإدراج طبقة [RxJava] بين الطبقة [swing] والطبقة [métier]. سيتم الآن استدعاء أساليب هذه الطبقة بشكل غير متزامن؛

سنقوم بذلك على عدة خطوات:

  • الخطوة 1: تقدم الطبقة [metier, DAO] حاليًا واجهة متزامنة مع الطبقة [ui]. سنقوم بتحويلها إلى طبقة غير متزامنة [RxJava, metier, DAO
  • الخطوة 2: سنقوم بتحويل تطبيق وحدة التحكم المتزامن إلى تطبيق لا يزال متزامنًا ولكنه يستخدم الواجهة غير المتزامنة [RxJava, metier, DAO
  • الخطوة 3: سنقوم بتحويل تطبيق Swing المتزامن إلى تطبيق Swing غير متزامن؛

20.1. الخطوة 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>
    <!-- Spring -->
    <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>
    <!-- Google Guava -->
    <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>
  • الأسطر 65-70: أضفنا التبعية لمكتبة 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<T> الطريقة [subscribe] التي ستحصل على النوع T بشكل غير متزامن. ترتبط بهذه الطريقة ثلاثة أحداث:

  • onSuccess(T result) الذي يُعلم بأن نتيجة من النوع T متاحة. يمكن أن توفر العملية غير المتزامنة عدة نتائج؛
  • onError(Throwable th) الذي يُعلم بأن العملية غير المتزامنة قد واجهت خطأً؛
  • onCompleted() الذي يُعلم بأن العملية غير المتزامنة قد اكتملت؛

ما لم يتم استدعاء الطريقة [Observable.subscribe]، لا يتم تشغيل العملية غير المتزامنة المرتبطة بالمراقب. لا يحصل الكود الذي يستدعي طريقة M من واجهة [IRxElectionsMetier] على النتيجة T المتوقعة، بل يحصل على نوع Observable<T> الذي سيسمح له لاحقًا بالحصول على النتيجة T عن طريق استدعاء الطريقة [Observable.subscribe].

التنفيذ [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) {
    ...
  }
}
  • السطور 12-13: حقن Spring للطبقة التجارية المتزامنة؛
  • الأسطر 20-34: سنعلق على الطريقة [getListesElectorales] التي بدلاً من إرجاع نوع [ListeElectorale[]]، ترجع نوع [Observable<ListeElectorale[]>]؛
  • الأسطر 22-32: تسمح الطريقة الثابتة [Observable.create] بإنشاء Observable من نوع [Subscriber]. يمثل النوع [Subscriber] مشتركًا في تدفقات النتائج التي تنتجها العملية المراقبة (Observable). ويوفر ثلاث طرق:
    • [Subscriber.onNext] (السطر 25) لتلقي نتيجة من العملية المراقبة؛
    • [Subscriber.onError] (السطر 30) لتلقي استثناء من العملية المراقبة. بعد حدوث استثناء، لا يصدر النوع [Observable] أي نتائج؛
    • [Subscriber.onCompleted] (السطر 27) لتلقي إشارة نهاية الإرسال من العملية المراقبة. هنا، لا ترسل العملية المراقبة سوى عنصر واحد. تجدر الإشارة هنا إلى أن هذه الإشارة لا يتم إرسالها في حالة حدوث استثناء. هذا هو السلوك الافتراضي للمراقبات: إرسال استثناء يشير أيضًا إلى نهاية عمليات الإرسال. المشتركون يعرفون ذلك؛
  • السطور 22-34: تقبل الطريقة [Observable.create] كمعلمة نوع [Observable.OnSubscribe]. هذا النوع هو واجهة وظيفية. تم إدخال هذا المفهوم مع Java 8 ويشير إلى واجهة ذات طريقة واحدة. هنا، الطريقة الوحيدة للواجهة [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
  • الأسطر 23-32: الرمز الذي نريد إعطاءه للطريقة [call
  • السطر 25: يتم طلب قوائم الناخبين بشكل متزامن من الطبقة [métier] التي تم إدخالها في السطر 13. وبالتالي سيكون هناك انتظار للنتيجة. وعندما يتم استلامها، يتم تمريرها إلى الطريقة [onNext] للمشترك؛
  • السطر 28: في حالة حدوث خطأ، يتم تمرير الاستثناء إلى طريقة [onError] الخاصة بالمشترك؛
  • السطر 31: ننتظر نتيجة واحدة فقط. وعند الحصول عليها (قوائم الناخبين أو استثناء)، يتم إخطار المشترك بأن العملية المراقبة قد انتهت من إصدار النتائج؛

يجب أن نتذكر جيدًا أن الطريقة [RxElectionsMetier] تُرجع نوع Observable<ListeElectorale[]> وليس النوع ListeElectorale[] نفسه. يجب أن تستدعي الشفرة المستدعية الطريقة Observable<ListeElectorale[]>.subscribe حتى يتم تنفيذ الشفرة في الأسطر 23-33 وتقديم قوائم الناخبين باستخدام السطر 25.

الكود الخاص بالطرق الأخرى مشابه:


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);
      }
    });
  }
}
  • السطران 20 و 81: لا يتم استدعاء الطريقة [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() {
    // يتم إنشاء جدول يضم 24 قائمة مرشحة، لكل منها صوت واحد
    ListeElectorale[] listes = new ListeElectorale[25];
    // ستحصل القوائم الـ 25 على نفس عدد الأصوات (4%)
    for (int i = 0; i < listes.length; i++) {
      listes[i] = new ListeElectorale("Liste" + (i + 1), 1, 0, false);
    }
    // حساب المقاعد - عادةً يجب أن يكون لدينا ElectionsException
    // مع عتبة انتخابية تبلغ 5%
    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());
  }
}

لنستعرض التعديلات:

  • السطر 48: الطريقة الثابتة [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 {
    // إشارة عند 1
    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));
}
  • السطر 2: الإشارة (semaphore) هي أداة تُستخدم لمزامنة الخيوط (threads) فيما بينها. الخيوط هي تدفقات تنفيذية تعمل بالتوازي. لتنفيذ مهمة T1، قد يحتاج الخيط [Thread1] إلى انتهاء مهمة T2 التي ينفذها الخيط [Thread2]. في هذه الحالة، ينتظر حتى يرسل له مؤشر الترابط [Thread2] إشارة تشير إلى انتهاء المهمة T2. هناك طرق مختلفة لإدارة هذه التزامن بين مؤشري الترابط. الطريقة المستخدمة هنا هي التالية؛
    • السطر 10: يقوم الخيط [Thread1] بإنشاء إشارة مع القيمة 1؛
    • السطر 12: يقوم الخيط [Thread1] بإنشاء وتشغيل خيط [Thread2]. ويتم ذلك من خلال الصيغة:

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

تحدد الطريقة [Observable.subscribeOn] الخيط الذي سيتم تنفيذ العملية المراقبة عليه. معلمة [subscribeOn] هي مجموعة من الخيوط. توفر مكتبة RxJava العديد منها لتناسب المواقف المختلفة. مجموعة [Schedulers.io()] هي المجموعة الموصى بها لعمليات الشبكة؛

  • (تابع)
    • السطران 12-13: العملية

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

العملية المتزامنة المُغلفة في المراقب [authenticate(user)]. ولكن نظرًا لأن هذه العملية المتزامنة يتم تشغيلها على مؤشر ترابط مختلف عن مؤشر الترابط [Thread1]، فإن هذا الأخير لا ينتظر استجابة الأسلوب [subscribe] وينتقل إلى التعليمات التالية؛

  • (تابع)
    • السطر 23: يتوقف الخيط [Thread1] وينتظر حتى يتحول السمافور إلى 0 (وهو عند 1 في الوقت الحالي)؛
  • الأسطر 13-21: تقبل الطريقة [subscribe] ثلاث دوال لامدا كمعلمات:
    • يتم استدعاء الأولى [(result)->{...}] في كل مرة يصدر فيها المراقب [authenticate(user)] نتيجة [result]. هنا لدينا مراقب [authenticate(user)] يقوم بشيء ما ولكنه لا يصدر أي نتيجة. لذلك لن يتم استدعاء الدالة لامدا [(result)->{}] أبدًا. ولهذا السبب فإن كودها فارغ هنا [{}]؛
    • أما الثانية [(th)->{...}] فتتلقى كمعلمة نوع [Throwable]. يتم استدعاؤها عندما يواجه تنفيذ المراقب استثناءً. هنا، نعالج المعلمة [Throwable th] بالطريقة التالية:
      • السطر 16: نقوم بتخزينه في حقل من فئة الاختبار من النوع [ElectionsException] لأن المراقب المنفذ لا يصدر سوى هذا النوع من الاستثناءات؛
      • السطر 17: نغير قيمة السمافور إلى 0 للإشارة إلى أن مؤشر الترابط [Thread2] قد أنهى عمله؛
    • يتم استدعاء الثالث [()->{...}] عندما لا يكون لدى المراقب عناصر أخرى لإصدارها. نعالج هذا الحدث بالطريقة التالية:
      • السطر 20: نضبط السمافور على 0 للإشارة إلى أن الخيط [Thread2] قد أنهى عمله؛

تجدر الإشارة إلى أن الدالة lambda الثالثة لا يتم استدعاؤها في حالة حدوث استثناء. ولهذا السبب، اضطررنا إلى تعيين الإشارة إلى 0 أيضًا في السطر 17؛

  • السطر 25: عند الوصول إلى هذا السطر، يكون المراقب قد أنهى عمله. يمكننا عندئذ إجراء نفس عمليات التحقق كما في الاختبار [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);
    // إشارة إلى 1
    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());
  }
  • الأسطر 20-30: التنفيذ غير المتزامن للمراقب [electionsMetier.calculerSieges(admin, listes)]؛
  • الأسطر 21-23: ينتج عن تنفيذ المراقب نوع [ListeElectorale[]] يتم تخزينه في حقل من فئة الاختبار، السطر 3؛
  • الأسطر 34-48: هذه الفحوصات هي تلك الخاصة باختبار [Test01] التي أضيفت إليها فحص السطر 34 الذي يضمن عدم وجود استثناء؛

الاختبار [Test02] متاح بالكامل في مواد الدورة التدريبية.


المهمة المطلوبة: إجراء الاختبار [Test02] والتحقق من نجاحه.


20.1.4.3. Test03

يقوم الاختبار [Test03] بنفس ما يقوم به الاختبار [Test01]: فهو يختبر الواجهة [IRxElectionsMetier] من خلال استدعاءات متزامنة لهذه الواجهة. وهو نسخة من الاختبار [Test02] مع اختلافين بسيطين:

  • لم تعد العناصر القابلة للمراقبة تُنفَّذ في مؤشر ترابط مختلف عن المؤشر الذي ينفِّذ الاختبارات. عندما يقوم مؤشر الترابط [Thread1] بتنفيذ الطريقة [subscribe] لعنصر قابل للمراقبة، فإن هذه الطريقة تبدأ عملية 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));
}
  • السطر 7: بشكل افتراضي، يتم تنفيذ الطريقة [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. الخطوة 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);
    }
}
  • السطر 13: نظرًا لتغيير اسم الفئة [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. الخطوة 3

ننتقل الآن إلى نقل تطبيق Swing إلى بيئة غير متزامنة.

نبدأ بنسخ المشروع [elections-swing-metier-dao-security-webjson] [1] من الفصل 17.6، إلى مشروع جديد [elections-swing-rxjava-metier-dao-security-webjson] [2]:

  • في [3, 4]، نزيل التبعية على الطبقة المتزامنة [console
  • في [5-9]، نضيف تبعية للطبقة غير المتزامنة console؛

ستقوم الطبقة [swing] بإجراء مكالمات غير متزامنة حقيقية إلى الطبقة [métier]. عند استدعاء إحدى طرقها، سيكون هناك خيطان:

  • خيط UI، وهو الذي يدير الأحداث؛
  • خيط E/S الذي سيقوم بتنفيذ استدعاء 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) {
    ...
  }

}
  • الأسطر 36-63: يتم تنفيذ الطريقة [doConnect] عندما يضغط المستخدم على خيار القائمة [Connexion]:
 

كل شيء موجود في السطر 40:


      metier.authenticate(user).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance()).subscribe(...)
  • العملية الملاحظة هي [metier.authenticate(user)]؛
  • وسيتم تنفيذها على مؤشر ترابط E/S مأخوذ من المجموعة [Schedulers.io()]؛
  • سيتم ملاحظتها في مؤشر ترابط UI، وهو الذي يدير أحداث واجهة Swing [observeOn(SwingScheduler.getInstance())]. يتم الحصول على هذا الخيط من خلال الطريقة [SwingScheduler.getInstance()] حيث [SwingScheduler] هي فئة مقدمة من المكتبة [RxSwing]. وهذا أمر إلزامي. عند الحصول على نتيجة العملية غير المتزامنة، غالبًا ما يتم استخدامها لتعديل عناصر واجهة Swing. لكن لا يمكن تعديل هذه الواجهة إلا في مؤشر الترابط الخاص بـ UI، وإلا فسيحدث استثناء. لذلك يجب أن يتم تنفيذ الأسطر 41-61 في مؤشر ترابط UI. ويتم ضمان ذلك هنا بواسطة الطريقة [observeOn(SwingScheduler.getInstance())]؛

لنشرح بقية الكود:

  • السطور 42-43: توجد هذه السطور لالتزام بصيغة الأسلوب [subscribe]. ولن يتم تنفيذها أبدًا لأن العملية [metier.authenticate(user)] لا تُرجع أي نتيجة؛
  • السطور 35-52: عند استلام استثناء، يتم عرضه؛
  • السطور 54-61: يتم تنفيذها عندما تشير العملية [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();
                    });
  }
...
  • السطر 46: يتم تنفيذ الطريقة [init] عند عرض النافذة المرتبطة بها. والغرض منها هو تهيئة المكونات [1-3] التالية:
 
  • السطور 71-85: يتم طلب قوائم المرشحين بشكل غير متزامن (المكون [1])؛
  • السطر 71: العملية المراقبة هي [metier.getListesElectorales(user)]. يتم تنفيذها على مؤشر ترابط الإدخال/الإخراج [subscribeOn(Schedulers.io())] ومراقبتها على مؤشر ترابط UI [observeOn(SwingScheduler.getInstance()]؛
  • الأسطر 74-77: يتم تخزين النتيجة التي ترجعها العملية المراقبة في الحقل [listes] في السطر 38؛
  • السطر 79: تتم معالجة الاستثناء المحتمل بالطريقة التالية:

  private void showException(Throwable th) {
    // عرض الاستثناء
    jTextPaneMessages.setText(getInfoForException("Les erreurs suivantes se sont produites : ", th));
    jTextPaneMessages.setCaretPosition(0);
}
  • الأسطر 81-84: في نهاية العملية المراقبة، يتم تنفيذ الأسطر 81-84. لا يتم تنفيذ هذه الأسطر في حالة وجود استثناء. تضمن الطريقة [doInitStep2] الخطوة 2 من التهيئة بالطريقة التالية:

  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();
                    });
}
  • الأسطر 3-5: يتم استخدام نتيجة الخطوة السابقة لملء القائمة المنسدلة بأسماء القوائم المرشحة؛
  • الأسطر 7-20: يتم طلب عدد المقاعد الشاغرة بشكل غير متزامن؛
  • السطر 7: العملية المراقبة هي [metier.getNbSiegesAPourvoir(user)]. يتم تنفيذها على مؤشر ترابط E/S [subscribeOn(Schedulers.io())] ومراقبتها على مؤشر ترابط UI [observeOn(SwingScheduler.getInstance()]؛
  • الأسطر 10-13: يتم استخدام النتيجة التي ترسلها العملية لتحديث واجهة المستخدم الرسومية؛
  • السطر 15: يتم عرض الاستثناء المحتمل؛
  • الأسطر 17-20: عند استلام إشارة نهاية المراقبة، ننتقل إلى الخطوة 3 من عملية التهيئة؛

يتم تنفيذ الخطوة 3 من التهيئة بواسطة الكود التالي:


  private void doInitStep3() {
    // الحد الأدنى الانتخابي
    metier.getSeuilElectoral(user).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance())
            .subscribe(
                    // الرد
                    seuilElectoral -> {
                      // يتم تهيئة التسمية المرتبطة بهذه المعلومات
                      jLabelSE.setText(jLabelSE.getText() + seuilElectoral);
                    },
                    // استثناء
                    (th) -> showException(th),
                    // نهاية قابلة للملاحظة
                    () -> {
                    });
}
  • السطران 3-4: يتم طلب عتبة التصويت بشكل غير متزامن؛
  • السطر 3: العملية المراقبة هي [metier.getSeuilElectoral(user)]. يتم تنفيذها على مؤشر ترابط الإدخال/الإخراج [subscribeOn(Schedulers.io())] ومراقبتها على مؤشر ترابط UI [observeOn(SwingScheduler.getInstance()]؛
  • الأسطر 6-9: يتم استخدام النتيجة التي ترسلها العملية لتحديث واجهة المستخدم الرسومية؛
  • السطر 11: يتم عرض الاستثناء المحتمل؛
  • السطور 13-14: عند استلام إشارة نهاية المراقبة، لا يتم القيام بأي شيء: عملية تهيئة واجهة المستخدم الرسومية قد اكتملت؛

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),
                    // نهاية قابلة للملاحظة
                    () -> {
                    }
            );
}
  • الأسطر 6-15: يتم حساب المقاعد التي حصلت عليها القوائم المختلفة بشكل غير متزامن؛
  • السطر 6: العملية المراقبة هي [metier.calculerSieges(user, tListesSaisies)]. يتم تنفيذها على مؤشر ترابط الإدخال/الإخراج [subscribeOn(Schedulers.io())] ومراقبتها على مؤشر ترابط UI [observeOn(SwingScheduler.getInstance()]؛
  • السطر 9: يتم استخدام النتيجة التي ترجعها العملية بواسطة الطريقة [consumeResultSieges
  • السطر 11: يتم عرض الاستثناء المحتمل؛
  • السطران 13-14: عند استلام إشارة نهاية المراقبة، لا يتم القيام بأي شيء؛

السطر 9، تستخدم الطريقة [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é");
}
  • السطور 4-14: يتم استخدام النتيجة التي تم الحصول عليها لتحديث واجهة المستخدم الرسومية؛

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é");
                    }
            );
}
  • الأسطر 4-17: يتم تسجيل نتائج الانتخابات بشكل غير متزامن؛
  • السطر 4: العملية المراقبة هي [metier.recordResultats(user, tListesSaisies)]. يتم تنفيذها على مؤشر ترابط E/S [subscribeOn(Schedulers.io())] ومراقبتها على مؤشر ترابط UI [observeOn(SwingScheduler.getInstance()]؛
  • السطران 7-8: لن يتم تنفيذ هذين السطرين أبدًا لأن العملية المراقبة لا تعطي أي نتيجة؛
  • السطر 10: يتم عرض الاستثناء المحتمل؛
  • السطور 14-16: عند استلام إشارة نهاية المراقبة، يتم تحديث الواجهة الرسومية؛

المهمة المطلوبة: تحقق من أن تطبيق Swing يعمل. ثم قم بتطوير الواجهة الرسومية والكود بحيث تظهر صورة انتظار وخيار لإلغاء العملية الجارية عند إجراء عملية غير متزامنة مع خادم الويب / jSON.