《整洁架构之道》的最后一章《细节决定成败》又在讨论 Javaer 永恒的问题:分层后 DAO Service Controller 应该按功能分包还是按层分包。 按功能分包的人认为这些文件在业务上是一起的,应该放在同一个包。按层分包的人认为每个层代表了不同的技术,应该按层分包。

可以想象,按层分包的人主张,DAO 层应该打包成一个 jar,对 PG 数据库用户提供 PG 的 jar,对 mysql 的提供 mysql 的 jar,到时换库很灵活。

而按功能分包的人现在有一个更好的理由,未来可以按功能切分成微服务!

《细节决定成败》这一章似乎是国内的译者写的,Bob 应当不太可能关注这种问题。但是问题的确存在。

假如我们把系统拆分来看,在一个古老的 CS 系统,分为服务器和客户端两大部分。客户端是 .net 开发的,服务端是 java 开发的。业务运行在 Java 服务器,客户端同样也表达一些业务知识,当然,主要是呈现性的知识。在包的组织上,客户端基本和服务器会采取一一对应的包设计。

这种系统里一个功能分在两个项目中,上面讨论的问题也就消失了。

从这个类比看,按层拆分似乎是最正确的选择。假如开发语言不同,同一个功能分成两个工程就很合理了。

那么,为什么大家还要纠结拆分呢?如果从一开始就分为三个工程,dao,service,controller 各一个工程,问题不是解决了?

问题出在那儿?

首先最直接的感受是效率的损耗,一个功能分前后端可以说是无奈的选择,现在一个后端还要分三个工程,老板就要问一问有没有价值了。

DAO 层要独立,原因是所谓的“数据库是细节”,不能绑定太深,未来可能要换数据库。

Controller 要独立,因为它是属于 Web 的,不是业务。

随后,更多的分层来了,Service 要分 ServiceInterface 和 ServiceImpl,DAO 也要分 ServiceInterface 和 ServiceImpl。

Service 这么分的理由是 Controller 没有资格访问具体的 ServiceImpl,应当通过依赖反转由容器对 Controller 赋予 ServiceImpl。

而 DAO 拆分的理由是,Service 直接触碰 DAO 太脏,DAO 可能更换,甚至可以说,DAO 接口是由 Service 出具的,只有那点可怜的 DAOImpl 才属于 DAO 工程。

也就是说,如果切分为 3 个工程,Service 这层应当出具一个 Service 接口工程给 Controller 工程,并且出具一个 DAO 接口工程给 DAO 工程。

以上就是完美的划分。

但是,在实践中,

  1. Controller 代码太少了,Controller 是不能改变业务逻辑的,所以 Controller 仅仅是将 Service 的一些函数映射出去而已。专门搞一个 Controller 工程非常傻。
  2. 数据库没有想象的那么烂————Uncle Bob 踩过早期数据库的坑,他固执的认为数据库仅仅是一个存储,带着这种错误的认知他甚至没有把关系运算当作一个范式。但是在实际项目中,DAO并不是简单的存取,往往用到关系运算或如 mongodb 的 map-reduce,换数据库难度是非常大的。试想,Model 最终都在数据库里,Service 要取用数据都要走数据库,而号称实现业务的 Service,使用的却是普通的运行于内存的编程语言,随时可能因为 JVM 宕机导致业务逻辑中断,并且还不支持事务,由此,一个业务件竟然不敢持有业务状态。并且
  3. Service DAO Controller 全部位于同一个 Spring 容器,这使得这种切分显得很荒谬。Controller 是 Web 服务器层的东西,带有高强度的 IO,随时会被冲垮,Service 这么宝贵的东西竟然和 Controller 混在一起,是不是很奇葩。
  4. 这就导致很多业务最终并不是靠 Service.java 或 ServiceImp.java,而是通过规则引擎、消息队列、工作队列等工作。

这时有人会说, 照你这个说法,Java 根本就不适合写业务?不是没有可能。

我可以理直气壮的说,如果一个 Service 把持久扔掉,它根本没有资格做 Service。此外, Controller 不过是 Service 的 RPC 入口。

最严重的问题是,它很蹩脚,这么蹩脚的设计不禁让我们思考:什么是理想的划层?

看三个例子:

网络 7 层,包括应用层传输层等,HTTP 是建立在 TCP 上的,TCP 这一层不需要为 HTTP 做任何调整。上一层对下一层是完全无知无感的,当层划分好后,甚至能做到下一层的变动不会影响上一层。

又如操作系统的层,我们日用的进程文件等等,属于操作系统提供的层。这一层上的应用例如视频播放器完全不需要考虑我的进程有没有被调度到CPU,磁盘磁道怎么安排,如果用的是 NVME 怎么办等等,视频播放器绝不会说我要加一个功能需要操作系统改一改升个级。

像 JVM 这种跨平台虚拟机进一步对操作系统做了抽象,我们甚至可以编写与目标操作系统完全无关的程序。

上述这些分层都是完全解耦的,上一层依赖下一层,但是下一层对上一层无感,不需要考虑上一层的心情。

对照来看我们可以发现 Java 项目这种所谓的分层有多么荒谬。既然已经分层还要考虑横切还是竖切,那就说明根本没有实现正交,Service 层不是建立在 DAO 基础上,DAO 也不建立在 Service 上,它们互相缠绕,把一个现实的功能凌迟的到处都是。

为什么 Java 要把持久化到数据库搞成一个层呢?究其原因就是 Uncle Bob 等人踩过的一些坑使其一朝被蛇咬十年怕井绳,Uncle Bob 豪迈的宣称:ORM 只是细节,只应当放在 Dao 层;对象数据库害了我们,我们把宝押在某某数据库、对象数据库,犯了大错,诸如此类。

客户要求换数据库固然会导致重写很多代码,但是换程序语言呢?假如客户要把 Java 换成 C#,rust,python 呢?那我们是不是该庆幸当初写的全是 SQL,只要把数据库连接方式改一下把SQL抄过来就 OK?据此推理下去,为了随时替换编程语言,我们把系统搞成 nginx + 存储过程?

如果我们不像 Uncle Bob 等人一样,带着畏惧情绪去看待数据库,而是把关系运算也当作一种范式,OOP + 关系运算结合才是业务的实际状态,换数据库和换 Java 一样,是绝对偶发的情形,问题就会简单不少。重复一遍,业务是由数据库和 OOP 共同完成的

同样的,Service 不可能是一个闷葫芦,它要对外提供服务,就必然有对外服务的手段,而这根本不足以形成一个层。为什么呢?Controller 没有资格自行决定暴露哪些 Service 接口,暴露哪些是业务规定的,因此,暴露 Service 这个工作就应当由 Service 自己说明。这可以是 Annotation 也可以是配置文件之类的,但是这并不足以构成一个层。在现实代码中 Controller 主要工作就是做输入校验,把 request 转化为 Service 的查询对象,如果 request 只做这么一点工作,它应该是通用的,这部分工作应当变成一种协议,也就是 Tcp 层 Http 层之后的一个对象路由层,用过 mina 的人都知道我在说什么。

该层输出的对象大体是这样的

class UserQuery extends Query{
@Required
String name
GenderEnum gender
}
abstract class Query{
abstract void validate() throws ValidationError;
HttpRequest request;
}

至于向该层投放的对象,由于是 Restful 接口,输出的查询结果用普通的 POJO 甚至 Map 均可。

有兴趣还可以继续推演。如需要将 Service 保持参数形态,如 UserService.search(String name, Gender gender) 而不是 UserServer.search(UserQuery query))那么这种层应当怎么设计?

把完整的框架发明出来超出了本文讨论范围。

综上,

第一:Service 理应和 DAO 合并,业务本就是透过 Java 和关系运算共同实现的。

第二:Controller 可能没有资格存在,应当处理为一个对象路由层。

其它讨论:

  • 有人会说,那么其它的中间件呢?为什么只有数据库有这种地位?消息队列呢?

看完上述分析我们可以理解,消息队列对于 Service 同样是一个对象路由层。

  • 那么 ES 呢?

ES 之类其它带有运算的存储,必然是业务共同体。假如 ES 是替换数据库的,那么,根据需求,我们可以从原来的 Service 派生一个 ESService,覆盖部分方法(部分替换),也可以将原 Service 抽象为 Service + PGService,然后另外派生一个 ESService(完全替换)。

Java 项目愚蠢的分层及解决方案的更多相关文章

  1. 1,eclipse导入项目jdk版本不一样解决方案 2,java报javax.servlet.jsp cannot be resolved to a type

    一:eclipse导入项目jdk版本不一样解决方案 参考博文: https://www.cnblogs.com/chenmingjun/p/8472885.html 选中项目右键 --> Pro ...

  2. 一次从0到1的java项目实践清单

    虽说工作就是简单的事情重复做,但不是所有简单的事你都能有机会做的. 我们平日工作里,大部分时候都是在做修修补补的工作,而这也是非常重要的.做好修补工作,做好优化工作,足够让你升职加薪! 但是如果有机会 ...

  3. 一份从0到1的java项目实践清单

    虽说工作就是简单的事情重复做,但不是所有简单的事你都能有机会做的. 我们平日工作里,大部分时候都是在做修修补补的工作,而这也是非常重要的.做好修补工作,做好优化工作,足够让你升职加薪! 但是如果有机会 ...

  4. Java 项目JDBC 链接数据库中会出现的错误

    1.出现的地方 package com.jdbc; import java.sql.Connection; import java.sql.DriverManager; import java.sql ...

  5. IntelliJ IDEA14.1中java项目Maven中没有配置JDK时的问题

    在IntelliJ IDEA 14.1中使用在java项目中使用Maven时当没有在Maven中配置JDK编译版本.源码版本时,IDEA将默认的编译版本.源码版本设置为jdk5. 在IDEA中Lang ...

  6. 使用maven来管理您的java项目

    maven是一个项目管理工具,使用maven可以自动管理java项目的整个生命周期,包括编译.构建.测试.发布和报告等.在大型项目开发中,使用maven来管理是必不可少的. 一.安装maven 1.W ...

  7. Java web项目引用java项目,类型找不到

    Java web项目引用java项目,类型找不到 错误信息: java.lang.ClassNotFoundException: org.codehaus.jackson.map.ObjectMapp ...

  8. web项目引用Java项目,连接报错error HTTP Status 500 - Servlet execution threw an exception

    错误信息 项目背景: 一个web项目引用一个java Project,项目中添加了引用,但是打开页面访问,总报500错误.提示:servlet初始化错误. 环境:Eclipse luna JDK: 1 ...

  9. 在命令行中运行eclipse中创建的java项目

    在命令行中运行eclipse中创建的java项目 博客分类: java相关 javaeclipse命令行  由于项目要求,需要对eclipse中的项目进行打包,似的可以在客户机上不装eclipse的情 ...

  10. Java项目经验——程序员成长的关键(转载)

    Java就是用来做项目的!Java的主要应用领域就是企业级的项目开发!要想从事企业级的项目开发,你必须掌握如下要点:1.掌握项目开发的基本步骤2.具备极强的面向对象的分析与设计技巧3.掌握用例驱动.以 ...

随机推荐

  1. 进程管理中的active_mm是做什么的?

    在Linux内核中,进程管理涉及到许多复杂的数据结构和机制,其中active_mm是与内存管理相关的一个关键概念.理解active_mm需要先了解与之相关的一些基本内核结构和概念. 基本概念 mm_s ...

  2. 【PyTorch】state_dict详解

    这篇博客来自csdn,完全用于学习. Introduce 在pytorch中,torch.nn.Module模块中的state_dict变量存放训练过程中需要学习的权重和偏执系数,state_dict ...

  3. Android复习(四)权限—>仅在默认处理程序中使用的权限

    仅在默认处理程序中使用的权限 注意:本指南主要面向准备在 Google Play 商店发布应用的 Android 应用开发者.不过,无论您在哪里发布 Android 应用,为了保护用户隐私,最好都完成 ...

  4. npm install报错 SyntaxError: Unexpected end of JSON input while parsing near '...=GmVg\r\n-----END PGP'

    解决方法:  npm cache clean --force 然后重新执行:npm install即可

  5. 9.24 csp(没学会的网络流)

    T1.商品 因为边界 l , r 是线性移动的,所以答案可以线性改变,直接用set维护连续段(小于l的和大于r的)的个数,并维护ans即可. 因为set的一个小错误调了两个小时,代码打成了一坨,结果最 ...

  6. idea高效实用快捷键【待补充】

    1.快捷键 ctrl+alt+L代码格式化 2.快捷键 ctrl+h查看hierarchy,只能查看向上向下继承关系,而不能看实现了哪些接口. 3,选中右键--Diagram可以查看实现了哪些接口 4 ...

  7. DFS序求LCA

    DFS序求LCA 介绍 欧拉序求LCA 的数组总是会忘记开两倍,并且预处理的常数较大.用 DFS序求LCA 可以解决这些问题. 欧拉序:进节点和出节点会重复记录节点. DFS序:深度优先搜索的顺序,不 ...

  8. "安装VMware Tools"显示灰色的解决办法

    用VMware Workstation Pro好几年了,期间这个问题也遇到过好几次,这次把解决方案记录一下,若后续有其他情况其他解决方案将在此博文更新. Step1:关闭虚拟机: Step2:在虚拟机 ...

  9. 人工智能模型训练中的数据之美——探索TFRecord

    上一篇:<构建人工智能模型基础:TFDS和Keras的完美搭配> 序言:在人工智能模型的训练过程中,如何高效管理和处理大量数据是一个重要的课题.TensorFlow 的 TFRecord ...

  10. linux终端高级玩法详细介绍

    专注于收集整理更多好玩技巧 更改终端命令行颜色 vi /etc/profile PS1='[\[\e[32m\]\u\[\e[0m\]\[\e[35m\]@\[\e[0m\]\[\e[33m\]\h\ ...