技术开发 频道

敏捷开发的必要技巧(七)

【IT168 技术文章】    这是一个会议管理系统。该系统记录每个参会者的ID、姓名、电话和所属地区。参会者ID是会议组织者分配的唯一的数字标识。参会者的姓名是必须要提供的。电话则不是必填的。所有的参会者只能来自三个地域:中国、美国或者欧洲。
    现在我们在数据库创建一个表来存储参会者的这些信息。表结构是这样的:

create table Participants ( id int primary key, name varchar(20) not null, telNo varchar(20), region varchar(20) );

    为了让系统的使用者可以增加新参会者,我们需要下面这样的代码。系统会自动从已有的所有参会者中找出最大的ID,加1作为新的参会者的默认ID,并显示在界面的输入框内。用户可以直接使用这个ID,也可以自行输入一个特定的ID。请认真读下面代码:

class AddParticipantDialog extends JDialog { Connection dbConn; JTextField id; JTextField name; JTextField telNo; JTextField region; AddParticipantDialog() { setupComponents(); dbConn = ...; } void setupComponents() { ... } void show() { showDefaultValues(); setVisible(true); } void showDefaultValues() { int nextId; PreparedStatement st = dbConn.prepareStatement("select max(id) from participants"); try { ResultSet rs = st.executeQuery(); try { rs.next(); nextId = rs.getInt(1)+1; } finally { rs.close(); } } finally { st.close(); } id.setText(new Integer(nextId).toString()); name.setText(""); region.setText("中国"); } void onOK() { if (name.getText().equals("")) { JOptionPane.showMessageDialog(this, "名称不能为空"); return; } if (!region.equals("中国") && !region.equals("美国") && !region.equals("欧洲")) { JOptionPane.showMessageDialog(this, "Region is unknown"); return; } PreparedStatement st =
dbConn.prepareStatement("insert into from participants values(?,?,?,?)");
try { st.setInt(1, Integer.parseInt(id.getText())); st.setString(2, name.getText()); st.setString(3, telNo.getText()); st.setString(4, region.getText()); st.executeUpdate(); } finally { st.close(); } dispose(); } }

    这段代码看起来还正常吧?但其实这里将下面三个方面的代码混在了一起:
    1. UI:JDialog、JTextField、响应用户事件的代码。
    2. 数据库访问:Connection、PreparedStatement、SQL statements和ResultSet等。
    3. 域逻辑:新参会者的默认ID、参会者姓名是必填的,以及所属地区的限制等。域逻辑又称为“域模型”或者“业务逻辑”。
    这三方面的代码混在一起,会造成下面的问题:
    1. 代码很复杂。
    2. 代码很难重用。如果我们想创建一个EditParticipantDialog,让用户更改参会者的信息,我们就想重用部分域逻辑(比如地区的限制)。但实现这部分域逻辑的代码与AddParticipantDialog混在一起,根本不能重用。如果是在一个Web系统中,就更难重用了。
    3. 代码很难测试。每次要测这样一段代码,我们都要建一个数据库,还要通过一个用户操作界面进行测试。
    4. 如果数据库表结构更改,AddParticipantDialog这个类和其他很多地方都要跟着更改。
    5. 它导致我们一直在考虑一些低层的、细节的概念,比如数据库字段、表的记录之类,而不是类、对象、方法和属性这些类的概念。或者说,一直在考虑怎么往数据库里面装数据,而没有面向对象的概念,没有建立业务模型的思维。
    因此,我们应该将UI、数据库访问和域逻辑这三种类别的代码分离开。

抽取访问数据库的代码

    我们先抽取出访问数据库的代码。先将数据库中所有的参会者当作一个集合体,这个集合体里面没有重复的元素,元素的排列也没有顺序。这个集合体支持增加、删除、更新和罗列的操作,可以将它命名为Participants。

class Participant { int id; String name; String telNo; String region; ... } interface ParticipantIterator { boolean next(); Participant getParticipant(); } class Participants { Connection dbConn; Participants() { dbConn = ...; } void addParticipant(Participant part) { PreparedStatement st =
    dbConn.prepareStatement("insert into from participants values(?,?,?,?)");
try { st.setInt(1, part.getId()); st.setString(2, part.getName()); st.setString(3, part.getTelNo()); st.setString(4, part.getRegion()); st.executeUpdate(); } finally { st.close(); } } void removeParticipant(int partId) { ... } void updateParticipant(Participant part) { ... } ParticipantIterator getAllParticipantsById() { ... } ParticipantIterator getParticipantsWithNameById(String name) { ... } }

    在AddParticipantDialog中,我们还需要一个找出当前最大参会者ID的功能。因此,还要在这个集合体中定义一个getMaxId方法:

class Participants { Connection dbConn; void addParticipant(Participant part) { ... } int getMaxId() { PreparedStatement st = dbConn.prepareStatement("select max(id) from participants"); try { ResultSet rs = st.executeQuery(); try { rs.next(); return rs.getInt(1); } finally { rs.close(); } } finally { st.close(); } } ... }

    现在,AddParticipantDialog这个类可以简化为:

class AddParticipantDialog extends JDialog { Participants participants; JTextField id; JTextField name; JTextField telNo; JTextField region; AddParticipantDialog(Participants participants) { this.participants = participants; setupComponents(); } void setupComponents() { ... } void show() { showDefaultValues(); setVisible(true); } void showDefaultValues() { int nextId = participants.getMaxId()+1; id.setText(new Integer(nextId).toString()); name.setText(""); telNo.setText(""); region.setText("中国"); } void onOK() { if (name.getText().equals("")) { JOptionPane.showMessageDialog(this, "名称不能为空"); return; } if (!region.equals("中国") && !region.equals("美国") && !region.equals("欧洲")) { JOptionPane.showMessageDialog(this, "Region is unknown"); return; } Participant part = new Participant( Integer.parseInt(id.getText()), name.getText(), telNo.getText(), region.getText()); participants.addParticipant(part); dispose(); } }

    现在,AddParticipantDialog类已经简单很多。从这里面,我们看不到任何跟数据库有关的东西存在了。

抽取访问数据库代码后得到的灵活性

    因为AddParticipantDialog类现在操作的是一个参会者的集合体Participants,而不再是数据库中的表。就算现在我们想把这些参会者的信息存储在一个XML文件或一个简单的文本文件中也可以。我们只需要修改Participants这个类里面每个方法的具体实现就行,不用修改AddParticipantDialog。

class Participants { void addParticipant(Participant part) { //将参会者信息存在XML文件里 } void getMaxId() { //从XML文件中找出最大ID } }

    甚至,如果我们同时要用数据库存储和XML文件存储时,我们就将Participants类变成一个接口,然后在一个实现类中用数据库实现这个接口,另一个实现类中用XML文件:

interface Participants { void addParticipant(Participant part); int getMaxId(); ... } class ParticipantsInDB implements Participants { void addParticipant(Participant part) { ... } int getMaxId() { ... } } class ParticipantsInXMLFile implements Participants { void addParticipant(Participant part) { //将参会者信息存在XML文件里 } int getMaxId() { //从XML文件中找出最大ID } } class AddParticipantDialog extends JDialog { AddParticipantDialog(Participants participants) { ... } ... }

将域逻辑与UI分离

    现在,我们分离域逻辑和UI:

class Participant { int id; String name; String telNo; String region; ... static Participant makeDefaultParticipant() { return new Participant(0, "", "", "中国"); } void assertValid() throws ParticipantException { if (name.equals("")) { throw new ParticipantException("名称不能为空"); } if (!region.equals("中国") && !region.equals("美国") && !region.equals("欧洲")) { throw new ParticipantException("Region is unknown"); } } } class ParticipantException extends Exception { ParticipantException(String msg) { super(msg); } }

    现在,AddParticipantDialog可以简化为:

class AddParticipantDialog extends JDialog { Participants participants; JTextField id; JTextField name; JTextField telNo; JTextField region; AddParticipantDialog(Participants participants) { this.participants = participants; setupComponents(); } void setupComponents() { ... } void show() { showDefaultValues(); setVisible(true); } void showDefaultValues() { Participant newPart = Participant.makeDefaultParticipant(); newPart.setId(participants.getMaxId()+1); showParticipant(newPart); } void showParticipant(Participant part) { id.setText(new Integer(part.getId()).toString()); name.setText(part.getName()); telNo.setText(part.getTelNo()); region.setText(part.getRegion()); } Participant makeParticipant() throws ParticipantException { Participant part = new Participant( Integer.parseInt(id.getText()), name.getText(), telNo.getText(), region.getText()); part.assertValid(); return part; } void onOK() { try { participants.addParticipant(makeParticipant()); dispose(); } catch (Exception e) { JOptionPane.showMessageDialog(this, e.getMessage()); } } }

    现在,AddParticipantDialog类更加简单。准确地说,这个类基本上要做的事情就是,通过多个不同的UI组件显示一个参会者对象的信息(showParticipant),根据UI组件上的内容生成一个参会者对象(makeParticipant)。就此无他。

改进后的代码

    下面看一下改进完的代码:

class Participant { int id; String name; String telNo; String region; ... static Participant makeDefaultParticipant() { return new Participant(0, "", "", "中国"); } void assertValid() throws ParticipantException { if (name.equals("")) { throw new ParticipantException("名称不能为空"); } if (!region.equals("中国") && !region.equals("美国") && !region.equals("欧洲")) { throw new ParticipantException("Region is unknown"); } } } class ParticipantException extends Exception { ParticipantException(String msg) { super(msg); } } class Participants { Connection dbConn; Participants() { dbConn = ...; } void addParticipant(Participant part) { PreparedStatement st =
    dbConn.prepareStatement("insert into from participants values(?,?,?,?)")
try { st.setInt(1, part.getId()); st.setString(2, part.getName()); st.setString(3, part.getTelNo()); st.setString(4, part.getRegion()); st.executeUpdate(); } finally { st.close(); } } int getMaxId() { PreparedStatement st = dbConn.prepareStatement("select max(id) from participants"); try { ResultSet rs = st.executeQuery(); try { rs.next(); return rs.getInt(1); } finally { rs.close(); } } finally { st.close(); } } } class AddParticipantDialog extends JDialog { Participants participants; JTextField id; JTextField name; JTextField telNo; JTextField region; AddParticipantDialog(Participants participants) { this.participants = participants; setupComponents(); } void setupComponents() { ... } void show() { showDefaultValues(); setVisible(true); } void showDefaultValues() { Participant newPart = Participant.makeDefaultParticipant(); newPart.setId(participants.getMaxId()+1); showParticipant(newPart); } void showParticipant(Participant part) { id.setText(new Integer(part.getId()).toString()); name.setText(part.getName()); telNo.setText(part.getTelNo()); region.setText(part.getRegion()); } Participant makeParticipant() throws ParticipantException { Participant part = new Participant( Integer.parseInt(id.getText()), name.getText(), telNo.getText(), region.getText()); part.assertValid(); return part; } void onOK() { try { participants.addParticipant(makeParticipant()); dispose(); } catch (Exception e) { JOptionPane.showMessageDialog(this, e.getMessage()); } } }

SQLException的陷阱

    事实上,代码里还有一个问题。我们先认真地看一下Participants这个类:

class Participants { ... int getMaxId() { PreparedStatement st = dbConn.prepareStatement("select max(id) from participants"); try { ResultSet rs = st.executeQuery(); try { rs.next(); return rs.getInt(1); } finally { rs.close(); } } finally { st.close(); } } }

    每当我们调用prepareStatement、executeQuery、next和close这样的方法,JDBC都会抛出SQLException这个异常。但是,getMaxId这个方法本身不能处理该异常,我们只好将它抛出去,让外部调用getMaxId方法的地方处理。所以,我们要将代码改成:

class Participants { ... void getMaxId() throws SQLException { PreparedStatement st = dbConn.prepareStatement("select max(id) from participants"); try { ResultSet rs = st.executeQuery(); try { rs.next(); return st.getInt(1); } finally { rs.close(); } } finally { st.close(); } } }

    问题就在这里,每次AddParticipantDialog里调用getMaxId方法,它都要处理这个异常(要么catch住自己处理,要么继续向上抛)。

class AddParticipantDialog extends JDialog { ... void showDefaultValues() { newPart = Participant.makeDefaultParticipant(); try { newPart.setId(participants.getMaxId()+1); } catch (SQLException e) { ... } showParticipant(newPart); } }

    因为AddParticipantDialog现在要处理SQLException这样的异常,我们一眼就可以看出这个类又与数据库藕合,它不再是单纯操作参与者的集合体或者一个XML文件。也就是说,SQLException强制性地让我们的AddParticipantDialog陷入调用数据库的坑中。如果别的环境中不用数据库存储的话,这段客户端代码就无法重用,即使很勉强地重用了,也会变成很糟糕的代码。
    为了解决这样的问题,当Participants类碰到一个数据库的异常时,它不应该直接将这个异常抛出去,而是要抛出一个跟participants这个集合体本身有关的异常:

class ParticipantsException extends Exception { ParticipantsException(Throwable cause) { super(cause); } } class Participants { ... int getMaxId() throws ParticipantsException { try { PreparedStatement st =
     dbConn.prepareStatement("select max(id) from participants");
try { ResultSet rs = st.executeQuery(); try { rs.next(); return rs.getInt(1); } finally { rs.close(); } } finally { st.close(); } } catch (SQLException e) { //抛出和Participants有关的异常 throw new ParticipantsException(e); } } }

    现在,AddParticipantDialog类就不用处理SQLException这样的异常了:

class AddParticipantDialog extends JDialog { ... void showDefaultValues() { Participant newPart = Participant.makeDefaultParticipant(); try { newPart.setId(participants.getMaxId()+1); } catch (ParticipantsException e) { ... } showParticipant(newPart); } }

给系统分层

    我们可以将上面这个会议系统分为三层:数据库访问层、域逻辑层和UI层。
    因为Participants类访问了数据库,所以我们将它放在数据库访问层。因为Participant、ParticipantException和ParticipantsException处理了与域逻辑有关的事情,我们将它们放在域逻辑层。因为AddParticipantDialog是管理与用户交互的界面的类,我们将它放在UI 层。(如图1)


图1

    如果类A引用了类B,我们就从A画一个箭头到B。图2就是这个系统中类之间的引用(依赖)关系:


图2

    而层之间的依赖关系如图3:


图3

    从图中我们可以看出:
    1. 域逻辑层是唯一没有依赖于其他层的分层,所以这一层最容易重用。
    2. 数据库访问层依赖于域逻辑层,并没有依赖UI层。所以,不论是在文本文件存储系统中还是在XML存储系统中,这一层都可以重用。
    3. UI层依赖于其他两层,因为这一层最难重用。
    对于第3个观点(UI层依赖于其他两层),我们再想一下:我们不是已经让AddParticipantDialog(UI层)不知道数据库的存在了吗?怎么图里面还是显示这样的依赖关系?根据我们前面讲的,我们是让Participants变成一个接口,它只是定义了一些业务逻辑操作,但并没有具体的实现。具体的实现有多种情况。现在我们在数据库环境的系统中创建一个实现类叫ParticipantsInDB,这样,很明显的,Participants就属于域逻辑层,而ParticipantsInDB则属于数据库访问层(如图4)。


图4

    而层之间的依赖关系如图5:


图5

    现在,各个层次间就只有两种依赖关系:
    1. UI层依赖于域逻辑层。
    2. 数据库访问层依赖于域逻辑层。
    并不是所有的系统都可以做到分层间只有两个依赖关系。比如,我们刚开始的AddParticipantDialog类就不属于这样的好系统,我们甚至想把它归入某一层都做不到。只有结构比较好的系统才有可能做到这样。而这就是我们设计结构的目标(特别是大项目)。

很多东西都属于UI层

    很多东西都属于UI层。不仅仅窗口、按钮之些属于UI,报表、servlet、jsp和文本控制台等也属于UI层。因此,请不要在servlets里放置任何的域逻辑或者数据库访问代码。特别要关注那些Servlets里有很多代码的系统,它们是最有嫌疑的。也不要在域逻辑中调用System.out.print。

其他方法

    将数据库看作一个对象的集合体,只是我们在UI层隐藏数据库层很多方法中的一个。其他的方法可以从以下链接学习:
    http://c2.com/cgi/wiki?ScatterSqlEverywhere
    http://c2.com/cgi/wiki?ModelFirst
    http://c2.com/cgi/wiki?ObjectRelationalMapping
    http://www.agiledata.org/essays/mappingObjects.html
    http://www.martinfowler.com/articles/dblogic.html
    http://www.martinfowler.com/eaaCatalog
    http://www.objectmentor.com/resources/articles/Proxy.pdf

0
相关文章