赞
踩
在看到一个又一个的项目、一批又一批的程序员不断掉进同一个坑里以后,我决定写此文把这个问题好好梳理总结一下,很可能大多数人根本没有意识到这是一个问题,也就注定了不可避免的重复这样的错误。
自十多年前Spring Framework大范围流行以来,java项目的架构质量“看上去“有了巨大改善——组件化、分层架构、依赖注入、面向接口编程 这些优秀的实践实施起来 变得比过去容易很多也自然很多。
于是出现了一场“分层运动”,程序员们一窝蜂式的把代码分了层、层与层之间用interface隔离用Spring把组件装配成一个轻量级架构,
然后就宣称设计出了一个架构优良的系统——面向对象、松耦合、实现灵活可替换。。。
但是,事情并没有这么简单——OO强调的“高内聚 低耦合”,大家只记住了后半句——低耦合,高内聚的原则完全被违背了,或者说根本就没有被理解。
在这场一窝蜂的“分层解耦运动”中,很多没有经验的程序员从一个极端(上帝类 不分层 硬编码 硬连接 意大利面架构)走到了另一个极端(过度设计 过度分层 为了用接口而用接口);
最显著特征就是API泛滥,这是过度分层的必然结果,因为层与层之间不存在继承关系(因此protected不管用),只能是上层调用下层组件暴露的public方法——API,于是项目中很快就开始充斥大量啰嗦、雷同、含义模糊的接口和public方法。
合理的分层架构 应该呈现倒金字塔的形状——越接近顶层(前端展现)组件的数量越随项目规模线性增长,因此数量也越多;越往底层 组件数量会越精简——经过项目初期的增长后,后面就基本稳定不再增长;
所以,当你发现系统有两个相邻的层 组件数量和API数量基本相当,那说明这两个层可以合并,因为其中必然有一层很单薄 只做了传声筒和重复的工作;
传声筒不仅造成巨大的浪费(徒增了一层代码、大量重复的代码),而且这样的设计会不断的给开发者带来困惑——一个新的功能到底应该在哪层实现,最终必然会出现不一致的选择和编程风格——于是每一层都被放置了一部分逻辑——于是破坏了内聚性——一个level的逻辑散落在了多处!这个问题看似没什么大不了的,但是熟知“破窗效应”的人马上就会意识到,这个设计从一开始就制造了很多broken window,并且在鼓励后续开发维护人不断制造新的破窗。。。不夸张的说,无数项目就是死在这个慢性毒药上。
高内聚和低耦合是OO的基本原则,说白了就是通过合理的抽象设计将一个level的逻辑放在一处 不同level的逻辑分开放置;
分开摆放只是体力活儿,真正重要和有技术含量的是前面的“合理抽象设计”这个技术活儿。
很多程序员用着spring 用着大量接口 用着分层设计,最终干的还是面向过程编程,这样做 甚至还不如不分层——不分层我维护起来还更方便些——在一个类能看到所有实现细节。很多人用了一堆接口搞出一堆组件,把逻辑毫无章法的随意摆放(其实是藏匿)在n多角落里,然后竟然得意的认为自己在解耦,其实tmd是在玩儿躲猫猫。这个躲猫猫一点都不好玩,因为软件开发是一项成人世界的严肃的商业活动!你要明白一个简单的事实——你写出的每一行代码都tmd是需要被你自己或其他接手项目的人无数次阅读无数次修改的!
为了避免API泛滥的泥潭,我们需要退一步,首先要避免过度分层,但是这并不是要大家退回到一个上帝class搞定一个业务模块的年代。我的解决方案简单归纳就是:
听起来有点绕——分层抽象跟分层有什么不同,看下三段代码对比你就明白了,实现1是上帝类搞定一切,实现2是常见的service和dao分层架构,实现3应用template method设计模式以分层继承方式组织代码。
- /** 上帝类 模块化编程,毕业设计水平 */
- public class ServiceA{
- public void funA(){
- .....doSth
- this.funA1();
- .....doSth
- this.funA2();
- .....doSth
- }
-
- private void funA1(){.....doSth}
- private void funA2(){.....doSth}
- private void funA3(){.....doSth}
- }
- /** 分层架构,入门程序员水平 */
- @Service
- public class ServiceA{
- @Rersource
- ServiceB serviceB;
- @Resource
- RepositoryC repoC;
-
- public void funA () {
- repoC.funA1(); //.....doSth
- repoC.funA2();//.....doSth
- serviceB.funA3();//.....doSth
- }
- }
- /** 面向对象设计,专业程序员水平 */
- public abstract class AbstractServiceA{
- /** 不变的业务逻辑和步骤、算法,封装在父类public final,子类不可改 */
- public final void funA () {
- .....doSth
- this.funA1();
- .....doSth
- this.funA2();
- this.doSth();
- }
- /** 扩展点1:可变的实现细节1,供子类实现 */
- abstract protected void funA1();
- /** 扩展点2:可变的实现细节2,供子类实现 */
- abstract protected void funA2();
-
- /** 扩展点3:可变的实现细节3,父类给出默认实现,子类可扩展也可不扩展 */
- protected void funA3(){...}
-
- /** 不变的实现细节,封装在父类private,子类不可见不可扩展 */
- private void doSth(){...}
- }
-
- public class ServiceA extends AbstractServiceA{
- protected void funA1(){}
-
- protected void funA2(){}
- }
如上三个实现的质量优劣应该是显而易见的,实现3具有更高的扩展性 复用性,对熟悉设计模式的人来说 也具有最好的代码可读性,因此最终获得了最好的可维护性 最低的修改成本。
分层抽象 本质上是将不变或很少变的逻辑封装在顶层基类,将易变多变的逻辑(实现细节)下放到具体子类,因此对实现细节的改动变得容易很多。不变的部分封装在基类 平时无需关注 不会浪费维护人精力。
分层抽象能获得高内聚性低耦合——不同level的逻辑可以聚在一处,尤其是对于具体子类 ,所有实现细节聚在一个子类中 ,同时又被不同的override重载函数优雅的划分开来,既有高内聚的方便又有低耦合的灵活性。
所谓开闭原则——对修改关闭 对扩展开放,我认为说的就是这个方法——抽象父类的final方法封装核心逻辑 对修改关闭,abstract protected方法便于子类扩展具体实现 。
最后,父类与子类之间通过protected方法交互,避免了public方法的泛滥——API泥潭。
OO面向对象的好处再怎么强调也不过分,但是也实在没有必要在2018年再去鼓吹了,OO本就应该是所有JAVA程序员的本能,不是吗?当然 我指的是合格的JAVA程序员。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。