今天再帖出在“插件项目实战”一章中关于建模的。内容虽然简单,但其中的方法我认为还是很重要的,因为在浏览很多帖子发现在建模时,还是有不少争论的,我估计至少有70%的Java程序员,无法很好的做到面向对象设计和分析,本节多少也反映了我的一些经验和观点吧,希望对大家有所帮助。
*******************************************************
作者:陈刚,普通程序员,曾有幸以Eclipse插件方式开发过一个中型软件。现将所学
付诸于纸,暂取书名<Eclipse插件开发指南>,将于2005年初由清华大学出版社出版。
blog:http://blog.csdn.net/glchengang/
*******************************************************
8.2 面向对象分析和数据表创建(版本V0010)
8.2.1 界面效果及实现功能
本章项目是编写一个学生成绩管理软件,由于主要目的是给出一个项目开发的示例,所以这个软件的功能是做得相当简单的,也不存在需求分析阶段。关于学生成绩管理软件的一些功能及概念,也不再多加说明,毕竟大家都是从学校和考试里走出来的,对这些已经很熟悉了。
本章项目的主体界面框架如下图8.19所示:
图8.19 主体界面框架
功能说明:
l 左上部是主功能导航器视图(简称为主功能导航器或主功能视图),其中提供了一个功能结点树,本章将实现“档案管理”和“成绩管理”两个结点的功能。
l 右部是一个编辑器,当单击“档案管理”结点时将生成一个编辑器。
l 左下部是成绩管理的搜索视图,可以根据这个视图设置的搜索条件,查询出相应的考试成绩。
l 右部还有一个名为“2003-12-11段考”的编辑器,当单击左下部的“搜索”按钮时将生成此编辑器,如下图8.20所示:
图8.20 成绩编辑器
8.2.2 面向对象的分析与设计
面向对象的分析与设计,也称OOAD(Object Oriented Analyse Design)。因为它能够更准确自然的用软件语言来描述现实事物,并使得在它基础上构建的软件具有更好的复用率、扩展性及可维护性,所以OOAD是当前最重要的软件方法学之一。
OOAD和Rose、Together等UML软件没有必然的关系,OOAD是一种方法,UML是描述这种方法的图形语言,而Rose等则是使用UML的具体工具。OOAD的关键在于思维方式的转变,而不是工具的使用,即使只用铅笔和白纸也可以成为一个优秀OOAD专家。
现在大学的课程以C、Basic、VB、FoxPro居多,即使是用C++、Java,也是可以用面向过程的方式来编写程序,所以使用面向对象的语言并不代表你是以面向对象的方式来思考和编程。徒具对象的形,而无对象的神,是现在一般程序员的最大缺陷所在。
以本项目为例,大多数习惯于面向过程的编程思维方式的开发人员,一般在做完需求分析后,便开始设计数据库的表结构,而在编码阶段才开始考虑根据表结构来进行对象的设计与创建,这种开发方式就是带有过去很深的面向过程、面向数据库表编程的烙印。
所谓“万物皆对象”,OOAD应该是把对象做为思考的核心,而不是仅仅把“对象”当成一种编程的手段,应当先完成对象设计,然后再根据对象创建表,这是最基本的次序。
当然这种方式在转化成数据库时会遇到一些困难和阻力,毕竟数据库不是面向对象的,SQL语言也不是面向对象的。但Hibernate、JDO、EJB等数据库持久化技术,已经可以让开发者用完全的面向对象方式来编程,而不必忍受“对象”到“关系”转化的痛苦。
为了让读者可以了解如何手工完成“对象”到“关系”的转化,本插件项目仍然使用纯JDBC方式来实现。在第9章会讲解Hibernate的使用,所谓“先苦后甜”,通过两种方式的比较,读者能更深的体会Hibernate等数据库持久化技术的美妙之处。
本章的学生成绩管理软件有以下对象:学生、老师、年级、班级、课程、成绩、考试,本项目所有对象创建在cn.com.chengang.sms.model包下,如下图8.21所示。接下来会具体分析一下这些对象,并给出其源代码和UML类图。
图8.21 数据对象所在的包
1、用户对象:学生、老师
这个系统有可能会存在一个前台网站,比如:老师用Eclipse做客户端来管理成绩,而学生则通过一个网页来查询成绩,所有的数据集中在学校的中心服务器上。因此系统的用户有两种:学生、老师,这两种用户有一些信息是相同的,有些则不同。比如他们都有用户名、姓名、密码等,而学生没有老师的课程属性,老师则没有学生的班级属性。
由上面的分析,我们将两种用户的共性抽象成一个接口:IUser,这个接口有如下属性:数据库ID号(Id)、用户名(userId)、密码(password)、姓名(name)、最后登录时间(latestOnline)。另外,学生类(Student)有班级属性(SchoolClass),老师类(Teacher)则有课程(Course)属性,学生类和老师类都实现于IUser接口。
将用户抽象成一个接口的另一个好处就是:使用户类置于同一个规范之下。今后要新增加一个种类型的用户,比如:家长用户,只需要再实现IUser接口即可。“接口”是用Java进行OOAD开发的一个最重要的概念,也是成为一个优秀的Java设计师所必须掌握和熟练使用的概念。
其他说明:类的实例变量有多种叫法:通用的名称是“实例变量”或“属性”;在实体类中因为和数据表的字段相对应,也可称之为“字段”;有些书籍文章也称之为“域”。
先给出用户类的UML设计图,如下图8.22所示:
图8.22 用户类的UML类图
用户类的源代码如下:
(1)用户接口IUser
package cn.com.chengang.sms.model;
import java.util.Date;
public interface IUser {
/**
* 得到数据库ID
*/
public Long getId();
/**
* 设置数据库ID
*/
public void setId(Long id);
/**
* 得到用户名
*/
public String getUserId();
/**
* 设置用户名
*/
public void setUserId(String userId);
/**
* 得到密码
*/
public String getPassword();
/**
* 设置密码
*/
public void setPassword(String password);
/**
* 得到用户姓名
*/
public String getName();
/**
* 设置用户姓名
*/
public void setName(String name);
/**
* 得到最后登录时间
*/
public Date getLatestOnline();
/**
* 设置最后登录时间
*/
public void setLatestOnline(Date date);
}
程序说明:
l 接口规定只能定义方法,不能定义属性变量,所以本例只定义了用户各属性的set/get方法。
l 接口定义的方法前面是否有public或abstract都是一样的,本例加了public,你也可以去除,两者效果相同。
l 这里需要注意的是Date对象是java.util.Date,不要和java.sql.Date混淆。
(2)实现接口IUser的抽象类AbstractUser
每一个具体用户类(学生、老师)都要实现一遍接口IUser中定义的方法,而这些方法的代码都是一样的,所以我们用一个抽象类AbstractUser来统一实现IUser接口中的公共属性,我们把这种抽象类称之为“默认实现抽象类”。AbstractUser不仅提供了方法的实现,也提供了属性变量的定义,所有的用户子类都将继承并拥有这些属性。
AbstractUser类的具体代码如下:
package cn.com.chengang.sms.model;
import java.util.Date;
abstract class AbstractUser implements IUser {
private Long id; //数据库ID
private String userId; //用户名
private String password; //密码
private String name; //姓名
private Date latestOnline;//最后登录时间
/********以下为接口IUser的实现方法***********/
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Date getLatestOnline() {
return latestOnline;
}
public void setLatestOnline(Date latestOnline) {
this.latestOnline = latestOnline;
}
}
(3)学生类Student
学生类Student继承自抽象类AbstractUser,所以也拥有了抽象类中所有的属性和方法,因此这里只需定义学生类独有的属性和方法。
package cn.com.chengang.sms.model;
public class Student extends AbstractUser {
//学生所属班级,为了避免和类(class)的名称混淆,将其命名为SchoolClass
private SchoolClass schoolclass;
/**
* 得到学生所在班级
*/
public SchoolClass getSchoolclass() {
return schoolclass;
}
/**
* 设置学生所在班级
*/
public void setSchoolclass(SchoolClass schoolclass) {
this.schoolclass = schoolclass;
}
}
(4)老师类Teacher
package cn.com.chengang.sms.model;
import java.util.HashSet;
import java.util.Set;
public class Teacher extends AbstractUser {
private Set courses = new HashSet(); //所教课程
/**
* 得到所有课程
*/
public Set getCourses() {
return courses;
}
/**
* 设置一批课程
*/
public void setCourses(Set courses) {
this.courses = courses;
}
/**
* 增加一个课程
*/
public void addCourse(Course course) {
courses.add(course);
}
/**
* 删除一个课程
*/
public void removeCourse(Course course) {
courses.remove(course);
}
/**
* 清除所有课程
*/
public void clearCourses() {
courses.clear();
}
/**
* 该老师是否教这个课
*/
public boolean isCourse(Course course) {
return courses.contains(course);
}
}
程序说明:
l 我们将课程也看作是一种对象,命名为Course,在后面将会给出它的代码。老师和课程是多对多的关系:一个老师有可能教多门课程,一门课程也可能有几个老师来教。当一个对象对应多个对象的情况时,比如老师,就需要一个Java集合(Collection)来存放这些课程,集合中的一个元素就是一门课程。
l 在List和Set两种集合中,本例选择了Set型集合。Set的特性是其包含的元素不会重复(如果加入重复的元素也不会出错,等于没有加),但Set中的元素是无序排列的,如果先加入“语文”后加入“数学”,以后取出显示时未必“语文”会在“数学”之前。List型集合则不同,它按加入的先后顺序排列,而且允许加入重复的元素。
l Set是一个接口,它实际使用的类是HashSet,在定义对象时应尽量使用效宽泛的类型,以便拥有更好的扩展性。
l 老师类的课程属性在set/get方法的基础上再加了三个方法:增加课程、删除课程、判断此老师是否教授某课程,加入这些方法主要是为了今后使用方便。
l 因为在类的isCourse、clearCourses、addCourse等方法中,当courses为空时都会出错,所以为了方便,在定义courses属性时,马上赋了一个HashSet值给它。
2、课程(Course)、班级(SchoolClass)、年级(Grade)对象
这三个对象比较简单。其源代码如下:
(1)课程类Course
package cn.com.chengang.sms.model;
public class Course {
private Long id;
private String name; //课程名:数学、语文
public Course() {}
public Course(Long id, String name) {
this.id = id;
this.name = name;
}
/*********属性相应的set/get方法*************/
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
程序说明:
l 对于课程这种记录条数很少的属性,似乎没有必要用Long型,但为了整体上的统一,因此所有对象的id都用Long类型。
l 这里为了在创建对象时方便,新增加了一个构造函数Course(Long id, String name) 。
(2)班级类SchoolClass
package cn.com.chengang.sms.model;
public class SchoolClass {
private Long id;
private String name; //班级:43班、52班
private Grade grade; //该班级所属年级
public SchoolClass() {}
public SchoolClass(Long id, String name, Grade grade) {
this.id = id;
this.name = name;
this.grade = grade;
}
/*********属性相应的set/get方法*************/
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Grade getGrade() {
return grade;
}
public void setGrade(Grade grade) {
this.grade = grade;
}
}
(3)年级类Grade
package cn.com.chengang.sms.model;
public class Grade {
private Long id;
private String name; //年级名:大一、初三
public Grade() {}
public Grade(Long id, String name) {
this.id = id;
this.name = name;
}
/*********属性相应的set/get方法*************/
public Grade(int id, String name) {
this.id = new Long(id);
this.name = name;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
(4)三个类的UML图,如下图8.23所示:
图8.23 课程、班级、年级的UML图
3、学生成绩(StudentScore)、考试(Exam)对象
学生的成绩一般要包含如下信息:是哪位学生的成绩、是哪一次考试、这位学生的得分是多少等。在这里我们将考试的信息抽取出来单独构成一个考试(Exam)对象。
l 学生成绩的属性有:学生对象、考试对象、分数。
l 学生对象前面已经给出了,分数是一个实数。
l 而考试对象包含如下属性:考试名称、监考老师、考试的课程、考试的班级、考试时间。如果有必要,还可以加入更多的属性字段,如:考试人数、及格人数、作弊人数等。
(1)学生成绩类StudentScore
package cn.com.chengang.sms.model;
public class StudentScore {
private Long id;
private Exam exam; //考试实体
private Student student; //学生
private float score; //得分
/*********属性相应的set/get方法*************/
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public float getScore() {
return score;
}
public void setScore(float score) {
this.score = score;
}
public Student getStudent() {
return student;
}
public void setStudent(Student student) {
this.student = student;
}
public Exam getExam() {
return exam;
}
public void setExam(Exam exam) {
this.exam = exam;
}
}
(2)考试类Exam
package cn.com.chengang.sms.model;
import java.util.Date;
public class Exam {
private Long id;
private String name; //考试名称,如:2004上半学期143班期未语文考试
private Teacher teacher; //监考老师
private Course course; //考试的课程
private SchoolClass schoolClass;//考试班级
private Date date; //考试时间
/*********属性相应的set/get方法*************/
public Course getCourse() {
return course;
}
public void setCourse(Course course) {
this.course = course;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public SchoolClass getSchoolClass() {
return schoolClass;
}
public void setSchoolClass(SchoolClass schoolClass) {
this.schoolClass = schoolClass;
}
public Teacher getTeacher() {
return teacher;
}
public void setTeacher(Teacher teacher) {
this.teacher = teacher;
}
public Date getDate() {
return date;
}
public void setDate(Date date) {
this.date = date;
}
}
(3)两类的UML图,如下图8.24所示
图8.24 学生成绩、考试的类图
4、总结
在年级、班级等对象设计的时候,还有一种可能的做法是――取消这些对象,并在学生类中直接使用字符型的年级、班级属性。这种方式在编程上似乎要方便一些,但不符合数据库的设计规范,它主要有以下缺点:
l 数据冗余 - 如果还需要增加一个“班主任”的属性,则本书的做法只需在班级类中再加一个属性,而后一种做法则需要在学生类中再加入一个班主任的属性。一个班有数十个学生,他们的老师都是一样的,这样就产生了大量的数据冗余。
l 修改不方便 - 如果要更改班级的名称,则本书的做法只需要修改班级表中的一条记录,而后一种做法则要更新学生表中所有的班级字段。
l 一致性差 - 后一种做法有可能存在一致性问题,比如某个班级也许会在学生表中存在多种名称:43、43班、高43班等等。
实践建议:
l 在设计对象时,应该保持对象的细粒度。比如:成绩对象、考试对象的设计就是遵循这个原则。可能有些人会将考试对象取消,而将其属性合并到成绩对象中,这样做是不对的,并且以后也会造成数据表的数据冗余。
l 尽量为每个实体对象(表),增加一个和业务逻辑没有关系的标识属性(字段),例如本例中的自动递增属性(字段)id。在速度和可扩展性之间平衡后,建议将它定义成java.lang.Long类型。
l 设计数据库尽量依照数据库设计范式来做,不要为了书写SQL语句方便,而将同一字段放在多个表中,除非你对查询速度的要求极高。而且要知道这样做会导致今后数据库维护和扩展的困难,并且在更新数据时将需要更新多个表,一样增加了复杂度。
l 实体对象是一种纯数据对象,和数据库表有着一定程度上的对应关系,但又不是完全对应。切记不要在实体对象中加入业务逻辑或从数据库里取数据的方法,应该让其与业务逻辑的完全分离,保证实体对象做为纯数据对象的纯洁性,这样可以让它具有更高的复用性。
其他说明:本节创建的对象称之为实体对象,它是由EJB中的EntityBean提出的概念,本文采用实体对象(实体类)的称法。也可称POJO(Plain Old Java Object,简单原始的Java对象),在Hibernate中使用POJO的称法较多。