结对编程

组员

黄冠:201421122115
林清荣:201421122102

项目地址

Coding.net:arith-exercise

本次作业完成的题目内容
  1. 实现GUI。
  2. 记录用户的对错总数,程序退出再启动的时候,能把以前的对错数量保存并在此基础上增量计算。
  3. 有计时功能,能显示用户开始答题后的消耗时间。
  4. 界面支持中文简体/中文繁体/英语,用户可以选择一种。

需求分析

GUI
  1. 要好看
  2. 分为开始、答题、结果三个界面
  3. 可以在每次测试前设置数量和范围
  4. 测试完成后显示成绩
记录对错总数
  1. 每次测试完成时,将对错数量和保存在本地的总数量相加,并将相加后的值保存
  2. 将对错总数显示在测试结果面板上
计时功能
  1. 在用户答题时实时更新已用时间
  2. 测试完成后显示所用时间
本地化界面
  1. 通过对话框切换语言
  2. 切换语言后,所有已显示的界面立刻刷新文字,显示切换后的语言
  3. 将用户选择的语言保存在本地

程序设计

GUI

为了对接之前的命令行程序,需要对其做一些修改。因为之前的配置方式是把命令行参数解析为一个Config 对象,然后传入 Creator.create(Config) 中,所以现在可以创建Config 并手动赋参数值,以此来替代之前的命令行输入方式,不需过多修改。使用该函数生成若干表达式及其标准答案后,将表达式列出在答题界面中,最后提交答案时将答案与标准答案比对并得出成绩。

记录对错总数

使用Properties 来实现数据的本地保存和读取。

计时功能

使用独立线程来更新UI,在UI相关的类中需要提供更新文字的接口。

本地化界面

建立Localizable 类,所有需要切换语言的UI类都继承该类(写博客时想了一下,好像并没有必要做成继承啊……),类里提供一个注册语言切换监听的方法

    /**
     * @param resourceBundle 包含各语言文字的资源包的名字
     * @param keys 所需文字的键名
     * @param callback 本地化文字后调用的回调函数
     */
    protected void register(String resourceBundle, String[] keys, Consumer<String[]> callback)

注册一个监听后,需要更新文字时,就会根据当前的Locale 打开对应的resourceBundle,再根据keys 获取对应的文字并传到callback 中。而每当用户切换语言,只需调用 Localizable.localizeAll(Locale) 即可实时通知所有已注册监听的界面,使其更新本地化文字。调用示例:

register("strings", new String[]{"ui.title", "ui.start"}, strings -> {
    mTitle.setText(strings[0]);
    mStartButton.setText(strings[1]);
});

(由于实在太忙了,思维导图就不画了……)

代码展示

记录对错总数 & 计时功能

由另外一位同学完成。

GUI

使用MVC架构,这部分没什么核心代码,就不贴了。

另外使用了一些自定义组件[spoiler] 因为swing默认样式太难看了 [/spoiler]

// 无边框窗口
public class BorderlessFrame extends JFrame {
    private int mLastX = 0;
    private int mLastY = 0;

    public BorderlessFrame() {
        super();
        setUndecorated(true);

        addMouseListener(new MouseAdapter() {
            @Override
            public void mousePressed(MouseEvent e) {
                mLastX = e.getX();
                mLastY = e.getY();
            }
        });

        addMouseMotionListener(new MouseMotionAdapter() {
            @Override
            public void mouseDragged(MouseEvent e) {
                setLocation(e.getXOnScreen() - mLastX, e.getYOnScreen() - mLastY);
            }
        });
    }
}
// 扁平化按钮
public class FlatButton extends JButton { // 非完整版

    public FlatButton(Color normal, Color focused, Color pressed) {
        mNormalColor = normal;
        mFocusedColor = focused;
        mPressedColor = pressed;

        setBorder(null);
        setBorderPainted(false);
        setFocusPainted(false);
        setContentAreaFilled(false);
        setForeground(mNormalTextColor);
    }

    @Override
    protected void paintComponent(Graphics g) {
        Graphics2D g2d = (Graphics2D) g;

        // 自定义绘制背景颜色
        if (mPressed)
            g2d.setColor(mPressedColor);
        else if (mFocused)
            g2d.setColor(mFocusedColor);
        else
            g2d.setColor(mNormalColor);

        g2d.fillRect(0, 0, getWidth(), getHeight());

        super.paintComponent(g);
    }
}
本地化界面
public class Localizable { // 非完整版
    // 所有注册有监听的对象
    private static Set<Localizable> sLocalizables = new HashSet<>();

    public static void localizeAll(Locale locale) {
        sLocalizables.forEach(obj -> obj.localize(locale));
    }

    // 对象内所有通过register注册的监听器
    private ArrayList<LocalizableBundle> mBundles = new ArrayList<>();

    protected void register(String resourceBundle, String[] keys, Consumer<String[]> callback) {
        mBundles.add(new LocalizableBundle(callback, keys, resourceBundle));
        sLocalizables.add(this);
    }

    // 通过Locale来选择文字,并触发所有监听器
    public void localize(Locale locale) {
        if (locale == null)
            locale = Locale.ROOT;

        String curResBundleName = null;
        ResourceBundle resBundle = null;

        for (LocalizableBundle bundle : mBundles) {
            if (bundle.resourceBundleName == null) {
                continue;
            } else if (!bundle.resourceBundleName.equals(curResBundleName)) {
                curResBundleName = bundle.resourceBundleName;
                resBundle = ResourceBundle.getBundle(curResBundleName, locale);
            }

            String[] keys = bundle.keys;
            String[] strings = new String[keys.length];

            for (int i = keys.length - 1; i >= 0; i--)
                try {
                    strings[i] = resBundle.getString(keys[i]);
                } catch (Exception e) {
                    strings[i] = null;
                }

            bundle.callback.accept(strings);
        }
    }

    private static class LocalizableBundle {
        Consumer<String[]> callback;
        String[] keys, resourceBundleName;

        public LocalizableBundle(Consumer<String[]> callback, String[] keys, String resourceBundleName) { /* 略 */ }
    }
}

程序运行

结对过程

因为大家时间都不多,所以经过考虑后,一开始采取了“半”结对编程的方式,先共同设计架构,然后在同一张桌子上用各自的电脑做自己的部分,期间会相互查看进度和一些接口的设计,对接出现问题也会及时解决。到了后期,我的任务先完成了,就开始了正式的结对编程。

整个过程在实验室内完成,只有我们两个人,所以合作的照片就没办法拍了……

小结感受

说实话,就我的感受来说,结对编程真不能说有1+1>2的效果,甚至可能是<2了。

在结对编程中,因为有随时的复审和交流,程序各方面的质量取决于一对程序员中各方面水平较高的那一位。[1]

确实,在我当领航员时,我指出了一些代码上的问题,避免了一些可能的错误;但是另一方面,我们在这上面花的时间远远超过单人完成同样任务的时间了,而这多出来的时间并没有给我们带来其它好处,知识共享也说不上,毕竟领航员在解释问题时提到的知识覆盖面肯定不如驾驶员自己去查资料来得广(当然,前提是自学能力好,不过自学能力也是程序员的标配了)。

我认为结对编程对两人各方面的要求都是相当苛刻的——

首先两人性格要合得来,倔脾气的,不服输的,观点不合的,这些做了还不如不做。

然后两个人水平要相近,如果相差太大的话,水平高的写代码,水平低的可能看不懂,还要花时间去解释;水平低的写代码,水平高的可能每一步都要去纠正,甚至恨不得自己抢过键盘来写。

同时两人水平还不能太低或太高,太低了的话遇到什么问题两个人都不懂,得去查资料,而两个人同时看资料又会遇到这样或那样的麻烦;太高了的话,写个代码都跟hello world似的信手拈来,也没有结对的必要。(或者说要完成的任务不能太难或太简单,同理)

另外,还得有专注力、忍耐力、判断力、等等等等……

最后,也许是最重要的一点,程序员很多都是独来独往的生物啊,参考下这篇文章,可能说出了相当一些人的想法:不要逼我结对编程

所以在我看来,结对编程并不适合所有人、所有情况,也许在特定的情景下会产生很多积极的作用,所以最好还是按照实际情况来安排。

评价合作伙伴

合作过程还是比较顺利的,人也比较谦虚,肯于接受意见,就是积极性稍微欠缺,编程能力也有待加强……不过鉴于他说要去当老板了,就不说什么了……

PSP

PSP2.1 Personal Software Process Stages Time Predicted Time
Planning 计划 5 5
· Estimate 估计这个任务需要多少时间 5 5
Development 开发 420 777
· Analysis 需求分析 (包括学习新技术) 15 15
· Design Spec 生成设计文档 - -
· Design Review 设计复审 - -
· Coding Standard 代码规范 30 -
· Design 具体设计 - -
· Coding 具体编码 200 542
· Code Review 代码复审 30 -
· Test 测试(自我测试,修改代码,提交修改) 145 220
Reporting 报告 100 215
. 测试报告 90 215
. 计算工作量 10 -
. 并提出过程改进计划 10 -

参考

1. 现代软件工程讲义 3 结对编程和两人合作

原文地址:https://www.cnblogs.com/plab/p/7670730.html