一.前言
这学期第一次接触Java中的面向对象程序设计,对于其中的类设计较为陌生,在类设计中有许多不规范的地方。
在类设计中有许多要遵循的原则,例如:单一职责原则,开闭原则,里氏代换原则,依赖倒转原则,接口隔离原则,迪米特法则等。
在三次PTA作业的类设计的要求中主要遵循单一职责原则和迪米特法则,但在三次PTA作业中存在一些不符合以上设计原则的地方,以下是我对三次大作业的一些总结。

二.
第一次作业:

1.第一次作业内容的大概内容是:输入试卷中试题的信息(编号、题目、标准答案)
输出试题每一题的答题信息以及最后每一题的判题结果。

一开始看到这道题时,觉得题目内容很多难懂,琢磨了很久才大概理解题目意思。
依据题目要求,我设计了三个类:
Question类:用于储存题目的题号,题目内容和题目答案
Paper类: 用于储存试卷编号和用数组储存的Question对象
Answer类: 用于储存答卷信息,其中每一题的答案和答案的正误判断都用数组储存,
还有判断答案正误的方法和输出答卷结果的方法。

2.这次作业的类图如下:

3.遇到的问题
在这次作业中由于第一次使用正则表达式,在写正则表达式中遇到了许多问题,自己写的正则表达式不能正确匹配字符串的问题。
4.缺点
虽然我的类间关系简单,但是没有严格满足类的单一性职责。例如,在Answer类中的print ProblemAnswer()方法,需要再创建一个类来执行这个方法;
5.改进

点击查看代码
class Answer{
    Paper paper;
    private String[] myAnswers;
    private boolean[] flags;
    public Answer() {
    }
    public Answer(int num) {
        paper = new Paper(num);
        myAnswers = new String[paper.getNum()];
        flags = new boolean[paper.getNum()];
    }
    public void mach() {
        int i = 0;
        Problem[] p = paper.getProblems();
        for (i = 0; i < paper.getNum(); i++) {
            if (p[i].getAnswer().equals(myAnswers[i])) {
                flags[i] = true;
            }
            else {
                flags[i] = false;
            }
        }
    }
    public void printProblemAnswer() {
        int i = 0;
        Problem[] p = paper.getProblems();
        for (i = 0; i < paper.getNum(); i++) {
            System.out.println(p[i].getTitle() + "~" + myAnswers[i]);
        }
        for (i = 0; i < paper.getNum(); i++) {
            if (i < paper.getNum() - 1) {
                System.out.print(flags[i] + " ");
            }
            else if (i == paper.getNum() - 1) {
                System.out.print(flags[i]);
            }
        }
    }
    public void setMyAnswers(String[] myAnswer) {
        myAnswers = myAnswer;
    }
    public String[] getMyAnswers() {
        return myAnswers;
    }
在以上类中需要将储存每一道题答案的数组改为储存答案的对象,再创建储存单个答案的Answer类,在答卷类中创建多个Answer对象。 还有将答卷类中的 printProblemAnswer()方法分割到专门执行输出结果的类中。

三.
第二次作业:
1.第二次作业在第一次作业的的基础上添加了多张试卷和多张答卷。
在这次作业中我用集合代替数组,目的是避免数组越界,也方便进行数据输入

依据题目要求,我设计以下四个类:
Question类:用于储存题目的题号,题目内容和题目答案
Paper类: 用于储存试卷编号和用数组储存的Question对象
Answer类: 用于储存答卷信息,其中每一题的答案和答案的正误判断都用数组储存,
AnswerList类:用于储存答卷信息和答卷题号
2.类图如下:

2.遇到的问题:
编写此次代码时,遇到难点是如何正确地将打乱试卷题目信息和答卷信息输入到个自的对象中。

3.缺点:
一下是主方法:

点击查看代码
public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        String[] problems = new String[100];
        String[] papers = new String[100];
        String[] myAnswers = new String[100];
        ArrayList<Paper> paperArrayList = new ArrayList<>();//所有试卷的集合
        ArrayList<AnswerList> paperAnswer = new ArrayList<>();
        int i = 0, pr = 0, pa = 0, my = 0;
        for (i = 0; i < 100; i++) {
            String str = sc.nextLine();
            if (str.startsWith("#N:")) {problems[pr] = str;pr++;}
            else if (str.startsWith("#T:")) {papers[pa] = str;pa++;}
            else if (str.startsWith("#S:")) {myAnswers[my] = str;my++;}
            else if (str.equals("end")) {break;}
        }
        Pattern p1 = Pattern.compile("#T:\\s*([1-9]\\d*) (.*)");
        Pattern p2 = Pattern.compile("([1-9]\\d*)-([1-9]\\d*)");
        Pattern p3 = Pattern.compile("#N:\\s*?(\\S+)\\s*?#Q:\\s*?(.+?)\\s*?#A:\\s*?([\\S ]+)\\s*?");
        Pattern p4 = Pattern.compile("#S:\\s*([1-9]\\d*)");
        Pattern p5 = Pattern.compile("#A:\\s*(\\S+)");


        for (i = 0; i < pa; i++) {
            int n = 0;
            int[] problemNum = new int[100];
            int[] score = new int[100];
            String[] answerPerNum = new String[100];
            Paper paper1 = new Paper();
            ArrayList<Problem> problems1 = new ArrayList<>();//每张试卷题目的集合
            ArrayList<AnswerList> answerLists1 = new ArrayList<>();//每张试卷的每次作答的集合
            Matcher matcher1 = p1.matcher(papers[i]);
            Matcher matcher2 = p2.matcher(papers[i]);
            //记录每张试卷的题目顺序和分数
            if (matcher1.find()) {
                paper1.setPaperNum(Integer.parseInt(matcher1.group(1)));
            }
            int j = 0;
            for (j = 0; matcher2.find(); j++) {
                problemNum[j] = Integer.parseInt(matcher2.group(1));
                score[j] = Integer.parseInt(matcher2.group(2));
            }
            //读取每一道题目
            for (int k = 0; k < j; k++) {
                for (int l = 0; l < pr; l++) {
                    Matcher matcher = p3.matcher(problems[l]);
                    if (matcher.find()) {
                        if (problemNum[k] == Integer.parseInt(matcher.group(1))) {
                            Problem problem = new Problem(Integer.parseInt(matcher.group(1)), matcher.group(2), matcher.group(3), score[k]);
                            problems1.add(problem);
                            break;
                        }
                    }
                }
            }
            //将所有作答以试卷序号分类
            for (int k = 0; k < my; k++) {
                Matcher matcher = p4.matcher(myAnswers[k]);
                if (matcher.find()) {
                    if (Integer.parseInt(matcher.group(1)) == paper1.getPaperNum()) {
                        answerPerNum[n] = myAnswers[k];
                        n++;
                    }
                }
            }
            //记录每张试卷的多次作答
            for (int m = 0; m < n; m++) {
                AnswerList answerList = new AnswerList();//创建一次作答的对象
                Matcher matcherMy1 = p4.matcher(answerPerNum[m]);
                Matcher matcherMy2 = p5.matcher(answerPerNum[m]);
                if (matcherMy1.find()) {
                    answerList.setAnswerNum(Integer.parseInt(matcherMy1.group(1)));
                }
                ArrayList<Answer> answers = new ArrayList<>();//创建所有作答的集合
                for (int k = 0; matcherMy2.find(); k++) {
                    Answer answer = new Answer();//创建对一个题目作答的对象
                    answer.setMyAnswer(matcherMy2.group(1));
                    answer.setProblemScore(score[k]);
                    answers.add(answer);
                }
                answerList.setAnswers(answers);
                answerLists1.add(answerList);
            }

            //将所有数据存入paper对象中
            paper1.setProblems(problems1);
            paper1.setAnswerLists(answerLists1);
            paperArrayList.add(paper1);
        }
        for (int k = 0; k < my; k++) {
            AnswerList arrayList = new AnswerList();
            Matcher matcherMy1 = p4.matcher(myAnswers[k]);
            Matcher matcherMy2 = p5.matcher(myAnswers[k]);
            if (matcherMy1.find()) {
                arrayList.setAnswerNum(Integer.parseInt(matcherMy1.group(1)));
            }
            ArrayList<Answer> answers = new ArrayList<>();
            for (int l = 0; matcherMy2.find(); l++) {
                Answer answer = new Answer();
                answer.setMyAnswer(matcherMy2.group(1));
                answer.setProblemScore(0);
                answers.add(answer);
            }
            arrayList.setAnswers(answers);
            paperAnswer.add(arrayList);
        }


        for (Paper paper : paperArrayList) {
            if (paper.GetPaperSumScore(paper.getProblems()) != 100) {
                System.out.println("alert: full score of test paper"+paper.getPaperNum()+" is not 100 points");
            }
        }
        for (AnswerList answerList : paperAnswer) {
            boolean flag = false;
            for (Paper paper : paperArrayList) {
                if (answerList.getAnswerNum() == paper.getPaperNum()) {
                    flag = true;
                    paper.printResult(paper.getProblems(), answerList.getAnswers());
                    break;
                }
            }
            if (!flag) {
                System.out.println("The test paper number does not exist");
            }
        }
    }
}
从以上代码可看出,在编写输入代码时,嵌套了多层循环进行输入,导致在寻找bug时很麻烦。 我将所有的输入环节,都放在了main方法中,使得代码不方便阅读。 还有在我将所有处理数据的方法都放在了Paper类中,使得paper类与其他类的关联性很强 ,而且也违背了类的单一性职责。

4.改进:
设计一个类,专门进行数据输入的操做, 成员方法中有输入题目,试卷,答卷的方法。

添加处理卷信息的类将该类与试卷类和答卷类相关联,其中的成员方法用来处理试卷和答卷信息。
添加一个输出类用于输出答题情况

四.
第三次作业:
1.在前两次作业的基础上添加了学生信息、删除题目信息以及多种异常输入的监测,如答案为空字符、仅有学生信息没有答案内容的空白卷、试卷引用、试题引用错误等

此次作业中我设计了六个类如下:
Question类:用于储存题目的题号,题目内容和题目答案
Paper类: 用于储存试卷编号和用数组储存的Question对象
Answer类: 用于储存答卷信息,其中每一题的答案和答案的正误判断都用数组储存,
PaperAnswer类:用于储存答卷信息和答卷题号
Student类:用于储存学生姓名和学号
PaperManager类:用于处理答卷和试卷信息

2.类图如下

从这次的类图与第二次的类图相比较,这次的类间关系相比上次的类间关系要清晰简洁许多,
但这与标准的类设计相比仍有许多不规范的地方。

3.遇到的问题
此次作业存在两大难点,其一是需要处理许多边界情况,例如:答案为空字符、仅有学生信息没有答案内容的空白卷、试卷引用、试题引用错误等情况。
其二是处理错误格式输入,而判断输入的字符串是否正确,需要正则表达式判定,但是在写正则表达式时,需要考虑各种可能出现的错误格式的字符串。
一下列举了一些经过多次修改的正则表达式:
Pattern p1 = Pattern.compile("#S:\s([1-9]\d) \s(\d{8})(\s(#A:\s[1-9]\d\s-\s[[\S ]&&[^#]]))\s\n");
Pattern p2 = Pattern.compile("#T:\s
([1-9]\d)( \s([1-9]\d\s-\s[1-9]\d))\s\n");
Pattern p3 = Pattern.compile("#X:\s((\d{8}) \s([[\S ]&&[^-]]+)\s)?(-\s(\d{8}) \s([[\S ]&&[^-]]+)\s)*\n");
在经过多次pta提交过程中,多次卡在如图所示的测试点:

经过多次的正则表达式的调试发现如下图

当输入sdfwf#N:1 #Q:1+1= #A:2时并没有显示wrong format:
经过这次测试发现我的正则表达式不能发现#字符前出现错误的字符的情况

4.缺点
这次代码依然存在第二次作业一样的缺点,就是在编写输入代码时,嵌套了多层循环进行输入,导致在寻找bug时很麻烦。我将所有的输入环节,都放在了main方法中,使得代码不方便阅读。

点击查看代码
public class PaperManager {
    public static int GetPaperSumScore(ArrayList<Question> questions) {
        int sum = 0;
        for (Question question : questions) {
            sum += question.getStanderScore();
        }
        return sum;
    }
    public static int GetStudentSumScore(ArrayList<Answer> answers) {
        int sum = 0;
        for (Answer answer : answers) {
            sum += answer.getMyScore();
        }
        return sum;
    }
    public static String findStudent(ArrayList<Student> students, String id) {
        String name = null;
        for (Student student : students) {
            if (student.getStudentId().equals(id)) {
                name = student.getStudentName();
                break;
            }
        }
        return name;
    }
    public static void judgeAnswerTrueOrFalse(ArrayList<Question> questions, ArrayList<Answer> answers) {
        for (int j = answers.size() - 1; j >= questions.size(); j--) {
            answers.remove(j);
        }
        for (int i = 0; i < questions.size(); i++) {
            boolean flag = false;
            for (Answer answer : answers) {
                if (i + 1 == answer.getOrderNum()) {
                    if (questions.get(i).getStanderAnswer().equals(answer.getMyAnswer())) {
                        answer.setAnswerTrueFalse(true);
                        answer.setMyScore(questions.get(i).getStanderScore());
                    }
                    flag = true;
                    break;
                }
            }
            if (!flag) {
                Answer answer = new Answer();
                answer.setMyScore(0);
                answer.setOrderNum(i + 1);
                answers.add(answer);
            }
        }
    }
    public static void printResult(ArrayList<Question> questions, PaperAnswer paperAnswer, ArrayList<Student> students) {
        judgeAnswerTrueOrFalse(questions, paperAnswer.getAnswers());
        ArrayList<Answer> answers = paperAnswer.getAnswers();
        for (int i = 0; i < questions.size(); i++) {
            for (Answer answer : answers) {
                if (i + 1 == answer.getOrderNum()) {
                    if (!Objects.isNull(answers.get(i).getMyAnswer())) {
                        if (questions.get(i).getStanderScore() != 0) {
                            System.out.println(questions.get(i).getQuestionContent() + "~" +
                                    answer.getMyAnswer() + "~" + answer.isAnswerTrueFalse());
                        }
                        else {
                            System.out.println(questions.get(i).getQuestionContent() + "~" + 0);
                        }
                    }
                    else {
                        System.out.println("answer is null");
                    }
                    break;
                }
            }
        }
        if (!Objects.isNull(findStudent(students, paperAnswer.getAnswererId()))) {
            System.out.print(paperAnswer.getAnswererId()+" "+findStudent(students, paperAnswer.getAnswererId())+": ");
            for (int i = 0; i < answers.size(); i++) {
                if (i < answers.size() - 1) {
                    System.out.print(answers.get(i).getMyScore() + " ");
                }
                else if (i == answers.size() - 1) {
                    System.out.print(answers.get(i).getMyScore() + "~");
                }
            }
            System.out.print(GetStudentSumScore(answers));
        }
        else {
            System.out.println(paperAnswer.getAnswererId() + " not found");
        }
    }
}
以上是PaperManager类,其不足之处是部分类放该类中没有体现该类的职责,例如,其中的GetPaperSumScore(ArrayList questions)方法是用来获取试卷的卷面分,理应该方法应放在Paper类中。

5.改进
添加一个类,用于输入答卷和答卷信息,还有学生信息,并判断信息的合法性
调整PaperManager类的成员方法

五.总结
经过三次PTA作业,我逐渐理解如何合理地设计类,从第一次作业到第三次作业,其中经历了一些由于类设计不合理导致每次遇到代码BUG时,修改代码尤为麻烦。经过两次代码的迭代,我的类设计逐渐变得合理起来,感受到类设计的合理性能极大地改善写代码的效率。通过这几次作业的类设计,我逐渐理解了类设计的单一性职责,迪米特法则等类设计原则。
在做三次PTA作业的过程中,我学习到了许多的新知识,例如,学会使用正则表达式,利用集合或链表储存数据。其次也了解到了许多高效工具网站和软件,例如我匹配正则表达式的网站https://regex101.com/来检验我的正则表达式是否能正确匹配字符串。还有我利用IDEA自带的UML生成类图的插件来查看代码中的类间关系。
经过两次的代码迭代,我的类设计愈加合理,但才一些类设计细节上还有一些欠缺。还有我对一些常用的API不了解,使我在实现某些类的功能时,都要在网站上查阅各种信息,临时学习新的API。今后我还需要学习更多的API,进一步理解类设计的原则。
到第三次作业,我的类设计也许没有太大的问题,但等到下次迭代时,可能要涉及到继承和多态的知识。如果涉及到新的类设计原则时,我现在的类设计还存在巨大的漏洞,还需要进一步改进。