[译文]使用服务代理来避免MVC控制器的膨胀

本文提出了一种可重用的设计方案,通过引入服务代理与MVC框架结合使用,避免控制器承担过多的服务逻辑,解决了控制器膨胀的问题。该方案提高了代码的可读性、可测试性和可重用性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

原文:Using a Service Delegate to Avoid MVC Controller Bloat

作者:Eric Spiegelberg

出处:http://today.java.net/article/2009/11/04/using-service-delegate-avoid-mvc-controller-bloat

 

软件产业中的关键概念之一是关注点分离:可靠地建立各种类型的复杂系统的唯一方法就是把系统分解成小的、简单的以及目标明确的多个组件,每个组件执行某个特定的功能,只执行某个特定功能使得每个组件更容易被理解、开发、单元测试、重用和维护。你可以看到在标准的三层Java web应用设计中严格遵守关注点分离的做法已经开始主控了Java软件的开发。一种常见的做法是,在表现/web层(例如Spring Web MVC或者Structs2)中使用web的模型-视图-控制器(model-view-controllerMVC)框架,在中间层(例如Spring)使用中间件框架,以及在数据层(例如Hibernate或者任何的JPA实现)使用对象关系映射(ORM)。

框架和工具的全部要点就是把你从复杂的技术挑战和样板代码中解放出来,否则你将被迫去编写自己的样本代码,这样就可以允许你更加集中精力以及更有效地开发自己的应用。不过,因为框架和工具处理的是困难的部分(而且还会重用在很多种情况中),因此他们往往比你正在使用他们来创建的应用有更多的技术复杂性,其结果就是框架和工具通常更强调架构和设计,而在这些框架和工具之上工作的开发者往往更重视的则是,把设计的最佳实践纳入到他们的软件中。因此再一次,这种做法把应用开发者解放了出来,使得他们可以享受到这样的好处,即必须“只是”设计和建立最大化地利用了所有的框架和工具的应用,并且确保这些框架和工具不仅彼此之间,而且能够与应用的自定义业务逻辑很好地集成及交互。

而这里就存在着一个设计缺口:由于框架和工具的创造者必须要把大量的注意力投放在设计方面,结果应用开发者就可以获得更大的宽松度。设计缺口的一个典型例子就是我所说的控制器膨胀,在web应用这种情况中,当应用开发者违反了关注点分离的做法,把非web服务代码直接放入他们的MVC框架控制器中时,控制器膨胀的情形就会发生。

当对象接收到一个请求时,该对象可以选择自己来执行该请求还是把该请求委托给将会完成这一工作的第二个对象。业界对该第二个对象的命名各不相同,不过我把它称作服务代理,这一术语强调了该第二个对象的角色是作为一个代理,而且还凸显了这样的一个事实,那就是该对象包含了服务级别的业务逻辑。

本文讨论了使用服务代理结合MVC框架的设计来避免控制器膨胀,对MVC模式的背景做了假设,虽然例子使用JavaSpring Web MVC来把设计应用到web应用中,不过这些概念可以应用到任何语言中以及应用到任何的MVC框架上。术语服务(service)、服务代码(service code)、服务层(service layer)和服务代理(service delegate)可被替换使用以便引用你自己的自定义业务逻辑。

 

问题:MVC控制器膨胀

 

使用Spring Web MVC作为例子,让我们来看一下一个典型的web MVC控制器的示例。

 

{

    private UserService userService;

   

    protected ModelAndView handleRequestInternal(HttpServletRequest request,

                                                 HttpServletResponse response) throws Exception

    {

        Map model = new HashMap();

       

        String userId = request.getParameter("id");

       

        User user = userService.findById(userId);      

       

        user.setLastAccessTime(new Date());

        userService.persist(user);

       

        if (user.isAccountExpirationWithin90Days())

        {

            userService.sendAccountExpirationWarningEmail();

        }

       

        // 分派视图

        String viewName = getViewName();

        ModelAndView modelAndView = new ModelAndView(viewName, model);

       

        return modelAndView;

    }

}

 

虽然以上代码例子是可以运作的,但它确实展示了一个主要的设计问题:服务代码(例如你的业务逻辑)没有必要位于控制器本身之内,handleRequestInternal内部的大部分代码与web层无关,因此违反了关注分离。这导致了几个级联在一起的缺点,因为代码存在于直接依赖web层的类中,因此代码不能方便地在非基于web的应用中重用;接下来,正是由于代码在基于web的应用之外难于被重用,因此很难对它进行单元测试。虽然已有太多的选择,比如使用模拟对象(mock object)或者诸如运行应用服务器的热部署代码一类的复杂策略,以及自动化的远程单元测试等,但事实的情况是服务与表现层的编码耦合复杂化了代码的测试和重用。

所有这些问题都源于同一个根本原因:没有理由把把服务代码置于控制器之内,在我看来,一个控制器应该尽可能地“瘦”,只包含与处理传入请求有直接关系的代码,把所有非请求/响应处理委托给服务代理,这些处理会生成在视图中使用的模型对象,然后控制器创建并返回传出去的响应。因为MVC框架负责处理第一和第三个步骤,通过把服务代码(负责第二个步骤)搬移到服务代理中,控制器膨胀问题就可以得到完全的避免。虽然大多数的架构师和开发者都同意这一设计理念,但实际上很少人真正把它纳入到他们的软件中。

 

解决方案:ViewService

 

现在已知道我们希望我们的控制器把处理委托给服务代理,远离或者阻止控制器膨胀的第一步是创建一个接口,该接口充当控制器和代理之间交互的行为合约。

 

public interface ViewService

{  

    public String REQUEST_PARAMETERS_KEY = "request-parameters";

    public String REQUEST_QUERY_STRING_KEY = "request-query-string";

   

    public void generateViewModel(Map model);

}

 

 

虽然该接口一开始很容易让人感觉平淡无奇,但是ViewService的强大和灵活性正是来自它的简易性。因为它并不包含任何的表现层或者服务层的实现细节,因此它能够在无需允许某个层渗入到其他层中的情况下充当一个独立的中介机构。正如你很快就会看到的那样,从控制器的角度看,所有的控制器都知道它需要调用这个使用Map作为参数的方法,而一旦调用完成之后,Map中的结果就可以使用。从服务的角度看,所有的服务类都知道这一方法将以由一些输入对象构成的Map实例为参数被调用,该方法应该使用这些输入对象来生成实现类负责的任何数据,然后经由同一个Map实例来返回结果。通过同意在层之间只传递Map,每一方都完全被封装,不会知道另一方是如何工作的。

这单个的、简单的、只有一行代码的接口带来了许多实实在在的好处,ViewService接口的使用完全实现了在代码和选择使用的MVC框架之间的解耦。你的MVC选择不再需要是一个战略的或者组织范围的决定,因为就现在来说,转换或者甚至是每个应用使用一个独立的MVC框架都是微不足道的一件事情了。现在你创建的每个应用都拥有了自由和灵活性,可以为指定的需求和技术环境使用最好的框架。

更进一步来看的话,可以注意到实现了ViewService的类现在能够重用在任何类型的应用中:web(基于servlet或者portal的)、批处理、胖客户端、EJB、非EJB以及诸如web服务或基于JMS的软件一类的远程应用等现在都是可能的了。这一层面的重用将会允许你遵循不要重复自己(Don’t Repeat YourselfDRY)的原则,而且因为是利用了针对编程接口这一业界最佳实践的缘故,表现层和服务层现在可以由多个开发者并行来创建。服务代码的单元测试显示是不太复杂的,因为可以通过简单的直接与服务层交互的单元测试来进行,这大大简化了测试,使得孤立问题更为容易,并可节省时间。

并不需要尖叫来引起你的注意,不过这一做法的另一个不易察觉的好处是这一设计使得新的团队成员或者缺少经验的开发者很容易就参与到你的项目中,基于种种原因,并不是所有的开发者都能够一头扎入到现有的项目中并能够马上做出重大贡献的。通过把层次和复杂性清晰的分离开来,你能够让特定的开发者在其最有实力的或者是感觉最舒适的个别领域中工作,尽管他们会获取不同系统部分的经验或者知识。

既然ViewService已经准备就绪,下一步就是把它纳入到你的应用中。下面的代码示例展示了Spring Web MVC控制器通过一个支持类的使用向ViewService的一个下层实现委派任务。

 

public class ViewServiceParameterizableViewController extends ParameterizableViewController

{

    private ViewService viewService;

   

    protected ModelAndView handleRequestInternal(HttpServletRequest request,

                                                 HttpServletResponse response) throws Exception

    {

        Map model = HttpViewServiceSupport.processRequest(request, response, viewService);

       

        String viewName = getViewName();

        ModelAndView modelAndView = new ModelAndView(viewName, model);

       

        return modelAndView;

    }

    ...

}

 

 

如同ViewService,控制器的简单容易让人感觉平淡无奇,不过可以再次这样说,控制器的强大和灵活性正是来自的它的简易性。抓住了真正意图和设计理念,该控制器是尽其可能的瘦,它的内容如此之少,正如你所见到的那样,为任何其他的MVC框架创建控制器实在是件微不足道而又快速的工作。

现在已经创建了一个超薄的控制器,让我们来看看包含了核心功能的HttpViewServiceSupport类。

 

public class HttpViewServiceSupport

{  

    public static Map processRequest(HttpServletRequest request,

                                     HttpServletResponse response,

                                     ViewService viewService)

    {

        Map model = new HashMap();

       

        buildModelFromRequest(request, model);

       

        viewService.generateViewModel(model);

 

        return model;

    }

   

    public static void buildModelFromRequest(HttpServletRequest request, Map model)

    {       

        String queryString = request.getQueryString();

        model.put(ViewService.REQUEST_QUERY_STRING_KEY, queryString);

       

        processParameters(request, model);               

        processAttributes(request, model);

        processCookies(request, model);

        processHeaders(request, model);

    }

 

    public static void processParameters(HttpServletRequest request, Map model)

    {       

        Enumeration parameterNames = request.getParameterNames();

 

        if (parameterNames.hasMoreElements())

        {

            Map parameters = new HashMap();

           

            while (parameterNames.hasMoreElements())

            {

                String parameterName = (String) parameterNames.nextElement();

                String values[] = request.getParameterValues(parameterName);

                           

                parameters.put(parameterName, values);

            }

           

            model.put(ViewService.REQUEST_PARAMETERS_KEY, parameters);

        }

    }   

    ...

}

 

 

对于任何传入的请求来说,processRequest()的执行流程由四个步骤组成,首先,一个空的Map被实例化,如同你从早先的讨论中了解到的一样,整个设计的中心概念是在系统的各个层面(在我们的例子中是控制器和ViewService)之间只传递Map,该Map正好只是一个普通的Java对象(POJO)这个事实则意味着没有哪个层面与任何其他的层面有紧密的耦合。接着,传入的请求从当前的表现层特有格式(在本例中是HttpServletRequest)被转换成一个更通用的形式,这个过程在buildModelFromRequest()中完成,该方法从传入请求中提取出感兴趣的或者有用的信息,并把信息放到之前实例化了的Map中。第三步,ViewService被调用,其下层的实现执行处理过程,一旦处理完成,ViewService将把结果存放到Map实例中,从而使得结果可供系统的任何可以访问得到该Map实例的部分使用。最后一步,随着ViewService工作的完成,被填充的Map被简单地返回给调用方法,它最后被返回给MVC框架,然后正如你所期望的,MvC框架的正常处理流程接手了。

在见识了一个超瘦的控制器及一个被用来处理传入请求和调用ViewService下层实现的独立的MVC支持类之后,接下来让我们看一个ViewService的实现例子。

 

public class UserLoginService implements ViewService

{

    private UserService userService;

   

    public static final String USER_KEY = "user";

    public static final String USER_ID_KEY = "user-id";

   

    public void generateViewModel(Map model)

    {

        String userId = (String) model.get(USER_ID_KEY);

       

        User user = userService.findById(userId);      

        model.put(USER_KEY, user);

       

        user.setLastAccessTime(new Date());

        userService.persist(user);

       

        if (user.isAccountExpirationWithin90Days())

        {

            userService.sendAccountExpirationWarningEmail();

        }

       

        Map parameterMap = (Map) model.get(ViewService.REQUEST_PARAMETERS_KEY);       

        String day[] = (String[]) parameterMap.get("day");

       

        if (day != null)

        {

            model.put("day", day[0]);           

        }       

    }

    ...

}

 

 

希望你能从我们最初的并且是有缺陷的例子中找出以上代码的大部分,新的内容是使用String类型常量作为熟知的键名称来在Map内部存放对象。最后,这里提供一个小的JSP页面来把它们放在一起。

 

 

Welcome ${user.firstname}!

Your last login was ${user.lastAccessTime}.

 

<c:if test="${not empty day}">

  Today is ${day}.

</c:if>

 

 

通过使用与ViewServcie实现提供的相同的熟知的键名称,JSTL被用来从JSP上下文中抽出模型对象并动态的填充JSP页面。

 

结论

 

关注点分离是软件产业的一个核心概念,控制器膨胀,例如当MVC控制器违反了关注点分离并无必要地包含了服务代码时,导致了许多重大问题。本文提出了一个可重用的设计,在个设计中,服务代理与MVC框架结合在一起使用,这样所有的这些问题就得到了避免。虽然示例代码使用基于Javaweb应用的Spring Web MVC来说明设计,但是提出的概念适用于所有类型的应用以及用任何语言为任何平台创建的软件,因为任何时候一旦强调良好的设计,其最终的结果就是代码更容易理解、开发、测试、重用和维护。

 

资源

 

           维基百科的委派模式

           核心J2EE模式——业务委派

           Spring

           Spring Web MVC

           Struts 2

 

 

Eric Spiegelberg是居住在MinneapolisJava/EE顾问,专注于SpringHibernate以及基于web的软件开发。

 

【基于QT的调色板】是一个使用Qt框架开发的色彩选择工具,似于Windows操作系统中常见的颜色选取器。Qt是一个跨平台的应用程序开发框架,广泛应用于桌面、移动和嵌入式设备,支持C++和QML语言。这个调色板功能提供了横竖两种渐变模式,用户可以方便地选取所需的颜色值。 在Qt中,调色板(QPalette)是一个关键的,用于管理应用程序的视觉样式。QPalette包含了一系列的颜色角色,如背景色、前景色、文本色、高亮色等,这些颜色可以根据用户的系统设置或应用程序的需求进行定制。通过自定义QPalette,开发者可以创建具有独特视觉风格的应用程序。 该调色板功能可能使用了QColorDialog,这是一个标准的Qt对话框,允许用户选择颜色。QColorDialog提供了一种简单的方式来获取用户的颜色选择,通常包括一个调色板界面,用户可以通过滑动或点击来选择RGB、HSV或其他色彩模型中的颜色。 横渐变取色可能通过QGradient实现,QGradient允许开发者创建线性或径向的色彩渐变。线性渐变(QLinearGradient)沿直线从一个点到另一个点过渡颜色,而径向渐变(QRadialGradient)则以圆心为中心向外扩散颜色。在调色板中,用户可能可以通过滑动条或鼠标拖动来改变渐变的位置,从而选取不同位置的颜色。 竖渐变取色则可能是通过调整QGradient的方向来实现的,将原本水平的渐变方向改为垂直。这种设计可以提供另一种方式来探索颜色空间,使得选取颜色更为直观和便捷。 在【colorpanelhsb】这个文件名中,我们可以推测这是与HSB(色相、饱和度、亮度)色彩模型相关的代码或资源。HSB模型是另一种常见且直观的颜色表示方式,与RGB或CMYK模型不同,它以人的感知为基础,更容易理解。在这个调色板中,用户可能可以通过调整H、S、B三个参数来选取所需的颜色。 基于QT的调色板是一个利用Qt框架和其提供的色彩管理工具,如QPalette、QColorDialog、QGradient等,构建的交互式颜色选择组件。它不仅提供了横竖渐变的色彩选取方式,还可能支持HSB色彩模型,使得用户在开发图形用户界面时能更加灵活和精准地控制色彩。
标题基于Spring Boot的二手物品交易网站系统研究AI更换标题第1章引言阐述基于Spring Boot开发二手物品交易网站的研究背景、意义、现状及本文方法与创新点。1.1研究背景与意义介绍二手物品交易的市场需求和Spring Boot技术的适用性。1.2国内外研究现状概述当前二手物品交易网站的发展现状和趋势。1.3论文方法与创新点说明本文采用的研究方法和在系统设计中的创新之处。第2章相关理论与技术介绍开发二手物品交易网站所涉及的相关理论和关键技术。2.1Spring Boot框架解释Spring Boot的核心概念和主要特性。2.2数据库技术讨论适用的数据库技术及其在系统中的角色。2.3前端技术阐述与后端配合的前端技术及其在系统中的应用。第3章系统需求分析详细分析二手物品交易网站系统的功能需求和性能需求。3.1功能需求列举系统应实现的主要功能模块。3.2性能需求明确系统应满足的性能指标和安全性要求。第4章系统设计与实现具体描述基于Spring Boot的二手物品交易网站系统的设计和实现过程。4.1系统架构设计给出系统的整体架构设计和各模块间的交互方式。4.2数据库设计详细阐述数据库的结构设计和数据操作流程。4.3界面设计与实现介绍系统的界面设计和用户交互的实现细节。第5章系统测试与优化说明对系统进行测试的方法和性能优化的措施。5.1测试方法与步骤测试环境的搭建、测试数据的准备及测试流程。5.2测试结果分析对测试结果进行详细分析,验证系统是否满足需求。5.3性能优化措施提出针对系统性能瓶颈的优化建议和实施方案。第6章结论与展望总结研究成果,并展望未来可能的研究方向和改进空间。6.1研究结论概括本文基于Spring Boot开发二手物品交易网站的主要发现和成果。6.2展望与改进讨论未来可能的系统改进方向和新的功能拓展。
1. 用户与权限管理模块 角色管理: 学生:查看个人住宿信息、提交报修申请、查看卫生检查结果、请假外出登记 宿管人员:分配宿舍床位、处理报修申请、记录卫生检查结果、登记晚归情况 管理员:维护楼栋与房间信息、管理用户账号、统计住宿数据、发布宿舍通知 用户操作: 登录认证:对接学校统一身份认证(模拟实现,用学号 / 工号作为账号),支持密码重置 信息管理:学生完善个人信息(院系、专业、联系电话),管理员维护所有用户信息 权限控制:不同角色仅可见对应功能(如学生无法修改床位分配信息) 2. 宿舍信息管理模块 楼栋与房间管理: 楼栋信息:名称(如 "1 号宿舍楼")、层数、性别限制(男 / 女 / 混合)、管理员(宿管) 房间信息:房间号(如 "101")、户型(4 人间 / 6 人间)、床位数量、已住人数、可用状态 设施信息:记录房间内设施(如空调、热水器、桌椅)的配置与完好状态 床位管理: 床位编号:为每个床位设置唯一编号(如 "101-1" 表示 101 房间 1 号床) 状态标记:标记床位为 "空闲 / 已分配 / 维修中",支持批量查询空闲床位 历史记录:保存床位的分配变更记录(如从学生 A 调换到学生 B 的时间与原因) 3. 住宿分配与调整模块 住宿分配: 新生分配:管理员导入新生名单后,宿管可按专业集中、性别匹配等规则批量分配床位 手动分配:针对转专业、复学学生,宿管手动指定空闲床位并记录分配时间 分配结果公示:学生登录后可查看自己的宿舍信息(楼栋、房间号、床位号、室友列表) 调整管理: 调宿申请:学生提交调宿原因(如室友矛盾、身体原因),选择意向宿舍(需有空位) 审批流程:宿管审核申请,通过后执行床位调换,更新双方住宿信息 换宿记录:保存调宿历史(申请人、原床位、新床位、审批人、时间) 4. 报修与安全管理模块 报修管理: 报修提交:学生选择宿舍、设施型(如 "
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值