結合 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個日期格。
考試列表,日程列表和日曆都屬於主介面的部件,包括主介面在內的業務邏輯都由 ScheduleController 來處理。最後得到類圖如下
程式碼實現
用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功底尚淺的原因,但也可能是因為設計的不合理。
類圖設計的快,可能會有細節被忽略;類圖設計的慢,不斷打磨精細,如果後續要修改,可能因為投入了較多的時間成本不想修改。
這次從建模到實現,收穫很大,真的感受到一個好的建模可以讓整個功能實現更加高效。我的建模可能還很不規範,之後還是多實踐和總結!