逸言

新人培养日记(一)

| Comments

我在公司内部Wiki找到了一份如何利用TDD驱动开发与设计的案例。这个案例的代码非常粗略,用C#编写。可贵之处在于它提供了20个已经写好的Story,并以正式项目中书写Story的方式编写。这个案例的需求类似一个真实软件系统,非常适合让新人体会应该如何开发一个完整地项目。它要求创建一个电子银行系统,提供银行的一些基本功能,例如客户管理、存取款等业务逻辑。

最初我并不知道新手的真实水平,不过,可以假设他只具备基础的Java编程知识,对TDD、Refactoring以及OO有最初步的了解,但处于懵懵懂懂之间。我最初希望新手仅仅针对业务层进行设计与编码,但新手坚持要加上数据库访问以及Web页面。我认为这种做法有利于新手学习到更多的Java框架和库,便同意了这种请求。

我给新人简单地讲了前面两个Story的需求,就将这个项目扔给新手,一周后,我再去Review他提交的代码。结果发现了一大堆问题。

首先,项目没有使用任何构建工具,例如Maven或者Gradle。项目依赖的一些Java包,例如JUnit和Mockito,使用了直接依赖的方式。没有构建工具来创建构建脚本,就无法使用最快捷的方式来编译、构建以及运行测试,从而使得开发者懒于在提交前进行构建,以确保代码没有引入问题。没有自动化构建,更谈不上持续集成了(我打算在项目中期要求新手搭建CI环境)。

新手对代码包的划分,以及相关类的命名,有些惨不忍睹。上图是对该项目包结构以及相关类分布的截图。infoTracker包的命名让人觉得莫名其妙,打开其中的类查看代码,发现它事实上是调用DBTransaction类,执行对数据表的CRUD操作。例如代码:

infoTracker.CustomerAccountUpdater.java
public class CustomerAccountUpdater {
    private static DBTransaction transaction = new DBTransaction();

    public static void addBalance(double money, Customer customer) {
        transaction.addBalance(money, customer);
    }

    public static void withdrawBalance(double money, Customer customer) throws CustomerBalanceInvalid {
        transaction.withdrawBalance(money, customer);
    }
}

database包的DBTransaction类的命名更是容易引起歧义,以为是对事务的处理,实则是DAO对象:

database.DBTransaction.java
public class DBTransaction {
    public static Connection connection;
    private PreparedStatement pstmt;

    public DBTransaction() {
        this.connection = DBConnection.getConnection();
    }

    public void addCustomer(Customer customer) {
        String nickname = customer.getNickname();
        Date dateOfBirth = customer.getDateOfBirth();
        String properName = customer.getProperName();
        Date joiningDate = customer.getJoiningDate();
        double balance = customer.getBalance();
        boolean isBonusAdded = customer.isBonusAdded();

        String sql = "insert into userinfo values (?, ?, ?, ?, ?, ?)";

        try {
            pstmt = connection.prepareStatement(sql);
            pstmt.setString(1, nickname);
            pstmt.setObject(2, dateOfBirth);
            pstmt.setString(3, properName);
            pstmt.setObject(4, joiningDate);
            pstmt.setDouble(5, balance);
            pstmt.setBoolean(6, isBonusAdded);

            pstmt.executeUpdate();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

新手并没有使用我建议的Spring JDBC Template,而是直接使用了JDBC,因而充斥了大量的冗余代码。再看他实现的DBConnection,居然将连接的相关属性硬编码到Java类中了:

database.DBConnection.java
public class DBConnection {

    private static Connection conn;

    public static Connection getConnection() {
        String driver = "com.mysql.jdbc.Driver";
        String url = "jdbc:mysql://127.0.0.1:3306/test_01";
        String username = "root";
        String password = "";

        try {
            Class.forName(driver);
            conn = DriverManager.getConnection(url, username, password);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (SQLException e) {
            e.printStackTrace();
        }

        return conn;
    }

    public static void disconnect() {
        try {
            conn.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

再看测试,没有单元测试和集成测试的概念。一些本来可以进行单元测试的逻辑,例如针对Customer Nickname的验证逻辑,并没有得到足够的测试覆盖。由于没有构建脚本帮助搭建本地测试环境,在获得系统源代码后,根本无法运行集成测试。

整体来看,新手对一个项目如何进行分层以及分包缺乏基本的概念,没有自动化构建的意识。对于如何编写数据库访问的类也缺乏足够的知识。不了解单元测试与集成测试之间的区别,没有开发环境和测试环境的基本认识。

我首先考虑引入gradle实现基本的自动化构建,它可以通过引入一些插件就可以比较容易地实现java编译、测试以及自动化打包的功能,还能比较容易地运行Jetty。

build.gradle
apply plugin: 'idea'
apply plugin: 'java'
apply plugin: 'war'
apply plugin: 'jetty'

repositories {
    mavenCentral()
}

dependencies {
    compile (
            'joda-time:joda-time:2.1'
    )
    testCompile (
            'junit:junit:4.11',
            'org.mockito:mockito-all:1.9.5'
    )
}

我修改了之前定义的infoTracker包,将其重命名为repository包,并放到domain包下,并对其下的类也同样进行了重命名。修改后的包结构如下图所示:

我没有修改数据库访问层的代码,而是要求新手使用JDBC Template,并着重给他讲解了数据库访问的基础知识,并要求他将数据库属性放置到配置文件中。我想,到了下一周,他又会提交一份什么样的代码呢?

Comments