逸言

新手培养日记(二)

| Comments

几天后,新手又一次提交了一份代码。由于改动较大,新手重新创建了一个项目。在这份代码中,新手接受了我的建议,改为使用Spring提供的JdbcTemplate。包的结构也得到了一定程度的改善。这充分说明他认识到了问题所在,并能够快速准确地采取行动去纠正这些问题。但或许是我提出的问题太多,给出的建议不够具体,在新提交的这份代码中,我还是看到了一些问题,且某些问题在上一次Review代码时,我曾经提及。

来看看如下两段代码。首先,是CustomerService,它定义了目前Story要求的基本业务:

@Service("customerService")
public class CustomerService {

    @Autowired
    @Qualifier("customerDAO")
    private CustomerDAO customerDAO;

    private final String defaultTableNameForCustomer = "customer";
    private String tableName = defaultTableNameForCustomer;

    public void setTableName(String tableName) {
        this.tableName = tableName;
    }

    @Transactional
    public void addCustomer(Customer customer) throws DuplicateCustomerException {
        customerDAO.addCustomer(customer, tableName);
    }

    public Customer getCustomer(String nickname) throws CustomerNotFoundException {
        return customerDAO.getCustomer(nickname, tableName);
    }

    public void withdraw(String nickname, double balanceToWithdraw) throws BalanceOverdrawException {
        customerDAO.withdrawBalance(nickname, balanceToWithdraw, tableName);
    }

    public void deposit(String nickname, double balance) {
        customerDAO.deposit(nickname, balance, tableName);
    }
}

在CustomerService类中,调用了CustomerDAO类的相关方法:

@Component
@Repository("customerDAO")
public class CustomerDAO {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    public void createTable(String tableName) {
        jdbcTemplate.execute("DROP TABLE IF EXISTS " + tableName);
        jdbcTemplate.execute("CREATE TABLE " + tableName + "(" + "nickname VARCHAR(45) NOT NULL ," +
                "dateOfBirth DATETIME NOT NULL," + "balance DOUBLE NOT NULL, " + "PRIMARY KEY(nickname)" + ");");
    }

    public void addCustomer(Customer customer, String tableName) throws DuplicateCustomerException {
        String SQL = "insert into " + tableName + "(nickname, dateOfBirth, balance) values (?, ?, ?)";
        try {
            jdbcTemplate.update(SQL, new Object[]{customer.getNickname(), customer.getDateOfBirth(), customer.getBalance()});
        } catch (DuplicateKeyException exception) {
            throw new DuplicateCustomerException("Customer with nickname " + customer.getNickname() + " has already existed");
        }
    }

    public Customer getCustomer(String nickname, String tableName) throws CustomerNotFoundException {
        String SQL = "select * from " + tableName + " where nickname = ?";
        Customer customer;
        try {
            customer = jdbcTemplate.queryForObject(SQL, new Object[]{nickname}, new CustomerMapper());
        } catch (EmptyResultDataAccessException exception) {
            throw new CustomerNotFoundException("Customer with nickname " + nickname + " is not found");
        }
        return customer;
    }

    public Double getBalance(String nickname, String tableName) {
        String SQL = "select balance from " + tableName + " where nickname = ?";
        double balance = jdbcTemplate.queryForObject(SQL, new Object[]{nickname}, Double.class);
        return balance;
    }

    public void withdrawBalance(String nickname, double balanceToWithdraw, String tableName) throws BalanceOverdrawException {
        double balanceBefore = getBalance(nickname, tableName);
        double balanceAfter = balanceBefore - balanceToWithdraw;

        if (balanceAfter < 0) {
            throw new BalanceOverdrawException("You have only " + balanceBefore + "$. You can not withdraw " + balanceToWithdraw + "$");
        }

        String SQL = "update " + tableName + " set balance = ? where nickname = ?";

        jdbcTemplate.update(SQL, new Object[]{balanceAfter, nickname});
    }

    public void deposit(String nickname, double balanceToDeposit, String tableName) {
        double balanceBefore = getBalance(nickname, tableName);
        double balanceAfter = balanceBefore + balanceToDeposit;

        String SQL = "update " + tableName + " set balance = ? where nickname = ?";
        jdbcTemplate.update(SQL, new Object[]{balanceAfter, nickname});
    }
}

这两段代码存在什么问题?

显然,我们看到CustomerService履行的职责仅仅是对调用的委派,另外还添加了事务功能,除此之外,它什么事情都没有做,接到了请求,转手就递给CustomerDAO了。再看CustomerDAO,特别关注withdrawBalance()方法,你会发现这个方法的实现其实体现了较多的业务逻辑。事实上,我们看到这个方法的名称,体现的就是业务的概念。显然,这里的职责分配是不合理的。新手明显没有深刻体会Service与Dao之间的区别。无论是传统的分层架构模型,还是DDD提出的领域层与基础设施层的分离,都表达了业务与数据访问关注点分离的原则。事实上,新手还错误地将Service类放到了database.service包中。

正确的做法应该是保证每个对象的纯洁性与单一性,让每个对象只做一件事情,只做它应该关心的事情,遵循单一职责原则。Dao是数据访问对象,那么它就应该只处理数据访问的逻辑,而对具体业务应该是“一无所知”的。一个简单的识别办法,就是不要在这个类中出现任何业务概念,它做的事情就是CRUD。

对于许多OO初学者而言,职责不清是最容易犯下的毛病。要么就是恨不得把所有内容都塞给一个类;要么就是张冠李戴,随着性子乱分配职责,全然不考虑每个对象的感受。我常常说,对象是有意识的生物,这样不尊重对象搞乱分配,迟早这些对象会造反。

Comments