日程功能模組【從建模到程式碼實現】UML + JavaFX

01kkkrill發表於2021-11-28

結合 uml 所學和 Javafx 從建模到實現一個子功能模組 —— 日程管理。新手上路,類圖到程式碼實現的過程還是很曲折但所幸收穫頗豐,記錄一下學習心得。

日程功能模組

最後成果

JAVAFX裡面沒有封裝日曆控制元件,找了些專案原始碼做參照肝了一個,不過為了簡化分析的過程,不會詳細寫其中業務邏輯。
總的來說,從建模帶程式碼實現的功能完成了90%,日程列表和日曆的通訊沒寫,預期效果是如果有新增的日程,日曆上相應的日曆塊會有 marked 的標記。這一塊按照我的想法寫程式碼會變得很亂,也是建模過程中沒有認真考慮的點。

建模過程

需求

日程主要是幫助使用者檢視和管理日常事務,使用者可以記錄待辦事件並且設定提醒時間,有助於管理時間和提高工作效率。
而作為成績管理系統學生介面的一個子功能模組,除了幫助學生管理課程和學習任務,它還需要能自動匯入課程考試資訊,便於學生規劃學習進度。

用例圖及主要用例描述

  • 用例圖
    從需求分離出物件為學生。把所有有意義的動賓短語先列出來【檢視日常事務】,【管理日常事務】,【記錄待辦事件】,【設定提醒時間】,【自動匯入課程考試資訊】。這裡是因為需求比較直觀簡單,一般還需要根據語義找隱藏的功能需求。
    進一步分析用例之間的關係,【管理日程事務】即對日程做刪改,應該是在選定具體的事務後。【記錄待辦事件】即新建日程,包括【設定提醒時間】等資訊設定。【自動匯入課程資訊】應該不是由學生完成的,學生只能檢視,所以還有參與者 —— 考試管理系統。
    畫出用例圖如下
    心得:在畫用例圖時並沒有花太多時間打磨,構建的快也修改的快。從需求快速提取用例,在寫用例描述的時候還會再倒回來修改的
    image

參考老師發的資料,從用例圖如何到類圖,觀點不一。有從活動圖 ——> 類圖以及從詳細的用例描述中抽象出類圖,兩種都參考嘗試了一下。

  • 用例描述
用例編號 S1.1
用例名稱 檢視日程詳細資訊
參與者 學生
觸發條件 點選列表中具體的一項日程
前置條件 學生已經登入,並且在日程介面
後置條件 顯示目標日程的詳細資訊
正常流程 1. 點選列表中目標日程,顯示日程資訊
擴充套件流程 1. 編輯目標日程,修改事件資訊 2. 刪除目標日程
特殊要求
用例編號 S1.2
用例名稱 新增日程
參與者 學生
觸發條件 點選“新增日程”按鈕
前置條件 學生已經登入,並且在日程介面
後置條件 新增了新的日程到日曆,對應日期格顯示 marked
正常流程 1. 點選“新增日程”按鈕,開啟日程建立皮膚 2. 填寫事件,設定時間段 3. 選擇是否設定提醒時間 4. 點選“提交”按鈕
擴充套件流程 1. 取消建立日程
特殊要求
用例編號 S1.3
用例名稱 檢視考試安排
參與者 學生
觸發條件 點選“檢視考試安排”按鈕
前置條件 學生已經登入,並且在日程介面
後置條件 顯示顯示本學期所有考試安排
正常流程 1. 點選“檢視考試安排”按鈕,顯示考試安排皮膚
擴充套件流程 1. 新增提醒
特殊要求

活動圖

image
image


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

類圖 <重中之重>

學習部落格【深入淺出UML類圖】
從用例描述中,抽象出所有的類。我們先提取實體類,有日程類,以及填充日曆的日期格類。邊界類這裡就是介面類,有日程主介面類,新增日程的介面類,兩個介面類分別都有控制類實現相關的業務邏輯。介面類裡面的部件主要是日曆類,日程列表類,考試列表類。
image

先把實體類的屬性寫好,日程類我們很容易可以知道,有日程名稱,開始和截止時間,提醒時間。使用者在填寫事件時,可能還有一些額外的資訊需要提醒自己,那麼就新增一個事件備註屬性。
日期格類應該包含的是當前格子表示的日期,因為我們還可以直接看到這個格子是否有日程,應該是抽象為一個狀態。這裡我預備用布林型 isMarked 來表示。(當時寫的時候還加了Mark屬性,作為如果存在日程的標記,多餘了。
然後進一步思考類之間的關係,先看聚合關係,ScheduleList 和 ExamList 都是由 Schedule 聚合而來,但 ExamList 屬於特殊的日程,它在這裡只能檢視不能修改。日期格和日曆Calendar也是聚合關係,我的想法是按月顯示,那關係就是一個日曆中由35個日期格。
考試列表,日程列表和日曆都屬於主介面的部件,包括主介面在內的業務邏輯都由 ScheduleController 來處理。最後得到類圖如下
image

程式碼實現

用starUML的正向工程工具根據上面畫好的類圖匯出所有的程式碼。然後用 sceneBuilder 開始頁面佈局。

主介面我直接用的做平時成績管理系統的介面稍加改動,已經有基本佈局和css渲染。新增新日程介面根據用例描述,就是有對日程基本資訊的編輯,然後確認取消按鍵。
下面是靜態初始介面,還沒有實現任何功能只是個UI。
image
image

編寫介面類,主介面繼承 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方法以及構造器

image

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

兩個 List 部件類

ScheduleList 有 Schedule 的聚合,初始化我們從資料庫匯入資料(因為用到資料庫的地方較少,就不分離出來的(絕不是懶:/,刪除和修改的方法這裡暫時不寫。
ExamList 直接從資料庫初始化資料之後不再會變化,所以它包含的應該是 final static 的 Schedule 陣列,與上面類似就已經完成了。
image

日曆類 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 做出的相應操作
image

總結心得

從類圖到程式碼仍舊花了不少時間在不斷思考如何組織和實現,一度想不管結構全部累在一起,這裡類圖起了一個很大的規範作用。它在設計階段,規範好整個框架,讓我先對業務流程有了大致的輪廓。如果感覺有錯誤,可以在類圖階段就修改,而不是等到實現時修改程式碼,減小犯錯成本。

其實程式碼實現後,我還返回去修改了類圖

一個是命名規範,當時設計類圖命名比較草率,導致在程式碼累積下來後不能見名知意,所以重構了程式碼並且修改類圖中類和方法的命名;
第二個是方法的引數和返回值,類圖設計時我對每個方法都寫了形參和返回值,但具體實現時大概率會發生變化,比如刪除日程那隻需要傳遞日程ID,而不是把Schedule傳過去;

類圖上的時間安排

其出現兩個問題,其一是在根據類圖實現程式碼時發現有些細節沒有考慮到,需要在實現時再花時間來設計。其二是根據類圖的設計無從下手,有我Java功底尚淺的原因,但也可能是因為設計的不合理。
類圖設計的快,可能會有細節被忽略;類圖設計的慢,不斷打磨精細,如果後續要修改,可能因為投入了較多的時間成本不想修改。
這次從建模到實現,收穫很大,真的感受到一個好的建模可以讓整個功能實現更加高效。我的建模可能還很不規範,之後還是多實踐和總結!

相關文章