多层构架在实践中一些问题

最近要开始一个新项目,上一个项目采用贫血的多层构架,感觉IDao,IService大多数都无用,虽然有IDE辅助但是仍然很痛苦,想请教一下大家在实践中是怎么使用多层构架的。

  1. DAO层是否有必要存在?毕竟对于一个电商网站你不太可能将数据序列化到文件吧,而且采用Redis等内存数据库也要特殊处理吧,不会直接替换一个DAO实现了事吧。如果只是作为关系数据库的封装,ORM也已经封装的不错了,Java中的常用的ORM也就Hibernate和ibatis一般根据人员学习成本相比中途也不会无痛切换吧。

  2. Service层什么情况下才需要有不同的实现类?Service不过是作为一些代理和跨dao,entity的调用,直接替换不同DAO实现来更换持久化方案我能理解,但是什么情况下才需要替换Service的具体实现呢?

  3. 开始时接口是否有必要存在?开始就采用接口基本就要两个文件来回跳来跳去,如果开始时候就直接使用实现类直到需要时在引入接口成本是否会很高?毕竟只是声明类型变了,业务逻辑没变。(该问题已有同学回答,参见每一个类都应该有一个接口吗?

引用别人对贫血模型和充血模型的总结(没记错的话应该是javaeye的robbin总结的):

对于Java来说,更加适合采用贫血的模型,Java比较适合于把一个复杂的业务逻辑分离到n个小对象中去,每个小对象描述单一的职责,n个对象 互相协作来表达一个复杂的业务逻辑,这n个对象之间的依赖和协作需要通过外部的容器例如IoC来显式的管理。但对于每个具体的对象来说,他们毫无疑问是贫 血的。

这种贫血的模型好处是:

1、每个贫血对象职责单一,所以模块解藕程度很高,有利于错误的隔离。

2、非常重要的是,这种模型非常适合于软件外包和大规模软件团队的协作。每个编程个体只需要负责单一职责的小对象模块编写,不会互相影响。

贫血模型的坏处是:

1、由于对象状态和行为分离,所以一个完整的业务逻辑的描述不能够在一个类当中完成,而是一组互相协作的类共同完成的。因此可复用的颗粒度比较 小,代码量膨胀的很厉害,最重要的是业务逻辑的描述能力比较差,一个稍微复杂的业务逻辑,就需要太多类和太多代码去表达(针对我们假定的这个简单的工时管 理系统的业务逻辑实现,ruby使用了50行代码,但Java至少要上千行代码)。

2、对象协作依赖于外部容器的组装,因此裸写代码是不可能的了,必须借助于外部的IoC容器。

对于Ruby来说,更加适合充血模型。因为ruby语言的表达能力非常强大,现在用ruby做企业应用的DSL是一个很热门的领域,DSL说白了就是用来描述某个行业业务逻辑的专用语言。

充血模型的好处是:

1、对象自洽程度很高,表达能力很强,因此非常适合于复杂的企业业务逻辑的实现,以及可复用程度比较高。

2、不必依赖外部容器的组装,所以RoR没有IoC的概念。

充血模型的坏处是:

1、对象高度自洽的结果是不利于大规模团队分工协作。一个编程个体至少要完成一个完整业务逻辑的功能。对于单个完整业务逻辑,无法再细分下去了。

2、随着业务逻辑的变动,领域模型可能会处于比较频繁的变动状态中,领域模型不够稳定也会带来web层代码频繁变动。

  1. 为什么Java只适合贫血模型?
  2. 有没有什么场景下可以允许所有业务逻辑和持久化全部揉在Model中。
阅读 5.9k
2 个回答

1、DAO层是否有必要存在?
-->使用mybatis的mapper当dao就可以了,如果再封一层dao,略显多余

2、Service层什么情况下才需要有不同的实现类?
-->其实service层的接口根据使用场景,一般大部分可以去掉。需要有不同实现类的情况看需求,比如现在的需求是要导出excel2003格式的,以后可能会有需求要导出excel2007的,做个接口就比较灵活

3、开始时接口是否有必要存在?
-->对系统外暴露的api服务,要有接口,系统内的话,接口看情况而定

4、为什么Java只适合贫血模型?
-->跟静态类型相关的,其实不一定要局限贫血、充血模型,便于使用和维护就可以。

5、有没有什么场景下可以允许所有业务逻辑和持久化全部揉在Model中
-->为什么要揉到model里头呢,其实可以简化service层,采用命令模式,把业务逻辑放到命令里头,这样既可以避免service层无限庞大,可以保持精简,也可以使得业务逻辑相对独立可测。

打从年初开始,我就已经看到这个问题,但奈何当时自己水平也一般,并没有什么好的解决办法,只是默默的记住了这个问题。

现在终于有所积淀,是时候回馈一波社区了,顺便推广一下 DDD 这个业务代码提升神器~~~

(以下内容节选自我的个人博客怎样写好业务代码——那些年领域建模教会我的东西


业务场景

我主要负责传输网资源管理中的传输模块管理。这个部分涉及相对来说比较复杂的关联关系,所以如果代码组织不够严谨的话,极易绕晕和出错,下面以一张简单的概念图来描述一下部分实体对象间的关联关系。

clipboard.png

如图,简单来说时隙和通道是一对多的父子关系,同时业务电路和多段通道间的下级时隙,存在更复杂的一对多承载关系。

所以这个关系中复杂的地方在哪里呢?理解上经常会绕混,电路创建要选多段通道时隙,通道本身要管理多个时隙。这样时隙和通道以及电路同时存在了一对多的联系,如何去准确的理解和区分这种联系,并将之有效的梳理在代码层面就很需要一定技巧。

稍微拓展一下,改改资源类型,把业务电路换为传输通道,把传输通道换为传输段,这套关系同样成立。

另外,真实的业务场景中,业务电路的下层路由,不仅支持高阶通道,还支持段时隙,端口等资源。

整体设计

从业务场景中我们可以看到,资源实体间的模型关系其实有很多相类似的地方。比如大体上总是分为路由关系,和层级关系这么两种,那么如何才能高效的对这两种关系进行代码层面的建模以高效的进行复用,同时又保留有每个资源足够的拓展空间呢?

传统思路

我们先来考虑一下,按照传统的贫血模型去处理传输通道这个资源,针对传输通道的需求,它是如何处理的呢?
图片描述

最粗陋的常规模型,其实就是依据不同类型的资源对需求进行简单分流,然后按照管理划分 Controller 层,Service 层,Dao 层。各层之间的交互,搞得好一点的会通过抽象出的一个薄薄的 domain 域对象,搞的不好的直接就是 List,Map,Object 对象的粗陋组合。

代码示例

/**
  * 删除通道 不调用ejb了
  *   业务逻辑:
  *       判断是否被用,被用不能删除
  *       判断是否是高阶通道且已被拆分,被拆不能删除
  *       逻辑删除通道路由表
  *       清空关联时隙、波道的channel_id字段
  *          将端口的状态置为空闲   
  *       逻辑删除通道表
  * @param paramLists 被删除通道,<b>必填属性:channelID</b>
  * @return 是否删除成功
  * @throws BOException 业务逻辑判断不满足删除条件引发的删除失败<br>
  *                     <b>通道非空闲状态</b><br>
  *                     <b>高阶通道已被拆分</b><br>
  *                     <b>删除通道数据库操作失败</b><br>
  *                     <b>删除HDSL系统失败</b><br>
  * @return 成功与否
  */
 public String deleteChannel(String channelId){
        String returnResult = "true:删除成功";
        Map<String,String> condition = new HashMap<String, String>();
        condition.put("channelID",channelId);
        condition.put("min_index","0");
        condition.put("max_index","1");
        boolean flag=true;
        List<Map<String,Object>> channel = this.channelJdbcDao.queryChannel(condition);
        if(channel==null||channel.size()==0){
            return "false:未查询到通道信息";
        }
        //判断是否被用,被用不能删除
        String oprStateId = channel.get(0).get("OPR_STATE_ID").toString();
        if(!"170001".equals(oprStateId)){
            return "false:通道状态非空闲,不能删除";
        }
        //判断是否是高阶通道且已被拆分,被拆不能删除
        flag=this.channelJdbcDao.isSplited(channelId);
        if(!flag){
            return "false:高阶通道已被拆分,不能删除";
        }
        //逻辑删除通道路由表 并且清空关联时隙、波道的channel_id字段
        this.channelJdbcDao.deleteChannelRoute(channelId,oprStateId);
        //将通道端口的端口状态置为空闲
        this.channelJdbcDao.occupyPort(String.valueOf(channel.get(0).get("A_PORT_ID")),"170001");
        this.channelJdbcDao.occupyPort(String.valueOf(channel.get(0).get("Z_PORT_ID")),"170001");
        //逻辑删除通道表
        this.channelJdbcDao.delete(channelId);
        //如果通道走了HDSL时隙则删除HDSL系统及下属资源 ,这里重新调用了传输系统的删除的ejb。
        List<Map<String,Object>> syss=this.channelJdbcDao.findSysByChannel(channelId);
        for(int i=0;i<syss.size();i++){
            if("56".equals(syss.get(i).get("SYS_TYPE").toString())){
                List<Map<String,String>> paramLists = new ArrayList<Map<String,String>>();
                List paramList = new ArrayList();
                Map map = new HashMap();
                map.put("res_type_id", "1001");
                map.put("type", "MAIN");
                paramList.add(map);
                map = new HashMap();
                map.put("sys_id", syss.get(i).get("SYS_ID"));
                paramList.add(map);
                //EJB里面从第二个数据开始读取要删除的系统id,所以下面又加了一层 。
                map = new HashMap();
                map.put("res_type_id", "1001");
                map.put("type", "SUB");
                paramList.add(map);
                map = new HashMap();
                map.put("sys_id", syss.get(i).get("SYS_ID"));
                paramLists.add(map);
                String inputXml = this.createInputXML("1001", "deleteTrsSys",
                        "TrsSysService", paramLists);
                String result = this.getEJBResult(inputXml);
                if(result==null||"".equals(result)){//如果ejb处理失败是以抛异常形式,被底层捕获而未抛出,导致返回结果为空
                    return "false:删除HDSL系统失败";
                }
                Document document = XMLUtils.createDocumentFromXmlString(result);
                Element rootElement = XMLUtils.getRootElement(document);
                if (!TransferUtil.getResultSign(rootElement).equals("success")){
                    result =rootElement.attribute("description").getValue();
                    return "false:删除HDSL系统失败"+result;
                }
            }
        }
        return returnResult;
}

上面这些代码,是我司n年前的一段已废弃代码。其实也是很典型的一种业务代码编写方式。

可以看到,比较关键的几个流程是 :

空闲不能删除(状态验证)—>路由删除->端口置为空闲(路由资源置为空闲)->资源实体删除

其中各个步骤的具体实现,基本上都是通过调用 dao 层的方法,同时配合若干行service层代码来实现的。这就带来了第一个弊端,方法实现和 dao层实现过于紧密,而dao层的实现又是和各个资源所属的表紧密耦合的。因此即便电路的删除逻辑和通道的删除逻辑有很相似的逻辑,也必然不可能进行代码复用了。

如果非要将不同资源删除方法统一起来,那也必然是充斥着各种的 if/else 语句的硬性判断,总代码量却甚至没有减少反而增加了,得不偿失。

拓展思考

笔者曾经看过前人写的一段传输资源的保存方法的代码。

方法目的是支持传输通道/段/电路三个资源的保存,方法参数是一些复杂的 List,Map 结构组合。由于一次支持了三种资源,每种资源又有自己独特的业务判断规则,多情况组合以后复杂度直接爆炸,再外原本方法的编写人员没有定期重构的习惯,所以到了笔者接手的时候,是一个长达500多行的方法,其间充斥着各式各样的 if 跳转,循环处理,以及业务逻辑验证。

解决办法

面对如此棘手的情况,笔者先是参考《重构·改善既有代码设计》一书中的一些简单套路,拆解重构了部分代码。将原本的 500 行变成了十来个几十行左右的小方法,重新组合。

方案局限

  • 重构难度及时间成本巨大。

  • 有大量的 if/else 跳转根本没法缩减,因为代码直接调用 dao 层方法,必然要有一些 if/else 方法用来验证资源类型然后调用不同的 dao 方法

  • 也因为上一点,重构仅是小修小补,化简了一些辅助性代码的调用(参数提取,错误处理等),对于业务逻辑调用的核心代码却无法进行简化。service层代码量还是爆炸

小结

站在分层的角度思考下,上述流程按照技术特点将需求处理逻辑分为了三个层次,可是为什么只有 Service 层会出现上述复杂度爆炸的情况呢?

看到这样的代码,不由让我想到了小学时候老师教写文章,讲文章要凤头,猪肚,豹尾。还真是贴切呢 :-)

换做学生时代的我,可能也就接受了,但是见识过高手的代码后,才发现写代码并不应该是简单的行数堆砌。

业务情景再分析

对于一个具体的传输通道A的对象而言,其内部都要管理哪些数据呢?

  • 资源对象层面

    • 自身属性信息

  • 路由层面

    • 下级路由对象列表

  • 层次关系层面

    • 上级资源对象

    • 下级资源对象列表

可以看到,所有这些数据其实分为了三个层面:

  1. 作为普通资源,传输通道需要管理自身的属性信息,比如速率,两端网元,两端端口,通道类型等。

  2. 作为带有路由的资源,传输通道需要管理关联的路由信息,比如承载自己的下层传输段,下层传输通道等。

  3. 作为带有层次关系的资源,传输通道需要管理关联的上下级资源信息,比如自己拆分出来的时隙列表。

更进一步,将传输通道的这几种职责的适用范围关系进行全业务对象级别汇总整理,如下所示:
图片描述

各种职责对应的业务对象范围如下:

  • 同时具有路由和层次关系的实体:

    • 传输时隙、传输通道、传输段、传输电路

  • 具有路由关系的实体:

    • 文本路由

  • 具有层次结构关系的对象:

    • 设备、机房、端口

  • 仅作为资源的实体:

    • 传输网管、传输子网、传输系统

拓展思考

微观层面

以传输通道这样一个具体的业务对象来看,传统的贫血模型基本不会考虑到传输通道本身的这三个层次的职责。但是对象的职责并不设计者没意识到而变得不存在。如前所述的保存方法,因为要兼顾对象属性的保存,对象路由数据的保存,对象层次结构数据的保存,再乘上通道,段,电路三种资源,很容易导致复杂度的飙升和耦合的严重。

因此,500行的函数出现某种程度上也是一种必然。因为原本业务的领域知识就是如此复杂,将这种复杂性简单映射在 Service 层中必然导致逻辑的复杂和代码维护成本的上升。

宏观层面

以各个资源的职责分类来看,具备路由或层次关系的资源并不在少数。也就是说,贫血模型中,承担类似路由管理职责的代码总是平均的分散在通道,段,电路的相关 Service 层中。

每种资源都不同程度的实现了一遍,而并没有有效的进行抽象。这是在业务对象的代码模型角度来说,是个败笔。

在这种情况下就算使用重构的小技巧,所能做的也只是对于各资源的部分重复代码进行抽取,很难自然而然的在路由的业务层面进行概念抽象。

既然传统的贫血模型没法应对复杂的业务逻辑,那么我们又该怎么办呢?

新的架构

代码示例

@Transactional
public int deleteResRoute(ResIdentify operationRes) {
    int result = ResCommConst.ZERO;
    
    //1:获得需要保存对象的Entity
    OperationRouteResEntity resEntity = context.getResEntity(operationRes,OperationRouteResEntity.class);
    
    //2:获得路由对象
    List<OperationResEntity> entityRoutes = resEntity.loadRouteData();

    //3:删除路由
    result = resEntity.delRoute();
    
    //4:释放删除的路由资源状态为空闲
    this.updateEntitysOprState(entityRoutes, ResDictValueConst.OPR_STATE_FREE);

    //日志记录
    resEntity.loadPropertys();
    String resName = resEntity.getResName();
    String resNo = resEntity.getResCode();
    String eport = "删除[" + ResSpecConst.getResSpecName(operationRes.getResSpcId()) + ": " + resNo + "]路由成功!";
    ResEntityUtil.recordOprateLog(operationRes, resName, resNo, ResEntityUtil.LOGTYPE_DELETE, eport);
    
    return result;
}

上述代码是我们传输业务模块的删除功能的service层代码片段,可以看到相较先前介绍的代码示例而言,最大的不同,就是多出来了个 entity 对象,路由资源的获取是通过这个对象,路由资源的删除也是通过这个对象。所有操作都只需要一行代码即可完成。对电路如此,对通道也是如此。

当然,别的service层代码也可以很方便的获取这个entity对象,调用相关的方法组合实现自己的业务逻辑以实现复用。

那么这种效果又是如何实现的呢?

(和编程避免重复代码一样,重复编写一段文字并没有什么价值,这里只是节选,推广下自己的博客,想了解更多还是去我的博客里看吧)

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题