Java MVC模式实践:构建清晰、可维护的应用程序

java mvc模式实践:构建清晰、可维护的应用程序

本文深入探讨Java中MVC模式的正确实践,通过分析一个餐厅管理系统案例,揭示视图层(View)和控制器层(Controller)常见的职责混淆问题。我们将详细阐述模型、视图、控制器的核心职责,并提供具体的代码重构示例,旨在帮助开发者实现更严格的职责分离,提升代码的可维护性、可测试性及UI灵活性,并探讨异常处理的最佳实践。

1. MVC模式概述

Model-View-Controller(MVC)是一种软件架构模式,旨在将应用程序的业务逻辑、数据和用户界面分离开来。这种分离有助于提高代码的模块化、可维护性和可扩展性。

Model(模型):负责管理应用程序的数据和业务逻辑。它独立于用户界面,处理数据的存储、检索、处理和验证。模型通常包含数据结构(如DailyMenu、MenuItem)和与数据操作相关的服务(如DailyMenuServices)。View(视图):负责显示模型中的数据,并接收用户的输入。视图是用户界面的表示,它不包含任何业务逻辑,只负责数据的渲染和用户交互的收集。在命令行应用中,视图负责打印提示信息和读取用户输入。Controller(控制器):作为模型和视图之间的协调者。它接收并解析用户的输入(来自视图),调用相应的业务逻辑(通过模型或服务),然后更新模型,并指示视图显示更新后的结果。控制器是应用程序的“大脑”,决定了如何响应用户操作。

2. 初始实现中的MVC模式误区分析

在项目初期,开发者常会不自觉地将不同层次的职责混淆,导致代码耦合度高,难以维护。以下是一个餐厅管理系统初始实现中常见的几个问题:

2.1 视图层(MenuView)包含业务逻辑

在原始的MenuView实现中,存在如下问题:

立即学习“Java免费学习笔记(深入)”;

// 原始MenuView片段public DailyMenu getMenuTypes(Menu menu){;    menu(); // 打印菜单选项    int option = Integer.parseInt(scanner.nextLine()); // 获取用户输入    MenuTypes menuTypes = MenuTypes.get(option-1);    switch (menuTypes){ // 包含业务决策逻辑        case FOODMENU -> {return getFoodMenuTypes(menu.getFoodMenu());}        case DRINKMENU -> {return getDrinkMenuTypes(menu.getDrinkMenu());}        default -> {return null;}    }}// 类似地,getFoodMenuTypes 和 getDrinkMenuTypes 也包含 switch 逻辑

问题分析:视图的职责是显示信息和收集用户输入,但上述代码中的getMenuTypes方法不仅显示了菜单选项并获取输入,还包含了根据用户选择进行业务决策switch语句)和数据导航(menu.getFoodMenu())。这种逻辑属于控制器或服务层,将其放在视图中会造成:

职责不清晰:视图不再是纯粹的UI层。维护困难:如果UI需要从命令行改为图形界面(Swing/JavaFX),这些业务逻辑也需要重写,而它们本应独立于UI。测试复杂:难以对视图中的业务逻辑进行单元测试,因为它们与UI输入输出紧密耦合。

2.2 服务层(DailyMenuServicesImpl)与视图层耦合

虽然提供的DailyMenuServicesImpl代码片段中没有直接的打印输出,但在实际开发中,服务层有时会错误地包含UI相关的输出逻辑,例如:

// 假设DailyMenuServicesImpl中存在类似代码(反例)public void updateMenu(DailyMenu dailyMenu,MenuItem updateMenuItem,String itemName) {    // menuPrinter.printMenu(dailyMenu); // 错误:服务层不应进行UI打印    dailyMenu.getMenuItemList().forEach(/* ... */);}

问题分析:服务层(Model的一部分)应专注于业务逻辑的实现,而不应关心数据如何展示给用户。menuPrinter.printMenu()是一个典型的视图操作。将UI打印逻辑嵌入服务层,会破坏模型层的独立性,使其难以在不同的UI环境或无UI场景下复用。

2.3 主方法(Main)中的直接协调

在初始的Main方法中,它直接实例化MenuView和DailyMenuServicesImpl,并根据用户输入直接调用它们的方法来执行操作:

// 原始Main方法片段public static void menuMain(Menu menu) throws IOException{    // ...    DailyMenuServices dailyMenuServices = new DailyMenuServicesImpl();    MenuView menuView = new MenuView();    // ...    switch (actions) {        case CREATE -> {            MenuItem menuItem = menuView.createMenuItem();            DailyMenu dailyMenu = menuView.getMenuTypes(menu); // 视图中包含逻辑            dailyMenuServices.addMenuItemsToMenu(dailyMenu,menuItem);        }        // ...    }}

问题分析:Main方法在此充当了一个隐式的控制器,但这种做法缺乏结构性。它直接处理用户输入、调用视图获取数据、再调用服务执行业务,导致Main方法变得臃肿且职责不清。一个成熟的MVC应用应该有一个明确的控制器类来承担这些协调职责。

3. 重构实践:构建职责分明的MVC组件

为了解决上述问题,我们需要对代码进行重构,严格遵循MVC的职责分离原则。

3.1 控制器(Controller)的核心作用

控制器是MVC模式的“胶水”,负责接收用户输入,将其转化为对模型(服务)的操作,并最终选择合适的视图来呈现结果。

重构后的MenuControllers示例

无阶未来模型擂台/AI 应用平台 无阶未来模型擂台/AI 应用平台

无阶未来模型擂台/AI 应用平台,一站式模型+应用平台

无阶未来模型擂台/AI 应用平台 35 查看详情 无阶未来模型擂台/AI 应用平台

public class MenuControllers {    private final MenuView view;    private final DailyMenuServices  dailyMenuServices;    private final MenuFileHandlingServices menuFileHandlingServices ;    // 依赖注入:通过构造函数获取视图和服务实例    public MenuControllers(){        this.view  = MenuView.getInstance(); // 使用单例获取视图实例        this.dailyMenuServices = DailyMenuServicesImpl.getInstance(); // 使用单例获取服务实例        this.menuFileHandlingServices = MenuFileHandlingServicesImpl.getInstance();    }    public void add(Menu menu){        MenuItem  menuItem = view.createMenuItem(); // 视图只负责获取原始数据        int option = view.getMenuTypes(); // 视图只返回用户选择的整数        MenuTypes menuTypes = MenuTypes.get(option-1); // 控制器解析用户选择        switch (menuTypes){ // 控制器包含业务决策逻辑            case FOODMENU -> addToFoodMenu(menu.getFoodMenu(),menuItem);            case DRINKMENU -> addToDrinkMenu(menu.getDrinkMenu(),menuItem);            default -> System.out.println("Invalid menu type selected."); // 错误提示也可以通过View        }    }    // 辅助方法,将具体操作进一步细化    public void addToFoodMenu(FoodMenu foodMenu, MenuItem menuItem){        int option = view.getFoodMenuTypes();        FoodMenuTypes foodMenuTypes = FoodMenuTypes.get(option-1);        switch (foodMenuTypes){            case BREAKFASTMENU -> dailyMenuServices.addMenuItemsToMenu(foodMenu.getBreakfastMenu(),menuItem);            case LUNCHMENU -> dailyMenuServices.addMenuItemsToMenu(foodMenu.getLunchMenu(),menuItem);            case DINNERMENU -> dailyMenuServices.addMenuItemsToMenu(foodMenu.getDinnerMenu(),menuItem);            default -> System.out.println("Invalid food menu type selected.");        }    }    // ... 其他 update, delete, showMenu 等方法类似    public void showMenu(Menu menu){        view.printMenu(menu); // 控制器指示视图显示菜单    }    // ... 文件操作也由控制器协调}

关键点

MenuControllers通过构造函数接收MenuView和DailyMenuServices的实例,实现了依赖注入,降低了耦合。控制器从MenuView获取原始的用户输入(如整数选项、字符串),然后由控制器来解析这些输入,并根据解析结果执行相应的业务逻辑。所有的switch决策逻辑都从视图移到了控制器中,使得控制器成为业务流程的协调者。控制器在完成业务操作后,会指示视图进行相应的显示(例如view.printMenu(menu))。

3.2 视图(View)的纯粹化

重构后的视图层应该尽可能地“哑巴”,只负责显示信息和收集原始的用户输入,不包含任何业务决策逻辑。

重构后的MenuView示例

public class MenuView {    private Scanner scanner = new Scanner(System.in);    private final MenuPrinter menuPrinter = MenuPrinterImpl.getInstance(); // 视图依赖打印器    // 单例模式    private MenuView(){ }    public static MenuView getInstance(){        return MenuViewHelper.menuView;    }    private static class MenuViewHelper{        private static final MenuView menuView = new MenuView();    }    public int getMenuTypes(){        menu(); // 仅打印菜单选项        return Integer.parseInt(scanner.nextLine()); // 仅返回用户选择的整数    }    public int getFoodMenuTypes(){        foodMenu();        return Integer.parseInt(scanner.nextLine());    }    public int getDrinkMenuTypes(){        drinkMenu();        return Integer.parseInt(scanner.nextLine());    }    // createMenuItem 和 getMenuItemName 负责收集用户输入并返回数据对象或字符串    public MenuItem createMenuItem(){ /* ... */ return menuItem; }    public String getMenuItemName(){ /* ... */ return scanner.nextLine(); }    public void printMenu(Menu menu){ // 视图通过打印器来显示菜单        menuPrinter.printMenu(menu);    }    // 静态方法用于打印具体的菜单提示信息    public static void menu(){ /* ... */ }    public static void drinkMenu(){ /* ... */ }    public static void foodMenu(){ /* ... */ }}

关键点

MenuView中的getMenuTypes、getFoodMenuTypes等方法现在只负责打印提示信息并返回用户输入的原始整数值,不再包含switch语句或任何业务决策。视图通过MenuPrinter接口来执行实际的打印操作,进一步解耦了UI的渲染逻辑。视图的职责被严格限制在“输入”和“输出”上。

3.3 模型与服务层(Model/Service)的独立性

模型层(包括数据结构和服务)应该完全独立于UI,专注于数据管理和业务逻辑的实现。

重构后的DailyMenuServicesImpl示例

public class DailyMenuServicesImpl implements DailyMenuServices {    // 单例模式    private DailyMenuServicesImpl(){}    public static DailyMenuServicesImpl getInstance(){        return DailyMenuServicesImplHelper.dailyMenuServicesImpl;    }    private static class DailyMenuServicesImplHelper{        private static final DailyMenuServicesImpl dailyMenuServicesImpl = new DailyMenuServicesImpl();    }    @Override    public void addMenuItemsToMenu(DailyMenu dailyMenu,MenuItem menuItem) {        List menuItemList = dailyMenu.getMenuItemList();        menuItemList.add(menuItem);    }    @Override    public void updateMenu(DailyMenu dailyMenu,MenuItem updateMenuItem,String itemName) {        // 服务层只执行业务逻辑,不进行任何UI打印        dailyMenu.getMenuItemList().stream()                                    .filter(menuItem -> menuItem.getNames().equals(itemName))                                    .findFirst()                                    .ifPresentOrElse(menuItem -> {                                        menuItem.setNames(updateMenuItem.getNames());                                        menuItem.setPrice(updateMenuItem.getPrice());                                        menuItem.setDescription(updateMenuItem.getDescription());                                        menuItem.setImage(updateMenuItem.getImage());                                    },()->{                                        throw new NullPointerException("Wrong menu Item name !!!"); // 抛出业务异常                                    });    }    @Override    public void deleteMenu(DailyMenu dailyMenu,String itemName) {        dailyMenu.getMenuItemList().removeIf(menuItem ->                menuItem.getNames().equals(itemName));    }}

关键点

DailyMenuServicesImpl完全专注于菜单项的增删改查业务逻辑。它不包含任何System.out.println或Scanner相关的代码。当业务操作失败时,它会抛出业务相关的异常(如NullPointerException,尽管更推荐自定义业务异常),而不是直接打印错误信息。

3.4 Main方法作为启动入口

重构后的Main方法将应用程序的控制权交给控制器,自身只负责初始化和启动。

public class Main {    private static Scanner scanner = new Scanner(System.in);    public static void main(String[] args) throws IOException {        int option;        Menu menu = new Menu();        Bill bill = new Bill();        while (true){            System.out.println("n1.Menu management");            System.out.println("2.Bill management");            System.out.print("Please choose which types Management you want to work with:");            option = Integer.parseInt(scanner.nextLine());            ManagementTypes types = ManagementTypes.get(option-1);            switch (types){                case MENU -> menuMain(menu); // 将控制权交给菜单管理的主控制器                case BILL -> billMain(bill,menu); // 将控制权交给账单管理的主控制器                default -> {}            }        }    }    public static void menuMain(Menu menu) throws IOException{        int option = 0;        MenuControllers menuControllers =  new MenuControllers(); // 实例化主控制器        // MenuPrinter menuPrinter = MenuPrinterImpl.getInstance(); // 打印器现在由MenuView持有        try {            while (option != 7) {                menu(); // 

以上就是Java MVC模式实践:构建清晰、可维护的应用程序的详细内容,更多请关注创想鸟其它相关文章!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 chuangxiangniao@163.com 举报,一经查实,本站将立刻删除。
发布者:程序猿,转转请注明出处:https://www.chuangxiangniao.com/p/572327.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2025年11月10日 06:34:57
下一篇 2025年11月10日 06:38:34

相关推荐

  • Uniapp 中如何不拉伸不裁剪地展示图片?

    灵活展示图片:如何不拉伸不裁剪 在界面设计中,常常需要以原尺寸展示用户上传的图片。本文将介绍一种在 uniapp 框架中实现该功能的简单方法。 对于不同尺寸的图片,可以采用以下处理方式: 极端宽高比:撑满屏幕宽度或高度,再等比缩放居中。非极端宽高比:居中显示,若能撑满则撑满。 然而,如果需要不拉伸不…

    2025年12月24日
    400
  • 如何让小说网站控制台显示乱码,同时网页内容正常显示?

    如何在不影响用户界面的情况下实现控制台乱码? 当在小说网站上下载小说时,大家可能会遇到一个问题:网站上的文本在网页内正常显示,但是在控制台中却是乱码。如何实现此类操作,从而在不影响用户界面(UI)的情况下保持控制台乱码呢? 答案在于使用自定义字体。网站可以通过在服务器端配置自定义字体,并通过在客户端…

    2025年12月24日
    800
  • 如何在地图上轻松创建气泡信息框?

    地图上气泡信息框的巧妙生成 地图上气泡信息框是一种常用的交互功能,它简便易用,能够为用户提供额外信息。本文将探讨如何借助地图库的功能轻松创建这一功能。 利用地图库的原生功能 大多数地图库,如高德地图,都提供了现成的信息窗体和右键菜单功能。这些功能可以通过以下途径实现: 高德地图 JS API 参考文…

    2025年12月24日
    400
  • 如何使用 scroll-behavior 属性实现元素scrollLeft变化时的平滑动画?

    如何实现元素scrollleft变化时的平滑动画效果? 在许多网页应用中,滚动容器的水平滚动条(scrollleft)需要频繁使用。为了让滚动动作更加自然,你希望给scrollleft的变化添加动画效果。 解决方案:scroll-behavior 属性 要实现scrollleft变化时的平滑动画效果…

    2025年12月24日
    000
  • 如何为滚动元素添加平滑过渡,使滚动条滑动时更自然流畅?

    给滚动元素平滑过渡 如何在滚动条属性(scrollleft)发生改变时为元素添加平滑的过渡效果? 解决方案:scroll-behavior 属性 为滚动容器设置 scroll-behavior 属性可以实现平滑滚动。 html 代码: click the button to slide right!…

    2025年12月24日
    500
  • 如何选择元素个数不固定的指定类名子元素?

    灵活选择元素个数不固定的指定类名子元素 在网页布局中,有时需要选择特定类名的子元素,但这些元素的数量并不固定。例如,下面这段 html 代码中,activebar 和 item 元素的数量均不固定: *n *n 如果需要选择第一个 item元素,可以使用 css 选择器 :nth-child()。该…

    2025年12月24日
    200
  • 使用 SVG 如何实现自定义宽度、间距和半径的虚线边框?

    使用 svg 实现自定义虚线边框 如何实现一个具有自定义宽度、间距和半径的虚线边框是一个常见的前端开发问题。传统的解决方案通常涉及使用 border-image 引入切片图片,但是这种方法存在引入外部资源、性能低下的缺点。 为了避免上述问题,可以使用 svg(可缩放矢量图形)来创建纯代码实现。一种方…

    2025年12月24日
    100
  • 如何让“元素跟随文本高度,而不是撑高父容器?

    如何让 元素跟随文本高度,而不是撑高父容器 在页面布局中,经常遇到父容器高度被子元素撑开的问题。在图例所示的案例中,父容器被较高的图片撑开,而文本的高度没有被考虑。本问答将提供纯css解决方案,让图片跟随文本高度,确保父容器的高度不会被图片影响。 解决方法 为了解决这个问题,需要将图片从文档流中脱离…

    2025年12月24日
    000
  • 为什么 CSS mask 属性未请求指定图片?

    解决 css mask 属性未请求图片的问题 在使用 css mask 属性时,指定了图片地址,但网络面板显示未请求获取该图片,这可能是由于浏览器兼容性问题造成的。 问题 如下代码所示: 立即学习“前端免费学习笔记(深入)”; icon [data-icon=”cloud”] { –icon-cl…

    2025年12月24日
    200
  • 如何利用 CSS 选中激活标签并影响相邻元素的样式?

    如何利用 css 选中激活标签并影响相邻元素? 为了实现激活标签影响相邻元素的样式需求,可以通过 :has 选择器来实现。以下是如何具体操作: 对于激活标签相邻后的元素,可以在 css 中使用以下代码进行设置: li:has(+li.active) { border-radius: 0 0 10px…

    2025年12月24日
    100
  • 如何模拟Windows 10 设置界面中的鼠标悬浮放大效果?

    win10设置界面的鼠标移动显示周边的样式(探照灯效果)的实现方式 在windows设置界面的鼠标悬浮效果中,光标周围会显示一个放大区域。在前端开发中,可以通过多种方式实现类似的效果。 使用css 使用css的transform和box-shadow属性。通过将transform: scale(1.…

    2025年12月24日
    200
  • 为什么我的 Safari 自定义样式表在百度页面上失效了?

    为什么在 Safari 中自定义样式表未能正常工作? 在 Safari 的偏好设置中设置自定义样式表后,您对其进行测试却发现效果不同。在您自己的网页中,样式有效,而在百度页面中却失效。 造成这种情况的原因是,第一个访问的项目使用了文件协议,可以访问本地目录中的图片文件。而第二个访问的百度使用了 ht…

    2025年12月24日
    000
  • 如何用前端实现 Windows 10 设置界面的鼠标移动探照灯效果?

    如何在前端实现 Windows 10 设置界面中的鼠标移动探照灯效果 想要在前端开发中实现 Windows 10 设置界面中类似的鼠标移动探照灯效果,可以通过以下途径: CSS 解决方案 DEMO 1: Windows 10 网格悬停效果:https://codepen.io/tr4553r7/pe…

    2025年12月24日
    000
  • 使用CSS mask属性指定图片URL时,为什么浏览器无法加载图片?

    css mask属性未能加载图片的解决方法 使用css mask属性指定图片url时,如示例中所示: mask: url(“https://api.iconify.design/mdi:apple-icloud.svg”) center / contain no-repeat; 但是,在网络面板中却…

    2025年12月24日
    000
  • 如何用CSS Paint API为网页元素添加时尚的斑马线边框?

    为元素添加时尚的斑马线边框 在网页设计中,有时我们需要添加时尚的边框来提升元素的视觉效果。其中,斑马线边框是一种既醒目又别致的设计元素。 实现斜向斑马线边框 要实现斜向斑马线间隔圆环,我们可以使用css paint api。该api提供了强大的功能,可以让我们在元素上绘制复杂的图形。 立即学习“前端…

    2025年12月24日
    000
  • 图片如何不撑高父容器?

    如何让图片不撑高父容器? 当父容器包含不同高度的子元素时,父容器的高度通常会被最高元素撑开。如果你希望父容器的高度由文本内容撑开,避免图片对其产生影响,可以通过以下 css 解决方法: 绝对定位元素: .child-image { position: absolute; top: 0; left: …

    2025年12月24日
    000
  • CSS 帮助

    我正在尝试将文本附加到棕色框的左侧。我不能。我不知道代码有什么问题。请帮助我。 css .hero { position: relative; bottom: 80px; display: flex; justify-content: left; align-items: start; color:…

    2025年12月24日 好文分享
    200
  • 前端代码辅助工具:如何选择最可靠的AI工具?

    前端代码辅助工具:可靠性探讨 对于前端工程师来说,在HTML、CSS和JavaScript开发中借助AI工具是司空见惯的事情。然而,并非所有工具都能提供同等的可靠性。 个性化需求 关于哪个AI工具最可靠,这个问题没有一刀切的答案。每个人的使用习惯和项目需求各不相同。以下是一些影响选择的重要因素: 立…

    2025年12月24日
    300
  • 如何用 CSS Paint API 实现倾斜的斑马线间隔圆环?

    实现斑马线边框样式:探究 css paint api 本文将探究如何使用 css paint api 实现倾斜的斑马线间隔圆环。 问题: 给定一个有多个圆圈组成的斑马线图案,如何使用 css 实现倾斜的斑马线间隔圆环? 答案: 立即学习“前端免费学习笔记(深入)”; 使用 css paint api…

    2025年12月24日
    000
  • 如何使用CSS Paint API实现倾斜斑马线间隔圆环边框?

    css实现斑马线边框样式 想定制一个带有倾斜斑马线间隔圆环的边框?现在使用css paint api,定制任何样式都轻而易举。 css paint api 这是一个新的css特性,允许开发人员创建自定义形状和图案,其中包括斑马线样式。 立即学习“前端免费学习笔记(深入)”; 实现倾斜斑马线间隔圆环 …

    2025年12月24日
    100

发表回复

登录后才能评论
关注微信