
本文深入探讨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 应用平台,一站式模型+应用平台
35 查看详情
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
微信扫一扫
支付宝扫一扫