学员会诊之01:那些典型的面向对象错误

       我原本不想用“错误”二字。因为错误显得太严重了,并且,软件编码本身就没有对错,只要你把功能实现了,剩下的就是思想流派的分歧。但这里仍旧想用“错误”两个字,因为本篇诊断所涉及的那些问题基本已经属于当前软件开发规则中的普适需要避免的。

       注意,被诊断的学员并不是学的差的学员,相反,他有可能是学的很好的那一个,今天要诊断的刘同学,就是这样的一位同学。刘同学来到最课程学习已经50天了,以下是他的学习记录:

       看到没有,分数基本都在99分左右,然后到了大作业这里……,80分。我当时跟刘同学是这么说的:基本上,面向对象编码过程中所犯的那些错误,你都犯了。当然,再次说明,刘同学到目前为止的学习都是非常棒的,不信你看他过去这几周发的博文,我相信,已经毕业的同学中都没有几个能达到他这样的理解力度:

关于CSS中的float可能出现的小问题

Java中的正则表达式之(?=X)和(?<=X)的讲解和简单应用(注:X在这里是表达式)

关于Java多线程的线程同步和线程通信的一些小问题(顺便分享几篇高质量的博文)

对Java通配符的个人理解(以集合为例)

关于Java中的equals方法

关于对Java中异常处理的try catch和throw的理解(浅显理解)

安利一款强大的学习软件XMind(顺便放上这几天制作的JavaSE的思维导图day1-day4)

       现在,让我们言归正传,看看刘同学在面向对象的作业(作业地址:最课程阶段大作业02:实现自己的利息计算器)中,都犯了那些错误。

1. 获取源码

       我们的第一个作业,就要求学员自己搭建源码服务器(SVN),所以第一件事情,既然我们要批改作业,那就得从源码服务器上去签出代码。过程如下:

       首先,new->other,

 

       其次,选择project from svn,

       紧接着,输入SVN地址,以及用户名和密码,同时注意勾选Save authentication,

       直接点击finish,

       再次点击finish,

       选择签出为Java Project,

       输入本地项目地址,注意:跟服务器上保持一致,

       以下就是我们顺利签出的项目。先欣赏一下代码吧,是不是第一感觉还不错:

 

2. 源码分析

       刘同学的源码,一共分为3个文件,分别为:

       InterestCalculator.java:利息计算主类,相关业务逻辑在此处;

       InterestCalculatorTester.java:测试类,main函数入口;

       Utility.java:工具类,主要用于处理键盘输入;

       这一眼看上去,很不错,每个类各司其职。同时,运行结果也不错,如下:

       但是,我看到这份源码,首先第一个就去找判断利息计算方式的代码,结果发现了如下的代码:

       于是,我问刘同学的第一个问题就来:“假设我此刻需要增加一种利息计算方式,该怎么办?”。

       刘同学的回答是:再增加一个case!

3. 违反开闭原则(Open Closed Principle,OCP

       咳咳,“再增加一个case”,恰恰就犯了第一个错误:违反开闭原则。

       所谓“开闭原则”,就是“对扩展开放,对修改关闭”。这要怎么讲?初次接触此概念的同学,很可能一脸懵逼,

二脸还是懵逼……

       我们先来说:对修改关闭。什么是对修改关闭?就是,对于利息计算来说,当一种计算方式被发明出来后,它的算法就再也没有变过。所以,一种算法对应一个Java类,那么,算法写完成后,这个类就不应该需要再修改了。

       那什么是:对扩展开放?就是,你的程序当前支持3种利息计算方式,但随着时代跟进,也许增加了一种计算方式,那代码必须得很方便的扩展。那什么是方便的扩展呢?其中一种方式就是:增加一个子类。

       所以,想明白没有?当前的这个switch可以重构为一个抽象工厂,同时,算法本身应该有继承体系,大致如下:

package com.zuikc.intesters;

import java.util.LinkedList;

public abstract class InterestCalculator {
    // 本金
    protected double principal;
    // 期限(月数)
    protected int numberOfPeriods;
    // 年利率
    protected double interestRate;
    // 总利息
    protected double totalInterests;
    // 本息合计
    protected double totalToPay;
    // 用于保存计算结果,即还款计划
    protected LinkedList<PayPlan> results = new LinkedList<>();
    
    public InterestCalculator(double principal, int numberOfPeriods, double interestRate){
        this.principal = principal;
        this.numberOfPeriods = numberOfPeriods;
        this.interestRate = interestRate;
    }
    
    // 获取输入并且计算
    public void inputAndCalculate(){
        getData();
         calculate();
    }
    
    // 利息计算,由子类去实现
    abstract void calculate();
    
    private void getData(){
        // 获取键盘输入,这里也可以提炼一个工具类;
        // 从键盘输入的值存储到本金,期限,年利率
    }
}

       注意,三个算法就是三个子类。

       在上文中,还有一个PayPlan,它是一个实体类,里面有一些属性,用来表示每个月的还款信息。

4. 违反单一职责原则(Single Responsibility Priciple,SRP

       在面向对象的开发中,我们还有另外一个原则,叫做:单一职责原则(Single Responsibility Priciple,SRP),简单来说,就是一个类只完成一件事情。

       而在刘同学的代码中,我们可以看到InterestCalculator干了非常多的事情,

       有负责获取输入的,有负责进行结果导出的,有负责利息计算的(还包好了三种计算),甚至有对结果进行加密解密的(虽然部分实现是在Utility中)。可是这样就会导致:一眼望去,就是妻妾太多,迟早要出事。

       导入导出重构出来一个类;

       加解密重构出来一个类;

       负责利息计算的,更不要说了;

5. 单个方法行数太多

       一种观点是,单个方法不要超过30行。为什么会有30行这个数字呢?那是因为早期的显示器,如果你的代码超过30行,就超过一屏的显示了,而我们阅读代码,最好是一屏幕内显示完毕。

如果一个方法在一屏内显示不完,在大多数情况下,这意味着你需要将当前方法重构为两个方法。

6. 不应将输出固定格式

       我们在作业之中要求结果是输出到控制台的,于是刘同学将整个结果保存到了一个字符串中,如下:

       那么问题又来了:假设我们现在要输出为HTML格式怎么办?

       我们会发现,整个计算的过程必须全部修改。

       正确的做法是,我们将计算结果保存到对象或者对象列表中,就像上文我重构的InterestCalculator一样。这样做的一个好处是,当我们需要将结果以不同的格式进行输出的话,我们只需要提取对象的属性就可以。

7. 其它问题

       其它问题不多了。如果其它问题过多的话,我估计刘同学要郁闷的。但是,不过,当然,确实还存在另一个比较严重的问题:对称加密。

       本次作业,几乎所有的同学都自己实现了一个对称加密的算法。但这是有问题的。由于这个议题相对来说比较独立,所以,让我们在下一篇中详细指出。

8. 提交修改

       我们在刘同学的代码上进行了一些修改,让我们进行签入吧。

       写上comment,

       好了,诊断结束,让我们给出刘同学的本次作业的会诊单吧。

 

最课程学员会诊单

华丽分割线

===========================================================

最课程JavaEE+互联网分布式新技术开班进行中,来http://www.zuikc.com看看吧。你想参加不一样的培训班,并且一毕业就NB,那就来加入我们吧;

更多技术文章和开班信息请加入,

QQ群:

原文地址:https://www.cnblogs.com/luminji/p/9512123.html