0

0

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

碧海醫心

碧海醫心

发布时间:2025-11-08 23:38:01

|

907人浏览过

|

来源于php中文网

原创

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())。这种逻辑属于控制器或服务层,将其放在视图中会造成:

  1. 职责不清晰:视图不再是纯粹的UI层。
  2. 维护困难:如果UI需要从命令行改为图形界面(Swing/JavaFX),这些业务逻辑也需要重写,而它们本应独立于UI。
  3. 测试复杂:难以对视图中的业务逻辑进行单元测试,因为它们与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模式的“胶水”,负责接收用户输入,将其转化为对模型(服务)的操作,并最终选择合适的视图来呈现结果。

ISite企业建站系统1.2.3
ISite企业建站系统1.2.3

ISite企业建站系统是为懂点网站建设和HTML技术的人员(例如企业建站人员)而开发的一套专门用于企业建站的开源免费程序。本系统采用了全新的栏目维护模式,内容添加过程中,前后台菜单是一样的,需要维护前台某个栏目的内容,只需要进后台相应栏目即可,一般的企业人员只需要查看简易的说明就可以上手维护网站内容。通过自由度极高的模板系统,可以适应大多数情况的界面需求,后台带有标签生成器,建站只需要构架好HTM

下载

重构后的MenuControllers示例

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<MenuItem> 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(); // 

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
switch语句用法
switch语句用法

switch语句用法:1、Switch语句只能用于整数类型,枚举类型和String类型,不能用于浮点数类型和布尔类型;2、每个case语句后面必须跟着一个break语句,以防止执行其他case的代码块,没有break语句,将会继续执行下一个case的代码块;3、可以在一个case语句中匹配多个值,使用逗号分隔;4、Switch语句中的default代码块是可选的等等。

569

2023.09.21

Java switch的用法
Java switch的用法

Java中的switch语句用于根据不同的条件执行不同的代码块。想了解更多switch的相关内容,可以阅读本专题下面的文章。

441

2024.03.13

js 字符串转数组
js 字符串转数组

js字符串转数组的方法:1、使用“split()”方法;2、使用“Array.from()”方法;3、使用for循环遍历;4、使用“Array.split()”方法。本专题为大家提供js字符串转数组的相关的文章、下载、课程内容,供大家免费下载体验。

760

2023.08.03

js截取字符串的方法
js截取字符串的方法

js截取字符串的方法有substring()方法、substr()方法、slice()方法、split()方法和slice()方法。本专题为大家提供字符串相关的文章、下载、课程内容,供大家免费下载体验。

221

2023.09.04

java基础知识汇总
java基础知识汇总

java基础知识有Java的历史和特点、Java的开发环境、Java的基本数据类型、变量和常量、运算符和表达式、控制语句、数组和字符串等等知识点。想要知道更多关于java基础知识的朋友,请阅读本专题下面的的有关文章,欢迎大家来php中文网学习。

1566

2023.10.24

字符串介绍
字符串介绍

字符串是一种数据类型,它可以是任何文本,包括字母、数字、符号等。字符串可以由不同的字符组成,例如空格、标点符号、数字等。在编程中,字符串通常用引号括起来,如单引号、双引号或反引号。想了解更多字符串的相关内容,可以阅读本专题下面的文章。

649

2023.11.24

java读取文件转成字符串的方法
java读取文件转成字符串的方法

Java8引入了新的文件I/O API,使用java.nio.file.Files类读取文件内容更加方便。对于较旧版本的Java,可以使用java.io.FileReader和java.io.BufferedReader来读取文件。在这些方法中,你需要将文件路径替换为你的实际文件路径,并且可能需要处理可能的IOException异常。想了解更多java的相关内容,可以阅读本专题下面的文章。

1228

2024.03.22

php中定义字符串的方式
php中定义字符串的方式

php中定义字符串的方式:单引号;双引号;heredoc语法等等。想了解更多字符串的相关内容,可以阅读本专题下面的文章。

1184

2024.04.29

C# ASP.NET Core微服务架构与API网关实践
C# ASP.NET Core微服务架构与API网关实践

本专题围绕 C# 在现代后端架构中的微服务实践展开,系统讲解基于 ASP.NET Core 构建可扩展服务体系的核心方法。内容涵盖服务拆分策略、RESTful API 设计、服务间通信、API 网关统一入口管理以及服务治理机制。通过真实项目案例,帮助开发者掌握构建高可用微服务系统的关键技术,提高系统的可扩展性与维护效率。

3

2026.03.11

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Kotlin 教程
Kotlin 教程

共23课时 | 4.3万人学习

C# 教程
C# 教程

共94课时 | 11.1万人学习

Java 教程
Java 教程

共578课时 | 80.8万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号