結合 uml 所學和 Javafx 從建模到實現一個子功能模塊 —— 日程管理。新手上路,類圖到代碼實現的過程還是很曲折但所幸收獲頗豐,記錄一下學習心得。
日程功能模塊
最后成果
JAVAFX里面沒有封裝日歷控件,找了些項目源碼做參照肝了一個,不過為了簡化分析的過程,不會詳細寫其中業務邏輯。
總的來說,從建模帶代碼實現的功能完成了90%,日程列表和日歷的通信沒寫,預期效果是如果有新增的日程,日歷上相應的日歷塊會有 marked 的標記。這一塊按照我的想法寫代碼會變得很亂,也是建模過程中沒有認真考慮的點。


建模過程
需求
日程主要是幫助用戶查看和管理日常事務,用戶可以記錄待辦事件並且設置提醒時間,有助於管理時間和提高工作效率。
而作為成績管理系統學生界面的一個子功能模塊,除了幫助學生管理課程和學習任務,它還需要能自動導入課程考試信息,便於學生規划學習進度。
用例圖及主要用例描述
- 用例圖
從需求分離出對象為學生。把所有有意義的動賓短語先列出來【查看日常事務】,【管理日常事務】,【記錄待辦事件】,【設置提醒時間】,【自動導入課程考試信息】。這里是因為需求比較直觀簡單,一般還需要根據語義找隱藏的功能需求。
進一步分析用例之間的關系,【管理日程事務】即對日程做刪改,應該是在選定具體的事務后。【記錄待辦事件】即新建日程,包括【設置提醒時間】等信息設置。【自動導入課程信息】應該不是由學生完成的,學生只能查看,所以還有參與者 —— 考試管理系統。
畫出用例圖如下
心得:在畫用例圖時並沒有花太多時間打磨,構建的快也修改的快。從需求快速提取用例,在寫用例描述的時候還會再倒回來修改的

參考老師發的資料,從用例圖如何到類圖,觀點不一。有從活動圖 ——> 類圖以及從詳細的用例描述中抽象出類圖,兩種都參考嘗試了一下。
- 用例描述
| 用例編號 | S1.1 |
| 用例名稱 | 查看日程詳細信息 |
| 參與者 | 學生 |
| 觸發條件 | 點擊列表中具體的一項日程 |
| 前置條件 | 學生已經登錄,並且在日程界面 |
| 后置條件 | 顯示目標日程的詳細信息 |
| 正常流程 | 1. 點擊列表中目標日程,顯示日程信息 |
| 擴展流程 | 1. 編輯目標日程,修改事件信息 2. 刪除目標日程 |
| 特殊要求 | 無 |
| 用例編號 | S1.2 |
| 用例名稱 | 新增日程 |
| 參與者 | 學生 |
| 觸發條件 | 點擊“新增日程”按鈕 |
| 前置條件 | 學生已經登錄,並且在日程界面 |
| 后置條件 | 添加了新的日程到日歷,對應日期格顯示 marked |
| 正常流程 | 1. 點擊“新增日程”按鈕,打開日程創建面板 2. 填寫事件,設置時間段 3. 選擇是否設置提醒時間 4. 點擊“提交”按鈕 |
| 擴展流程 | 1. 取消創建日程 |
| 特殊要求 | 無 |
| 用例編號 | S1.3 |
| 用例名稱 | 查看考試安排 |
| 參與者 | 學生 |
| 觸發條件 | 點擊“查看考試安排”按鈕 |
| 前置條件 | 學生已經登錄,並且在日程界面 |
| 后置條件 | 顯示顯示本學期所有考試安排 |
| 正常流程 | 1. 點擊“查看考試安排”按鈕,顯示考試安排面板 |
| 擴展流程 | 1. 添加提醒 |
| 特殊要求 | 無 |
活動圖


較為傾向於從用例描述中抽象出類,老師發的資料中也寫到,用例描述占據着皇后的位置,而三王一后中沒有出現活動圖。我在寫完用例描述后對程序也已經有了輪廓。

類圖 <重中之重>
學習博客【深入淺出UML類圖】
從用例描述中,抽象出所有的類。我們先提取實體類,有日程類,以及填充日歷的日期格類。邊界類這里就是界面類,有日程主界面類,添加日程的界面類,兩個界面類分別都有控制類實現相關的業務邏輯。界面類里面的部件主要是日歷類,日程列表類,考試列表類。

先把實體類的屬性寫好,日程類我們很容易可以知道,有日程名稱,開始和截止時間,提醒時間。用戶在填寫事件時,可能還有一些額外的信息需要提醒自己,那么就添加一個事件備注屬性。
日期格類應該包含的是當前格子表示的日期,因為我們還可以直接看到這個格子是否有日程,應該是抽象為一個狀態。這里我預備用布爾型 isMarked 來表示。(當時寫的時候還加了Mark屬性,作為如果存在日程的標記,多余了。
然后進一步思考類之間的關系,先看聚合關系,ScheduleList 和 ExamList 都是由 Schedule 聚合而來,但 ExamList 屬於特殊的日程,它在這里只能查看不能修改。日期格和日歷Calendar也是聚合關系,我的想法是按月顯示,那關系就是一個日歷中由35個日期格。
考試列表,日程列表和日歷都屬於主界面的部件。最后得到類圖如下

代碼實現
用starUML的正向工程工具根據上面畫好的類圖導出所有的代碼。然后用 sceneBuilder 開始頁面布局。
主界面我直接用的做平時成績管理系統的界面稍加改動,已經有基本布局和css渲染。添加新日程界面根據用例描述,就是有對日程基本信息的編輯,然后確認取消按鍵。
下面是靜態初始界面,還沒有實現任何功能只是個UI。


編寫界面類,主界面繼承 Application 類。添加日程是由主界面按鈕觸發彈出的,暫時不寫。
public class ScheduleStage extends Application {
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage stage) throws Exception {
Parent root = FXMLLoader.load(getClass().getResource("resource/ScheduleStage.fxml"));
stage.setTitle("Student Schedule");
stage.setScene(new Scene(root, 1080, 720));
stage.setResizable(false);
stage.centerOnScreen();
stage.show();
}
}
開始編寫代碼,正向工程導入后已經有了屬性,實體類只需要編寫setter和getter方法以及構造器

日歷是用GirdPane寫的,每一個日期格都繼承AnchorPane,便於在內部進行布局

兩個 List 部件類
ScheduleList 有 Schedule 的聚合,初始化我們從數據庫導入數據(因為用到數據庫的地方較少,就不分離出來的(絕不是懶:/,刪除和修改的方法這里暫時不寫。
ExamList 直接從數據庫初始化數據之后不再會變化,所以它包含的應該是 final static 的 Schedule 數組,與上面類似就已經完成了。

日歷類 Calendar 和兩個控件按鈕的行為
日歷類其中涉及細節較多,這里把它當作已經封裝好的日歷控件FXCalendar。根據類圖,我們要完成的時在按上月和下月的按鈕時,日歷要做出變化。
兩個按鈕觸發的行為實現代碼
@FXML
void onButtonLastMonthClicked(ActionEvent event){
LocalDateTime now = LocalDateTime.of(Integer.parseInt(labelYear.getText()), Month.valueOf(labelMonth.getText()), 1, 0, 0, 0);
now = now.minusMonths(1);
labelYear.setText(String.valueOf(now.getYear()));
labelMonth.setText(String.valueOf(now.getMonth()));
changeCalendar(now.getYear(), String.valueOf(labelMonth.getText()));
}
@FXML
void onButtonNextMonthClicked(ActionEvent event){
LocalDateTime now = LocalDateTime.of(Integer.parseInt(labelYear.getText()), Month.valueOf(labelMonth.getText()), 1, 0, 0, 0);
now = now.plusMonths(1);
labelYear.setText(String.valueOf(now.getYear()));
labelMonth.setText(String.valueOf(now.getMonth()));
changeCalendar(now.getYear(), String.valueOf(labelMonth.getText()));
}
可以看到上面調用了 changeCalendar() 方法來實現日歷的變化,下面是 changeCalendar() 代碼實現
private void changeCalendar(int year, String month){
DateTimeFormatter formatter = new DateTimeFormatterBuilder()
.parseCaseInsensitive()
.appendPattern("yyyy MMMM")
.toFormatter(Locale.ENGLISH);
populateDate(YearMonth.parse(year + " " + month, formatter));
selectedMonth = month;
}
兩個界面的交互和添加日程界面完善
在主界面按下添加日程按鍵是觸發新增日程界面信息,每次都會產生一個新的界面,一個主界面可以有多個添加日程界面。
/*
* 添加新的日程
* */
@FXML
void onButtonAddNewClicked(ActionEvent event) {
Stage addNewStage = new Stage();
Parent root = null;
try {
root = FXMLLoader.load(getClass().getResource("resource/AddNewSchedule.fxml"));
} catch (IOException e) {
e.printStackTrace();
}
addNewStage.initStyle(StageStyle.UNDECORATED);
addNewStage.setTitle("Student");
addNewStage.setScene(new Scene(root, 600, 450));
addNewStage.centerOnScreen();
addNewStage.show();
}
我們從類圖中得知,主要有填入事件信息,是否需要提醒,確認添加和取消操作。(這里在編寫的時候就發現,類圖的不足之處,打開提醒應該是由控制類來完成。
填入事件信息是在界面中完成的
確認添加事件 getNewScheduleButton()
/*
* 將新增的事件放入Schedule
* */
@FXML
void getNewScheduleButton(ActionEvent event) {
String name = itemName.getText();
String comment = itemRemark.getText();
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
String btime = beginDate.getValue() + " " + beginTime.getValue();
String etime = endDate.getValue() + " " + endTime.getValue();
newSchedule = new Schedule(name, comment, LocalDateTime.parse(btime, dtf), LocalDateTime.parse(etime, dtf));
if(isRemindTogButton.isSelected()) {
newSchedule.setRemind(true);
newSchedule.setReminderTime(LocalDateTime.parse(remindDate.getValue() + " " + remindTime.getValue(), dtf));
}
addNew(newSchedule);
closeAddNewStage(event);
}
確認和取消都會觸發窗口的關閉,而窗口是在主界面控制類生成的,在這里需要獲取當前按鈕所在窗口來關閉
@FXML
void closeAddNewStage(ActionEvent event) {
Stage stage = (Stage) closeButton.getScene().getWindow();
stage.close();
}
是否提醒
/*
* 是否打開提醒
* */
private JFXTimePicker remindTime = new JFXTimePicker();
private JFXDatePicker remindDate = new JFXDatePicker();
@FXML
void isReminding(ActionEvent event) {
boolean isSelected = isRemindTogButton.isSelected();
if(isSelected) {
remindDate.setDefaultColor(Paint.valueOf("#0442bf"));
remindTime.setDefaultColor(Paint.valueOf("#0442bf"));
remindDate.setPrefSize(153, 23);
remindTime.setPrefSize(153, 23);
remindTime.setTranslateY(15);
remindDate.setTranslateY(15);
remindHBox.getChildren().addAll(remindDate, remindTime);
}
else {
remindHBox.getChildren().remove(1, 3);
}
}
到這里,添加日程界面和控制類都完成了
最后要做的是刪除選中日程和查看選中日程deleteSchedule(in schedule:Schedule),showScheduleDetail(schedule:Schedule)
這里有個十分迷惑的小bug,雖然處理了。但還是不知道為什么,希望有大佬解惑
ClickedID是在監聽日程列表中被鼠標選中的事件編號。
/*
* 刪除選中日程
* */
@FXML
void deleteButtonOnAction(ActionEvent event) {
System.out.println(clickedId);
if(clickedId == -1) return;
observableList.remove(clickedId);
// 十分神奇的bug!!!在observableList移除最后一個元素后,clickedId自動從0變成-1,故加下面這句
if(clickedId == -1) clickedId++;
System.out.println(clickedId);
delete(clickedId);
}
查看選中日程時,生成對話框來提示選中日程的所有細節。
/*
* 顯示選擇日程細節
* */
@FXML
void showDetailButtonOnAction(ActionEvent event) {
if(clickedId == -1) return;
Schedule schedule = list.get(clickedId);
JFXAlert alert = new JFXAlert(showDetailButton.getScene().getWindow());
alert.initModality(Modality.APPLICATION_MODAL);
alert.setOverlayClose(false);
JFXDialogLayout layout = new JFXDialogLayout();
Label label = new Label(schedule.getItemName());
label.setFont(new Font("Cambria", 32));
layout.setHeading(label);
Label newContent = new Label("備注: " + schedule.getItemRemark()
+ "\n開始時間: " + schedule.getStartDate()
+ "\n結束時間: " + schedule.getEndDate()
+ "\n提醒時間: " + schedule.getReminderTime());
newContent.setFont(new Font("Cambria", 16));
layout.setBody(newContent);
JFXButton closeButton = new JFXButton("確 認");
closeButton.setPrefSize(150,55);
closeButton.setFont(new Font("Cambria", 16));
closeButton.getStyleClass().add("dialog-accept");
closeButton.setOnAction(e -> alert.hideWithAnimation());
layout.setActions(closeButton);
alert.setContent(layout);
alert.show();
}
list 做出的相應操作

總結心得
從類圖到代碼仍舊花了不少時間在不斷思考如何組織和實現,一度想不管結構全部累在一起,這里類圖起了一個很大的規范作用。它在設計階段,規范好整個框架,讓我先對業務流程有了大致的輪廓。如果感覺有錯誤,可以在類圖階段就修改,而不是等到實現時修改代碼,減小犯錯成本。
其實代碼實現后,我還返回去修改了類圖
一個是命名規范,當時設計類圖命名比較草率,導致在代碼累積下來后不能見名知意,所以重構了代碼並且修改類圖中類和方法的命名;
第二個是方法的參數和返回值,類圖設計時我對每個方法都寫了形參和返回值,但具體實現時大概率會發生變化,比如刪除日程那只需要傳遞日程ID,而不是把Schedule傳過去;
類圖上的時間安排
其出現兩個問題,其一是在根據類圖實現代碼時發現有些細節沒有考慮到,需要在實現時再花時間來設計。其二是根據類圖的設計無從下手,有我Java功底尚淺的原因,但也可能是因為設計的不合理。
類圖設計的快,可能會有細節被忽略;類圖設計的慢,不斷打磨精細,如果后續要修改,可能因為投入了較多的時間成本不想修改。
這次從建模到實現,收獲很大,真的感受到一個好的建模可以讓整個功能實現更加高效。我的建模可能還很不規范,之后還是多實踐和總結!
