实践中服务层和控制器的区别

21
我已经阅读了许多有关服务层和控制器之间差异的理论,并且对如何在实践中实现此功能有一些疑问。一个回答Service layer and controller: who takes care of what?说:

我试图将控制器限制在与验证http参数相关的工作,决定使用哪些参数调用什么服务方法,将什么放入httpsession或请求中,重定向或转发到哪个视图等类似于web的事情。

还有来自http://www.bennadel.com/blog/2379-a-better-understanding-of-mvc-model-view-controller-thanks-to-steven-neiland.htm的:

红旗警告:如果Controller滥用Service层,则我的Controller架构可能出现问题。

Controller对Service层发出太多请求。控制器向Service层发出许多不返回数据的请求。控制器在不传递参数的情况下向Service层发出请求。

目前,我正在使用Spring MVC开发Web应用程序,我有一个保存更改后的用户电子邮件的方法:

/**
     * <p>If no errors exist, current password is right and new email is unique,
     * updates user's email and redirects to {@link #profile(Principal)}
     */
    @RequestMapping(value = "/saveEmail",method = RequestMethod.POST)
    public ModelAndView saveEmail(
            @Valid @ModelAttribute("changeEmailBean") ChangeEmailBean changeEmailBean,
            BindingResult changeEmailResult,
            Principal user,
            HttpServletRequest request){

        if(changeEmailResult.hasErrors()){
            ModelAndView model = new ModelAndView("/client/editEmail");
            return model;
        }
        final String oldEmail = user.getName();
        Client client = (Client) clientService.getUserByEmail(oldEmail);
        if(!clientService.isPasswordRight(changeEmailBean.getCurrentPassword(), 
                                          client.getPassword())){
            ModelAndView model = new ModelAndView("/client/editEmail");
            model.addObject("wrongPassword","Password doesn't match to real");
            return model;
        }
        final String newEmail = changeEmailBean.getNewEmail();
        if(clientService.isEmailChanged(oldEmail, newEmail)){
            if(clientService.isEmailUnique(newEmail)){
                clientService.editUserEmail(oldEmail, newEmail);
                refreshUsername(newEmail);
                ModelAndView profile = new ModelAndView("redirect:/client/profile");
                return profile;
            }else{
                ModelAndView model = new ModelAndView("/client/editEmail");
                model.addObject("email", oldEmail);
                model.addObject("emailExists","Such email is registered in system already");
                return model;
            }
        }
        ModelAndView profile = new ModelAndView("redirect:/client/profile");
        return profile;
    }

你可以看到我向服务层发送了许多请求,并且我从控制器进行重定向 - 这是业务逻辑。请展示这个方法的更好版本。
还有另一个例子。我有这个方法,它返回用户的个人资料:
/**
     * Returns {@link ModelAndView} client's profile
     * @param user - principal, from whom we get {@code Client}
     * @throws UnsupportedEncodingException
     */
    @RequestMapping(value = "/profile", method = RequestMethod.GET)
    public ModelAndView profile(Principal user) throws UnsupportedEncodingException{
        Client clientFromDB = (Client)clientService.getUserByEmail(user.getName());
        ModelAndView model = new ModelAndView("/client/profile");
        model.addObject("client", clientFromDB);
        if(clientFromDB.getAvatar() != null){
            model.addObject("image", convertAvaForRendering(clientFromDB.getAvatar()));
        }
        return model;
    }

这个控制器的超类中放置了一个名为convertAvaForRendering(clientFromDB.getAvatar())的方法,这个方法是否放置得当?或者他必须被放在服务层中?

请帮忙看一下,对我来说真的很重要。


对我来说,“控制器向服务层发出太多请求。控制器向不返回数据的服务层发出了许多请求。控制器在没有传递参数的情况下向服务层发出请求。”听起来像胡说八道 - user180100
请展示这个方法的更好版本吗?尝试访问codereview.stackexchange.com。 - Grim
2个回答

20

Spring的Controller通常与Spring API(例如ModelModelAndView等)或Servlet API(HttpServletRequestHttpServletResponse等)相关联。方法可以返回String结果,该结果将解析为模板的名称(JSP等)。Controller很大程度上偏向于Web GUI,对Web技术具有强烈依赖。

Service则应根据业务逻辑设计,不应对客户端作出任何假设。我们可以远程服务,将其公开为Web服务,实现Web前端或Swing客户端。Service不应依赖于Spring MVC,Servlet API等。这样,如果需要重新定位应用程序,则可以重用大部分业务逻辑。

至于关于控制器层向服务层调用次数过多的注释,这主要是性能问题,我认为这是另一回事。如果每个服务层调用都查询数据库,则可能会遇到性能问题。如果服务层和控制器层没有在同一个JVM中运行,则也可能会遇到性能问题。这是设计应用程序时非常重要的另一个方面,但这表明您应该为控制器层提供更粗粒度的操作来封装服务调用。


7
在这两个例子中,为什么需要对Client进行强制转换?这是一种代码异味。
由于调用服务层也是建立数据库事务边界的调用,因此进行多次调用意味着它们在不同的事务中执行,因此彼此之间不一定一致。这就是为什么不鼓励进行多次调用的原因之一。@ArthurNoseda在他的答案中提到了其他很好的原因。
在您的第一个情况下,应该有一个对服务层的单个调用,例如像这样:
if (changeEmailResult.hasErrors()) {
    return new ModelAndView("/client/editEmail");
}
try {
    clientService.updateUserEmail(user.getName(),
                                  changeEmailBean.getCurrentPassword(),
                                  changeEmailBean.getNewEmail());
} catch (InvalidPasswordException unused) {
    ModelAndView model = new ModelAndView("/client/editEmail");
    model.addObject("wrongPassword", "Password doesn't match to real");
    return model;
} catch (DuplicateEmailException unused) {
    ModelAndView model = new ModelAndView("/client/editEmail");
    model.addObject("email", oldEmail);
    model.addObject("emailExists", "Such email is registered in system already");
    return model;
}
refreshUsername(newEmail);
return new ModelAndView("redirect:/client/profile");

你可以使用返回值而不是异常。
正如你所看到的,这将把更改电子邮件的业务逻辑委托给服务层,同时将所有与UI相关的操作保留在控制器中。

非常感谢您的示例。我将Client进行了强制转换,因为在我的Web应用程序中,我有一个名为User的类和两个子类Client和Translator,还有一个抽象类UserService - 它具有一些对于User和Translator都通用的方法。由于这个方法放置在ClientController中(而不是TranslatorController),所以我可以将其强制转换为Client。如果这样做不好,请告诉我更好的方法 :) - Yuriy
@Yuriy,service 如何知道为给定的用户返回 Client 还是 Translator?您应该有两个服务方法,一个用于每种类型。 - Andreas
所以,我将创建一个通用接口UserService和两个子类ClientService和TranslatorService。谢谢 :) - Yuriy
再问一句。我能有一个方法吗?因为控制器从主体对象中获取电子邮件,强制转换取决于控制器类型,并且我会为该方法编写适当的文档。编辑我的第一条评论:我有一个抽象类UserService - 它具有对客户和翻译器都通用的一些方法。这是因为两个类中存在非常相似的很多方法,并且为每个这样的方法创建两个不同的实现-这是重复代码。 - Yuriy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接