草庐IT

日程功能模块【从建模到代码实现】UML + JavaFX

wake up(); 2023-03-28 原文

结合 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功底尚浅的原因,但也可能是因为设计的不合理。
类图设计的快,可能会有细节被忽略;类图设计的慢,不断打磨精细,如果后续要修改,可能因为投入了较多的时间成本不想修改。
这次从建模到实现,收获很大,真的感受到一个好的建模可以让整个功能实现更加高效。我的建模可能还很不规范,之后还是多实践和总结!

有关日程功能模块【从建模到代码实现】UML + JavaFX的更多相关文章

  1. ruby - 在 Ruby 中使用匿名模块 - 2

    假设我做了一个模块如下:m=Module.newdoclassCendend三个问题:除了对m的引用之外,还有什么方法可以访问C和m中的其他内容?我可以在创建匿名模块后为其命名吗(就像我输入“module...”一样)?如何在使用完匿名模块后将其删除,使其定义的常量不再存在? 最佳答案 三个答案:是的,使用ObjectSpace.此代码使c引用你的类(class)C不引用m:c=nilObjectSpace.each_object{|obj|c=objif(Class===objandobj.name=~/::C$/)}当然这取决于

  2. ruby-on-rails - Ruby net/ldap 模块中的内存泄漏 - 2

    作为我的Rails应用程序的一部分,我编写了一个小导入程序,它从我们的LDAP系统中吸取数据并将其塞入一个用户表中。不幸的是,与LDAP相关的代码在遍历我们的32K用户时泄漏了大量内存,我一直无法弄清楚如何解决这个问题。这个问题似乎在某种程度上与LDAP库有关,因为当我删除对LDAP内容的调用时,内存使用情况会很好地稳定下来。此外,不断增加的对象是Net::BER::BerIdentifiedString和Net::BER::BerIdentifiedArray,它们都是LDAP库的一部分。当我运行导入时,内存使用量最终达到超过1GB的峰值。如果问题存在,我需要找到一些方法来更正我的代

  3. ruby - 如何在 buildr 项目中使用 Ruby 代码? - 2

    如何在buildr项目中使用Ruby?我在很多不同的项目中使用过Ruby、JRuby、Java和Clojure。我目前正在使用我的标准Ruby开发一个模拟应用程序,我想尝试使用Clojure后端(我确实喜欢功能代码)以及JRubygui和测试套件。我还可以看到在未来的不同项目中使用Scala作为后端。我想我要为我的项目尝试一下buildr(http://buildr.apache.org/),但我注意到buildr似乎没有设置为在项目中使用JRuby代码本身!这看起来有点傻,因为该工具旨在统一通用的JVM语言并且是在ruby中构建的。除了将输出的jar包含在一个独特的、仅限ruby​​

  4. ruby-on-rails - Rails 源代码 : initialize hash in a weird way? - 2

    在rails源中:https://github.com/rails/rails/blob/master/activesupport/lib/active_support/lazy_load_hooks.rb可以看到以下内容@load_hooks=Hash.new{|h,k|h[k]=[]}在IRB中,它只是初始化一个空哈希。和做有什么区别@load_hooks=Hash.new 最佳答案 查看rubydocumentationforHashnew→new_hashclicktotogglesourcenew(obj)→new_has

  5. ruby-on-rails - 建模收藏夹 - 2

    我希望将Favorite模型添加到我的User和Link模型。业务逻辑用户可以有多个链接(即可以添加多个链接)用户可以收藏多个链接(他们自己的或其他用户的)一个链接可以被多个用户收藏,但只有一个所有者我对如何为这种关联建模以及在模型就位后如何创建用户收藏夹感到困惑?classUser 最佳答案 下面的数据模型怎么样:classUser:destroyhas_many:favorite_links,:through=>:favorites,:source=>:linkendclassLink:destroyhas_many:favor

  6. ruby-on-rails - 在混合/模块中覆盖模型的属性访问器 - 2

    我有一个包含模块的模型。我想在模块中覆盖模型的访问器方法。例如:classBlah这显然行不通。有什么想法可以实现吗? 最佳答案 您的代码看起来是正确的。我们正在毫无困难地使用这个确切的模式。如果我没记错的话,Rails使用#method_missing作为属性setter,因此您的模块将优先,阻止ActiveRecord的setter。如果您正在使用ActiveSupport::Concern(参见thisblogpost),那么您的实例方法需要进入一个特殊的模块:classBlah

  7. ruby - 如何根据特征实现 FactoryGirl 的条件行为 - 2

    我有一个用户工厂。我希望默认情况下确认用户。但是鉴于unconfirmed特征,我不希望它们被确认。虽然我有一个基于实现细节而不是抽象的工作实现,但我想知道如何正确地做到这一点。factory:userdoafter(:create)do|user,evaluator|#unwantedimplementationdetailshereunlessFactoryGirl.factories[:user].defined_traits.map(&:name).include?(:unconfirmed)user.confirm!endendtrait:unconfirmeddoenden

  8. ruby - 当使用::指定模块时,为什么 Ruby 不在更高范围内查找类? - 2

    我刚刚被困在这个问题上一段时间了。以这个基地为例:moduleTopclassTestendmoduleFooendend稍后,我可以通过这样做在Foo中定义扩展Test的类:moduleTopmoduleFooclassSomeTest但是,如果我尝试通过使用::指定模块来最小化缩进:moduleTop::FooclassFailure这失败了:NameError:uninitializedconstantTop::Foo::Test这是一个错误,还是仅仅是Ruby解析变量名的方式的逻辑结果? 最佳答案 Isthisabug,or

  9. ruby-on-rails - 浏览 Ruby 源代码 - 2

    我的主要目标是能够完全理解我正在使用的库/gem。我尝试在Github上从头到尾阅读源代码,但这真的很难。我认为更有趣、更温和的踏脚石就是在使用时阅读每个库/gem方法的源代码。例如,我想知道RubyonRails中的redirect_to方法是如何工作的:如何查找redirect_to方法的源代码?我知道在pry中我可以执行类似show-methodmethod的操作,但我如何才能对Rails框架中的方法执行此操作?您对我如何更好地理解Gem及其API有什么建议吗?仅仅阅读源代码似乎真的很难,尤其是对于框架。谢谢! 最佳答案 Ru

  10. ruby - 获取模块中定义的所有常量的值 - 2

    我想获取模块中定义的所有常量的值:moduleLettersA='apple'.freezeB='boy'.freezeendconstants给了我常量的名字:Letters.constants(false)#=>[:A,:B]如何获取它们的值的数组,即["apple","boy"]? 最佳答案 为了做到这一点,请使用mapLetters.constants(false).map&Letters.method(:const_get)这将返回["a","b"]第二种方式:Letters.constants(false).map{|c

随机推荐