您的位置:首页 > 运维架构 > 网站架构

论软件架构设计中被普遍误读误用的原则——分层

2018-01-31 12:46 507 查看
 在看到一个又一个的项目、一批又一批的程序员不断掉进同一个坑里以后,我决定写此文把这个问题好好梳理总结一下,
很可能大多数人根本没有意识到这是一个问题,也就注定了不可避免的重复这样的错误。

被误解和滥用的分层原则,结局必然是API泥潭
自十多年前Spring Framework大范围流行以来,java项目的架构质量“看上去“有了巨大改善——组件化、分层架构、依赖注入、面向接口编程 这些优秀的实践实施起来 变得比过去容易很多也自然很多。
于是出现了一场“分层运动”,程序员们一窝蜂式的把代码分了层、层与层之间用interface隔离用Spring把组件装配成一个轻量级架构,
然后就宣称设计出了一个架构优良的系统——面向对象、松耦合、实现灵活可替换。。。
但是,事情并没有这么简单——OO强调的“高内聚 低耦合”,大家只记住了后半句——低耦合,高内聚的原则完全被违背了,或者说根本就没有被理解。
在这场一窝蜂的“分层解耦运动”中,很多没有经验的程序员从一个极端(上帝类 不分层 硬编码 硬连接 意大利面架构)走到了另一个极端(过度设计 过度分层 为了用接口而用接口);
最显著特征就是API泛滥,这是过度分层的必然结果,因为层与层之间不存在继承关系(因此protected和private不管用),只能是上层调用下层组件暴露的public方法——API,于是项目中很快就开始充斥大量啰嗦、雷同、含义模糊的public方法。

合理的分层架构 应该呈现倒金字塔的形状——越接近顶层(前端展现)组件的数量越随项目规模线性增长,因此数量也越多;越往底层 组件数量会越精简——经过项目初期的增长后,后面就基本稳定不再增长;
所以,当你发现系统有两个相邻的层 组件数量和API数量基本相当,那说明这两个层可以合并,因为其中必然有一层很单薄 只做了传声筒;
传声筒不仅造成巨大的浪费(徒增了一层代码、大量重复的代码),而且这样的设计会不断的给开发者带来困惑——一个新的功能到底应该在哪层实现,最终必然会出现不一致的选择和变成风格——于是每一层都被放置了一部分逻辑——于是破坏了内聚性——一个level的逻辑散落在了多处!这个问题看似没什么大不了的,但是熟知“破窗效应”的人马上就会意识到,这个设计从一开始就制造了很多broken window,并且在鼓励后续开发维护人不断制造新的破窗。。。不夸张的说,无数项目就是死在这个陷阱里。

高内聚和低耦合是OO的基本原则,说白了就是进行合理的抽象设计,将一个level的逻辑放在一处实现 不同level的逻辑分开放置;
分层只是体力劳动,真正重要的是前面的“合理抽象设计”这个技术活儿。
很多程序员用着spring 用着大量接口 用着分层设计,最终干的还是面向过程编程,这样做 甚至还不如不分层——不分层我维护起来还更方便些——在一个类能看到所有实现细节。

解决方案:减少分层,在层内进行面向对象的分层抽象设计
为了避免API泛滥的泥潭,我们需要退一步,首先要避免过度分层,但是这并不是要大家退回到一个上帝class搞定一个业务模块的年代。
我的解决方案 简单归纳就是:
1 在controller展现控制层 和db之间 只保留一个service层,消灭dao层,service直接依赖通用的dao utils,因为dao utils不随着业务线性增长 因此不算一个层,只能算lib,就好比你不会把string utils看作一个层。
2 在这个丰满的service层内,通过合理使用design patterns进行分层抽象设计 开发出高内聚低耦合的业务逻辑组件。
听起来有点绕——分层抽象跟分层 有什么不同,看下三段代码对比你就明白了,实现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 void funA () {
.....doSth
this.funA1();
.....doSth
this.funA2();
this.doSth();
}

abstract protected void funA1();

abstract protected void funA2();

/** 默认实现 可被override */
protected void funA3(){...}

protected final 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程序员的本本能!
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: