Skip to content

9. [TD]:使用控制台程序实现 [ui] 层

关键词:多层架构、Spring、依赖注入。

9.1. Support

在 [1] 中,文件夹 [support / chap-09] 包含控制台应用程序 [UI] 层的 Eclipse 项目。

9.2. Maven 配置

Eclipse 项目 [elections-ui-metier-dao-jdbc] 由以下 Maven 文件 [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-ui-metier-dao-jdbc</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>elections-ui-metier-dao-jdbc</name>

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

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

    <dependencies>
        <!-- 业务 -->
        <dependency>
            <groupId>istia.st.elections</groupId>
            <artifactId>elections-metier-dao-jdbc</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
        <!-- Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- Spring Boot 测试 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <!-- 插件 -->
    <build>
        <plugins>
            <plugin>
                <artifactId>maven-assembly-plugin</artifactId>
                <configuration>
                    <archive>
                        <manifest>
                            <mainClass>config.AppConfig</mainClass>
                        </manifest>
                    </archive>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                </configuration>
            </plugin>
            <!-- 用于将项目工件安装到本地 Maven 仓库 -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>
</project>
  • 第 22-26 行:导入 [métier] 层的归档文件,并级联导入 [DAO] 层的归档文件;

9.3. Spring 配置

  

类 [UiConfig] 配置 Spring 应用程序:


package elections.ui.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;

import elections.metier.config.MetierConfig;

@Import(MetierConfig.class)
@ComponentScan(basePackages = { "elections.ui.service" })
public class UiConfig {
}
  • 第 8 行:导入 [métier] 层中定义的 Bean。该层此前已导入 [DAO] 层中定义的 Bean。因此,此处可访问这三个层中的 Bean;
  • 第 9 行:[elections.ui.service] 包包含其他 Bean;

9.4. [UI] 层的接口

  

要理解 [UI] 层的 Java 接口可能是什么,我们需要知道谁将使用该接口。这并非指上图中的用户,而是指将启动整个应用程序的程序。

[IElectionsUI] 接口向将启动应用程序的主程序 [main] 提供服务。 [main] 可以向 [ui] 层提出什么请求?它可以要求该层开始与用户进行交互,以便用户输入选举中缺失的数据。我们将采用以下最小接口:


package istia.st.elections.ui;

public interface IElectionsUI {
    /**
     * lance le dialogue avec l'utilisateur
     */
    public void run();
}
  • 第 7 行:该接口仅有一个方法:run。调用此方法即要求 [ui] 层开始与用户进行交互。

9.5. 应用程序的启动类

  

应用程序的启动类是一个包含静态方法 [main] 的 Java 类。 该方法需创建 [ui, metier, demo] 层的实例,并要求 [ui] 层开始与用户对话。该类可以是以下 [AbstractBootElections] 类:


package elections.ui.boot;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import elections.dao.entities.ElectionsException;
import elections.ui.config.UiConfig;
import elections.ui.service.IElectionsUI;

public abstract class AbstractBootElections {

    // 获取 Spring 上下文
    protected AnnotationConfigApplicationContext ctx;

    public void run() {
        // 实例化 [ui] 层
        IElectionsUI electionsUI = null;
        try {
            // 获取 Spring 上下文
            ctx = new AnnotationConfigApplicationContext(UiConfig.class);
            // 加载 [ui] 层
            electionsUI = getUI();
        } catch (RuntimeException ex) {
            // 报告错误
            afficheExceptions("Les erreurs suivantes se sont produites :", ex);
            // 停止应用程序
            System.exit(1);
        }
        // 执行 [ui] 层
        try {
            electionsUI.run();
        } catch (ElectionsException ex2) {
            // 报告错误
            afficheExceptions("Les erreurs suivantes se sont produites :", ex2);
            // 停止应用程序
            System.exit(3);
        } catch (RuntimeException ex1) {
            // 报告错误
            afficheExceptions("Les erreurs suivantes se sont produites :", ex1);
            // 停止应用程序
            System.exit(2);
        }
    }

    protected abstract IElectionsUI getUI();

    private void afficheExceptions(String message, ElectionsException ex) {
        // 显示消息
        System.out.println(String.format("%s -------------", message));
        System.out.println(String.format("Code erreur : %d", ex.getCode()));
        // 显示错误
        for (String erreur : ex.getErreurs()) {
            System.out.println(String.format("-- %s", erreur));
        }

    }

    public void afficheExceptions(String message, Exception ex) {
        // 显示消息
        System.out.println(String.format("%s -------------", message));
        // 显示异常堆栈
        Throwable cause = ex;
        while (cause != null) {
            System.out.println(String.format("-- %s", cause.getMessage()));
            cause = cause.getCause();
        }
    }
}
  • 第 19 行:实例化 Spring 上下文:将创建各个配置文件中定义的所有 Bean;
  • 第 21 行:获取实现 [IElectionsUI] 接口的 Bean 的引用。我们将通过两个 Bean 来实现 [IElectionsUI] 接口:
    • [ElectionsConsole] 用于控制台应用程序;
    • [ElectionsSwing] 用于 Swing 应用程序;

方法 [getUI] 是抽象方法(第 44 行)。实际上,类 [AbstractBootElections] 将作为两个类的父类:

  • [BootElectionsConsole] 将向其父类提供 Bean [ElectionsConsole];
  • [BootElectionsSwing],它将向其父类提供 Bean [ElectionsSwing];
  • 第 30 行:接口中的 [run] 方法被调用;

异常在多个位置被处理:

  • 第 22-27 行:在实例化 Spring 上下文时可能发生异常;
  • 第 31-41 行:在执行 [UI] 层时可能发生异常。异常分为两类:
    • 由 [DAO] 层抛出的 [ElectionsException] 类;
    • 类 [RuntimeException] 用于处理运行过程中可能出现的其他异常;

类 [BootElectionsConsole] 是启动控制台实现的类。其代码如下:


package elections.ui.boot;

import elections.ui.service.IElectionsUI;

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

    @Override
    protected IElectionsUI getUI() {
        return ctx.getBean("electionsConsole",IElectionsUI.class);
    }
}
  • 第 5 行,类 [BootElectionsConsole] 继承自类 [AbstractBootElections]。因此,它必须实现其父类声明为抽象的方法 [getUI];
  • 第10-13行:实现方法[getUI];
  • 第 12 行:声明名为 [electionsConsole] 的 Bean,该 Bean 实现了 [IElectionsUI] 接口。[ctx] 是父类中通过以下声明定义的 Spring 上下文:

    // 获取 Spring 上下文
    protected AnnotationConfigApplicationContext ctx;

由于该字段具有 [protected] 属性,因此可在子类中访问。

第 12 行中的 Bean 将按以下方式声明:


@Component
public class ElectionsConsole implements IElectionsUI {

该 Bean 的默认名称是类名的首字母小写形式。可以通过显式编写以下内容来覆盖此默认名称:

@Component(nom_du_bean_entre_guillemets)

调用 Swing 实现的 [BootElectionsSwing] 类可能如下所示:


package elections.ui.boot;

import elections.ui.service.IElectionsUI;

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

    @Override
    protected IElectionsUI getUI() {
        return ctx.getBean("electionsSwing", IElectionsUI.class);
    }
}

这种设计模式将各类共有的行为提取到父类中,并让子类实现各自特有的细节,称为 Strategy 设计模式。该设计模式要求所有子类都遵循共同的行为规范。

9.6. 实现类 [ElectionsConsole]

  

我们对 [ui] 层的首个实现类将是一个使用控制台与用户交互的类。以下是在 Eclipse 控制台上获得的对话示例:


Il y a 7 listes en compétition. Veuillez indiquer le nombre de voix de chacune d'elles :
Nombre de voix de la liste [A] : 2500
Nombre de voix de la liste [B] : 4500
Nombre de voix de la liste [C] : x
Nombre de voix incorrect. Veuillez recommencer
Nombre de voix de la liste [C] : 8000
Nombre de voix de la liste [D] : 12000
Nombre de voix de la liste [E] : 16000
Nombre de voix de la liste [F] : 25000
Nombre de voix de la liste [G] : 32000

Résultats de l'élection

[G,32000,2,false]
[F,25000,2,false]
[E,16000,1,false]
[D,12000,1,false]
[C,8000,0,false]
[B,4500,0,true]
[A,2500,0,true]

[ElectionsConsole] 类的骨架可能如下所示:


package elections.ui.service;

import java.util.Comparator;
import java.util.Scanner;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import elections.dao.entities.ListeElectorale;
import elections.metier.service.IElectionsMetier;

@Component
public class ElectionsConsole implements IElectionsUI {

    @Autowired
    private IElectionsMetier electionsMetier;

    @Override
    public void run() {
        // 数据输入
        try (Scanner clavier = new Scanner(System.in)) {
            // 向 [metier] 层请求竞争列表

            // 进行选票录入

        }
        // 计算席位

        // 记录结果

        // 按得票数从高到低排序候选名单

        // 显示结果
    }

    // 选票名单比较类
    class CompareListesElectorales implements Comparator<ListeElectorale> {

        // 根据得票数比较两个候选名单
        @Override
        public int compare(ListeElectorale listeElectorale1, ListeElectorale listeElectorale2) {
            // 比较这两个候选名单的得票数
            int nbVoix1 = listeElectorale1.getVoix();
            int nbVoix2 = listeElectorale2.getVoix();
            if (nbVoix1 < nbVoix2) {
                return +1;
            } else {
                if (nbVoix1 > nbVoix2)
                    return -1;
                else
                    return 0;
            }
        }
    }

}
  • 第 12 行:类 [ElectionsConsole] 是 Spring 组件;
  • 第 13 行:该类实现了 [IElectionsUI] 接口
  • 第 15-16 行:Spring 向 [metier] 层注入了一个引用;
  • 第18-34行:接口[IelectionsUI]中的方法[run];

第21-28行的try名为try-with-resources,其语法如下:

1
2
3
try(ressource){
}

第 1 行的资源必须实现 [java.lang.AutoCloseable] 接口。该资源在第 1 行打开,并在第 3 行之后自动关闭,无论在这两行之间执行的代码中是否发生异常。这种语法确保了打开的资源无论发生什么情况都会被正确关闭。


待完成任务:编写方法 [run] 的代码。请参考注释。