在回答这个问题以前,必须强调几个常识
第一个常识,设计模式并不能改善代码的质量,它是目的导向的。如果你去看GOF编写的《设计模式》(其他所有的设计模式的书都基于此),它每个模式都有一个“动机”。
这意味着,你只有需要有这个动机,你才应该使用这个设计模式,而不是说,我先用了设计模式,设计模式能给我什么用处。
这就好比扳手、锤子都是用来修理的。你的东西没有毛病,你没有使用锤子的动机非用锤子砸它,只是破坏。
第二个常识,任何设计模式都是以增加代码的复杂度,降低代码其它部分的灵活程度为代价,局部降低某个特定需求的代码的复杂度和提高某个特定部分的灵活度。同时还增加了代码编写、设计、重构的成本。
这好比空调和冰箱,它的确可以制冷,但是前提是增加了更多环境的热量并且消耗了能量才达成的。
由以上两个常识推出第三个,也可以说是原则,就是如无必要,勿增实体
。
回到你的问题,你的分析其实是对的,只是你还没有上升到原则的高度。
我们可以分析下依赖倒置,面向接口编程,比如说典型的,策略模式。
它解决的问题是,把某个类调用某个算法这个过程的算法,抽象出来一个接口(可能是C++的抽象类,C#的委托,或者Java的接口),然后让这个类去掉用这个接口。从而实现只要替换实现这个接口的策略类,就能替换算法。
它降低了作为算法编写者的工作难度,他实现别的算法只要局限在某个接口内。也提高了一个类调用不同算法的灵活性,因为只要实例化不同的策略,就可以了。但是请你注意,这种复杂性的降低,灵活性的提高,是把全系统的复杂性提高,全系统的灵活性降低作为代价或者前提的。
如你说的那样,“很多类里面都需要创建addXXXListener的方法”,也就是说,一个接口一旦确定下来,这个接口要更改,意味着所有的接口实现类都要修改。这其实造成了更大的维护困难。算法更改的灵活是牺牲了接口更改不灵活换来的。并且同一个流程,调用不同的算法很容易切换,这个灵活了,但是同一个算法,适应不同的接口,这个就不灵活了。再比如,很明显,为了实现策略算法,我们需要额外定义一个接口,起码一个实现的类,还有这个类注入到调用者的逻辑,这就是使用策略模式的额外代码开销。所有的设计模式,都是通过牺牲另一部分的灵活性换来这一部分的灵活性,增加另一部分的复杂性,降低这一部分的复杂性。
那么既然如此,设计模式还有什么用?不,设计模式有用,这是因为,对于具体的项目,你每个部分的权重不同。比如说你需要换50个算法,但是一点也不需要换接口,所以你才可以牺牲后者换前者。这也就回到了我说的第一个常识,你需要用设计模式,你才用设计模式。如果你为了用而用,没用到关键地方,你用了比不用更糟糕。这就好比虽然空调没有办法降低整个大环境的温度,但是你可以让所有人或者大部分人都待在房间内,然后使用空调,使得大部分人都凉快了。如果你不知道人在哪里,比如人都在户外,你在房子里开空调,只会让人更热。
为什么你有这样的困惑,本质上说,就是因为设计模式的本质,很少有人真正理解。
这个本质,你应该用熵增定律去理解。包括热力学定律都是从其中衍生出来的。简单来说,你在一个封闭的系统内,如果要降低某个部分的混乱程度,你必然要增加整个系统的混乱程度。并且你做功越大,这种混乱程度就会增加。
也许你会看到一个系统的混乱程度没有增加,那么是因为它不是封闭的。
比如说你编写一个库,你一直维护了所谓的简洁和灵活性,其实你是把混乱丢给了调用这个库的人。打一个比方说,eclipse是一个开放的,支持插件的ide。你可以体会下,给eclipse编写一个插件多么麻烦,如果你单独写一个编辑器并且增加一个功能,很容易搞定的事情,换到给eclipse写插件,你就要实现一大堆接口,甚至完全没有编写代码,但是也要实现这个接口。这就是一个库,把复杂丢给调用者,保持自身容易维护的例子。