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

原文: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-controller,MVC)框架,在中间层(例如Spring)使用中间件框架,以及在数据层(例如Hibernate或者任何的JPA实现)使用对象关系映射(ORM)。

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

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

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

本文讨论了使用服务代理结合MVC框架的设计来避免控制器膨胀,对MVC模式的背景做了假设,虽然例子使用Java和Spring 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 Yourself,DRY)的原则,而且因为是利用了针对编程接口这一业界最佳实践的缘故,表现层和服务层现在可以由多个开发者并行来创建。服务代码的单元测试显示是不太复杂的,因为可以通过简单的直接与服务层交互的单元测试来进行,这大大简化了测试,使得孤立问题更为容易,并可节省时间。

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

既然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框架结合在一起使用,这样所有的这些问题就得到了避免。虽然示例代码使用基于Java的web应用的Spring Web MVC来说明设计,但是提出的概念适用于所有类型的应用以及用任何语言为任何平台创建的软件,因为任何时候一旦强调良好的设计,其最终的结果就是代码更容易理解、开发、测试、重用和维护。

 

资源

 

           维基百科的委派模式

           核心J2EE模式——业务委派

           Spring

           Spring Web MVC

           Struts 2

 

 

Eric Spiegelberg是居住在Minneapolis的Java/EE顾问,专注于Spring、Hibernate以及基于web的软件开发。

 

相关推荐