第一次Blog作业(修正)

第一次Blog作业(修正)

 

 

一、前言

本次博客主要记录自2022年2月20日以来的五次雨刷设计,五次pintia.cn题目集以及三次实验报告。

 

二、设计与分析

1/雨刷设计

题目要求:对上述雨刷问题进行编程模拟,可使用交互式菜单,完成司机对雨刷系统的控制及测试

 

第一次作业:本次作业未作任何要求,可自由设计。(Java入门作业)

  • 设计策略:作为一个java小白我只设计了一个类,在一个类中设计了很多函数,与C语言的设计风格较为相像。

类图如下:

不足之处:

      1.只有一个类

      将刻度盘、雨刷、控制杆这些因素全都放在了一个类里面,使这一个类变得极为复杂,这样的设计完全不符合java的单一职责设计原则,这样的代码可读性不强,逻辑性不够,复用性不高,如果将来我要增加这一系统的功能,这个类就会变得庞大,而一旦其中的一个功能出了问题,就可能牵一发而动全身,瘫痪掉整个系统。

     2. 编码不规范

  • 部分命名不符合UpperCamelCase命名风格,在java设计中我们对类名,函数名以及变量名大多采用驼峰命名法,对于类名我们应采用大驼峰命名法,即单字之间不以空格或连接号断开,每个单字首字母都为大写,对于函数名以及变量名我们应采用小驼峰命名法,与前者的唯一差别是第一个单字的首字母为小写。
  • Switch块缺少default语句
public static int reDialSpeed(int dial) {
        int speed = 4;
        switch (dial)
        {
            case 1:
                break;
            case 2:
                speed = 6;
                break;
            case 3:
                speed = 12;
                break;
        }
        return speed;
    }
  • 'if' 没有加大括号
public static void upGear() {
        gear++;
        if(gear > 4)
            gear = 4;
    }

    public static void downGear() {
        gear--;
        if(gear < 1)
            gear = 1;
    }

    public static void upDial() {
        scale++;
        if(scale > 3)
            scale = 3;
    }

    public static void downDial() {
        scale--;
        if(scale < 1)
            scale = 1;
    }

 

第二次作业:重构上次作业雨刷问题,需求不变,采用面向对象技术,合理设计实体类、业务(控制)类、接口类及各个类之间的关系,务必符合SRP(Single Responsibility Principe,单一职责原则)

  • 设计策略:采用MVC设计模式

     MVC 设计模式代表 Model-View-Controller(模型-视图-控制器) 模式。这种模式用于应用程序的分层开发。

  • Model(模型) - 模型代表一个存取数据的对象或 JAVA POJO。它也可以带有逻辑,在数据变化时更新控制器。(实体类)
  • View(视图) - 视图代表模型包含的数据的可视化。(接口类)
  • Controller(控制器) - 控制器作用于模型和视图上。它控制数据流向模型对象,并在数据变化时更新视图。它使视图与模型分离开。(数据类)

      此次设计将刻度盘、雨刷以及控制杆作为实体类,每个实体类有自己属性与方法,这样的设计符合单一职责原则。

类图如下:

MVC设计模式的优点:

  1. 耦合性低
  2. 复用性高
  3. 可维护性高

不足:

  1. MVC设计模式本来是要保证接口类、实体类与视图类之间耦合性非常低的,但是本次设计却导致了视图类与实体类之间的高耦合性。
public Central() {
        dial = new Dial(1);
        lever = new Lever(1);
        speed = new Speed(0);
        display = new Display(lever,dial,speed);
    }
  1. 代码中魔法值的使用太多,根据阿里规约,要避免魔法值。

什么是魔法值?例如这几行代码中的’P’

while(opera != 'p') {
            menu();
            central.control(opera);
            opera = in.next().charAt(0);
        }

       当其他人员在阅读这几行代码时,不免会产生疑问,为什么在opera不等于p时中断循环?这个p究竟代表什么?所谓魔法值,是指在代码中直接出现的数值,只有在这个数值记述的那部分代码中才能明确了解其含义。

 

第四次作业:重构第三次作业(为什么没有第三次作业因为第三次与第二次毫无区别),尝试嵌入单例模式,设计两个接口类。

  • 设计策略:加入Driver令Main类的行数缩小至一行两行,且更加符合逻辑,加入Business类(第二个接口类)作为MVC设计模式的DEMO,将菜单及一切输出函数加入到视图类中,并解决视图类与实体类的高耦合性问题。

类图如下:

可以看出每个实体类都有一条从自身出发最后回指自身的箭头,这就是嵌入单例模式的样子。

 

       单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

       这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

注意:

  • 1、单例类只能有一个实例。
  • 2、单例类必须自己创建自己的唯一实例。
  • 3、单例类必须给所有其他对象提供这一实例。

      这是代码中实现单例模式的方式(饿汉式)

    private static Dial instance = new Dial();

    private Dial(){}

    public static Dial getInstance() {
        return instance;
    }

实现单例模式的三个步骤:

  1. 将类的构造方法定义为私有方法。这样其他类的代码就无法通过调用该类的构造方法来实例化该类的对象,只能通过该类提供的静态方法来得到该类的唯一实例。
  2. 定义一个私有的类的静态实例。
  3. 提供一个公有的获取实例的静态方法。

 

第五次作业:重构第四次作业,且遵循开闭原则。拓展雨刷系统的挡位,刻度以及速度,并引入各实体类以及接口类的父类。

  • 设计策略:拓展雨刷系统的功能后,依据开闭原则,不动原来的代码,设计控制类的继承与多态,使本程序拥有两套系统,一套原始系统,一套拓展系统,并且设计实体类的继承与多态,每个实体类父类都应拥有两个子类。

类图如下:

 

 

每个抽象类的父类与子类之间都应符合里氏代换原则

里氏替换原则通俗的来讲就是:子类可以扩展父类的功能,但不能改变父类原有的功能。它包含以下4层含义:

  1. 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。
  2. 子类中可以增加自己特有的方法。
  3. 当子类的方法重载父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
  4. 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。

优点

  1. 代码共享,减少创建类的工作量,每个子类都拥有父类的方法和属性;
  2. 提高代码的重用性;
  3. 子类可以形似父类,但又异于父类,“龙生龙,凤生凤,老鼠生来会打洞”是说子拥有父的“种”,“世界上没有两片完全相同的叶子”是指明子与父的不同;
  4. 提高代码的可扩展性,实现父类的方法就可以“为所欲为”了,君不见很多开源框架的扩展接口都是通过继承父类来完成的;
  5. 提高产品或项目的开放性。

缺点

  1. 继承是侵入性的。只要继承,就必须拥有父类的所有属性和方法;
  2. 降低代码的灵活性。子类必须拥有父类的属性和方法,让子类自由的世界中多了些约束;
  3. 增强了耦合性。当父类的常量、变量和方法被修改时,需要考虑子类的修改,而且在缺乏规范的环境下,这种修改可能带来非常糟糕的结果————大段的代码需要重构。

2/PTA类设计

 日期类设计 (30 分)

     参考题目集二中和日期相关的程序,设计一个类DateUtil,该类有三个私有属性year、month、day(均为整型数),其中,year∈[1820,2020] ,month∈[1,12] ,day∈[1,31]

应用程序共测试三个功能:

  1. 求下n天
  2. 求前n天
  3. 求两个日期相差的天数

这是第二题的设计

这是第三题的设计

可以看出第三题的设计符合单一职责原则,代码更具有逻辑性,方法和类的复杂度要更低。

可以比较一下两种设计方法对于求两个日期相差的天数这一功能的函数构造。

 

这是第二题的求两个日期相差的天数的函数:

查看代码

public int getDaysofDates(DateUtil date) {//求当前日期与date之间相差的天数
    int[] days = {0,31,28,31,30,31,30,31,31,30,31,30,31};
    int[] leap_days = {0,31,29,31,30,31,30,31,31,30,31,30,31};
    int sum = 0;

    if (equalTwoDates(date)) {
        return 0;
    } else if (compareDates(date)) {
        if (year > date.year) {
            for (int i = date.getMonth() + 1; i <= 12; i++) {
                if (isLeapYear(date.getYear())) {
                    sum += leap_days[i];
                } else {
                    sum += days[i];
                }
            }
            for (int i = 1; i < month; i++) {
                if (isLeapYear(year)) {
                    sum += leap_days[i];
                } else {
                    sum += days[i];
                }
            }
            for (int i = date.getYear() + 1; i < year; i++) {
                if (isLeapYear(i)) {
                    sum += 366;
                } else {
                    sum += 365;
                }
            }
        } else {
            for (int i = date.getMonth() + 1; i < month; i++) {
                if (isLeapYear(year)) {
                    sum += leap_days[i];
                } else {
                    sum += days[i];
                }
            }
        }
        if (isLeapYear(date.getYear())) {
            sum += (leap_days[date.getMonth()] - date.getDay()) + day;
        } else {
            sum += (days[date.getMonth()] - date.getDay()) + day;
        }
    } else {
        if (date.getYear() > year) {
            for (int i = month + 1; i <= 12; i++) {
                if (isLeapYear(year)) {
                    sum += leap_days[i];
                } else {
                    sum += days[i];
                }
            }
            for (int i = 1; i < date.getMonth(); i++) {
                if (isLeapYear(date.getYear())) {
                    sum += leap_days[i];
                } else {
                    sum += days[i];
                }
            }
            for (int i = year + 1; i < date.getYear(); i++) {
                if (isLeapYear(i)) {
                    sum += 366;
                } else {
                    sum += 365;
                }
            }
        } else  {
            for (int i = month + 1; i < date.getMonth(); i++) {
                if (isLeapYear(year)) {
                    sum += leap_days[i];
                } else {
                    sum += days[i];
                }
            }
        }
        if (isLeapYear(year)) {
            sum += date.getDay() + (leap_days[month] - day);
        } else {
            sum += date.getDay() + (days[month] - day);
        }
    }
    return sum;
}

再看看第三题的函数构造:

public int getDaysofDates(DateUtil date) {
    int sum = 0;

    while (compareDates(date)) {
        if (equalTwoDates(date)) {
            break;
        }
        date.day.dayIncrement();
        sum++;
    }

    while (!compareDates(date)) {
        if (equalTwoDates(date)) {
            break;
        }
        day.dayIncrement();
        sum++;
    }

    return sum;
}

       在第二题中无比繁复的函数让人看了简直头皮发麻,而在第三题中却又条理清晰,逻辑分明,让人看了觉得赏心悦目,但实际上两个函数所要实现的功能与实现功能的原理都是一样的,可以说两个函数实际上是一个水平的人写出来的,但如果是你,你更喜欢哪种呢?显然符合单一职责原则的代码往往更加简洁明了,易于修改与拓展。

 

三、踩坑心得

1/浮点数误差问题

       Java中的简单浮点数类型float和double不能够进行运算,因为计算机是二进制的,从二进制转化为十进制浮点数时,精度容易丢失,导致精度下降,此问题会造成严重的后果。

       因为Java的浮点数误差,在做题中主要发现以下两个问题:

    (1)直角三角形判断问题

       我们知道直角三角形的判断条件是a方+b方=c方,在某些时候,一条边的数值往往是无理数,就比如三条边的长度分别为1、2、根号5的情况下,如果让你输入根号5的值,你不可能准确的输入根号5的值是多少,因此这个等式是不可能成立的。所以一般采用a方+b方-c方<error(误差)这种形式来判断直角三角形。

    (2)保留小数问题

       比如有一个题目,要求输出的浮点数如果小数点后超过了六位便保留六位小数,没超过就有几位输出几位。这时候因为java的小数误差就会出现一个很棘手的问题。假如我有一个数据算出来的正常结果应该是1.0,而因为误差算出来的结果是0.99999998,那么输出的结果就变成了1.000000,这时候程序就出错了。

       我个人对于这种问题的解决方法是使用Java在java.math包中提供的API类BigDecimal,BigDecimal用来对超过16位有效位的数进行精确的运算。可以参考这个博客:T-JAVA 小数计算精度问题_weixin_44331516的博客-CSDN博客

 

2/坐标的正则表达式

在坐标输入中判断坐标格式是否正确是一件麻烦事,而用正则表达式就很方便

        /*

          正则表达式:坐标的基本格式

         */

        String regexCoordinates = "^[-+]?\\d+(.\\d+)?,[-+]?\\d+(.\\d+)?$";

        /*

          两种错误坐标格式,一种是01,XX型,一种为XX,01型

         */

        String regex0X = "^[-+]?0+\\d+(.\\d+)?,[-+]?\\d+(.\\d+)?$";

        String regexX0 = "^[-+]?\\d+(.\\d+)?,[-+]?0+\\d+(.\\d+)?$";

 

四、改进建议

以最近这一道题为例

以下是我的源码类图:

       可以看得出,类简直少得可怜,除了一个关于精度计算的工具类以外,只有一个主类。这次作业虽然提交速度很快,分数是满的,但代码质量却很低。虽然做了很多注释,可读性也高不了哪去。从改进上,我认为依据单一职责,三角形应该单独为一个类,选项四、选项五的复杂方法,仍然要有单独的类,提高代码的复用性,能极大提升代码的可读性与逻辑性。

 

五、总结

回顾2022年2月20日以来的面向对象课程,我们总共学习了:

  1. 类与类之间的关系
  2. 面向对象编程的设计原则
  3. 面向对象编程的设计模式
  4. 继承与多态
  5. 软件质量与软件测试的方法
  6. 泛型与集合

需要进一步学习研究的地方:

  1. 对于设计原则中的接口隔离原则、里氏代换原则、依赖倒转原则以及合成复用原则还需要一些练习来体会与感悟。
  2. 对于简单工厂模式、建造者模式与桥接模式还处于懵懂的阶段
  3. 对于Comparable接口类中的CompareTo方法还没有理解
  4. 对泛型的应用还需大量练习以熟练
  5. 对集合框架的运用处于零的阶段

对教师、课程、作业、实验、课上及课下组织方式等方面的改进建议及意见:

在防疫封闭时期希望学习生活能增添一点乐趣。

 

posted @ 2022-04-13 16:25  Inmata  阅读(267)  评论(0)    收藏  举报