《整洁架构之道》的最后一章《细节决定成败》又在讨论 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. springboot多文件上传、删除、下载到项目本地

    package com.example.demo.document; import cn.hutool.core.io.FileUtil; import cn.hutool.core.io.IoUti ...

  2. 图片 电力电网行业IT运维方案

    智能电网背景下,电力.电网企业信息化逐渐渗透到其业务链的各个环节,云计算.物联网.移动互联网等新技术的应用,更驱动信息化与业务创新深度融合.电力.电网企业集团信息系统群逐渐朝着一体化方向发展,信息链越 ...

  3. CTF中特别小的EXE是怎么生成的

    我们在打CTF时候,出题的爷爷们给出的exe都很小 就10k左右,有的甚至就5k,那时候我很郁闷啊.现在我也能了啊哈哈 不多bb按如下操作: 我们来看看正常的release生成的代码 #include ...

  4. C# 的空类型

    // 空类型 null int iii; // 默认 0 bool bbb; // 默认 false bool? b; // 空值 null int? i; // 空值 null string str ...

  5. 33. mvvm理解

    MVVM 是module view view-module 数据驱动视图开发模型,是MVC的改进版,采用业务逻辑和页面解构分离的开发思想: MVVM 实现了 view 和 module 的双向绑定,我 ...

  6. KubeSphere 社区双周报|07.05-07.18

    KubeSphere 社区双周报主要整理展示新增的贡献者名单和证书.新增的讲师证书以及两周内提交过 commit 的贡献者,并对近期重要的 PR 进行解析,同时还包含了线上/线下活动和布道推广等一系列 ...

  7. Eclipse中缓存清理

    (1)点击tomcat服务器,右键"clean-": (2)删除servers的tomcat服务器下的项目: 控制台显示"Servers窗口":菜单栏--Win ...

  8. Re:从零开始的pwn学习(栈溢出篇)

    写在前面:本文旨在帮助刚接触pwn题的小伙伴少走一些弯路,快速上手pwn题,内容较为基础,大佬轻喷.本文默认读者明白最基础的汇编指令的含义,并且已经配置好linux64位环境,明白基础的Linux指令 ...

  9. 饿了么Element UI之Upload组件图片上传【原创】

    图片文件换汤不换药,只要注意前端的写法即可 1.饿了么组件可以利用 http-request 的属性对上传进行自定义 :http-request="uploadFile" 2.设置 ...

  10. JS 通过年份获取月,季度,半年度,年度

    原文请关注公众号 "酒酒酒酒"​,关注公众号 回复  "JS 通过年份获取月,季度,半年度,年度" 可获取源代码 功能描述: 实例化一个函数,给函数内传递不同的 ...