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

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

被误解和滥用的分层原则,结局必然是API泥潭

自十多年前Spring Framework大范围流行以来,java项目的架构质量“看上去“有了巨大改善——组件化、分层架构、依赖注入、面向接口编程 这些优秀的实践实施起来 变得比过去容易很多也自然很多。

于是出现了一场“分层运动”,程序员们一窝蜂式的把代码分了层、层与层之间用interface隔离用Spring把组件装配成一个轻量级架构,

然后就宣称设计出了一个架构优良的系统——面向对象、松耦合、实现灵活可替换。。。

但是,事情并没有这么简单——OO强调的“高内聚 低耦合”,大家只记住了后半句——低耦合,高内聚的原则完全被违背了,或者说根本就没有被理解。

在这场一窝蜂的“分层解耦运动”中,很多没有经验的程序员从一个极端(上帝类 不分层 硬编码 硬连接 意大利面架构)走到了另一个极端(过度设计 过度分层 为了用接口而用接口);

最显著特征就是API泛滥,这是过度分层的必然结果,因为层与层之间不存在继承关系(因此protected不管用),只能是上层调用下层组件暴露的public方法——API,于是项目中很快就开始充斥大量啰嗦、雷同、含义模糊的接口和public方法。

合理的分层架构 应该呈现倒金字塔的形状——越接近顶层(前端展现)组件的数量越随项目规模线性增长,因此数量也越多;越往底层 组件数量会越精简——经过项目初期的增长后,后面就基本稳定不再增长;

所以,当你发现系统有两个相邻的层 组件数量和API数量基本相当,那说明这两个层可以合并,因为其中必然有一层很单薄 只做了传声筒和重复的工作;

传声筒不仅造成巨大的浪费(徒增了一层代码、大量重复的代码),而且这样的设计会不断的给开发者带来困惑——一个新的功能到底应该在哪层实现,最终必然会出现不一致的选择和编程风格——于是每一层都被放置了一部分逻辑——于是破坏了内聚性——一个level的逻辑散落在了多处!这个问题看似没什么大不了的,但是熟知“破窗效应”的人马上就会意识到,这个设计从一开始就制造了很多broken window,并且在鼓励后续开发维护人不断制造新的破窗。。。不夸张的说,无数项目就是死在这个慢性毒药上。

高内聚和低耦合是OO的基本原则,说白了就是通过合理的抽象设计将一个level的逻辑放在一处 不同level的逻辑分开放置

分开摆放只是体力活儿,真正重要和有技术含量的是前面的“合理抽象设计”这个技术活儿。

很多程序员用着spring 用着大量接口 用着分层设计,最终干的还是面向过程编程,这样做 甚至还不如不分层——不分层我维护起来还更方便些——在一个类能看到所有实现细节。很多人用了一堆接口搞出一堆组件,把逻辑毫无章法的随意摆放(其实是藏匿)在n多角落里,然后竟然得意的认为自己在解耦,其实tmd是在玩儿躲猫猫。这个躲猫猫一点都不好玩,因为软件开发是一项成人世界的严肃的商业活动!你要明白一个简单的事实——你写出的每一行代码都tmd是需要被你自己或其他接手项目的人无数次阅读无数次修改的!

解决方案:减少分层,在层内进行面向对象的分层抽象设计

为了避免API泛滥的泥潭,我们需要退一步,首先要避免过度分层,但是这并不是要大家退回到一个上帝class搞定一个业务模块的年代。我的解决方案简单归纳就是:

  • 在controller展现控制层 和db之间 只保留一个service层,消灭dao层,service直接依赖通用的dao utils,因为dao utils不随着业务线性增长 因此不算一个层,只能算lib,就好比你不会把string utils看作一个层。
  • 在这个丰满的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,子类不可改 */
    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程序员。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值