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的稱法較多。