模式的存在是为了解决复杂软件的应变性。而这一非功能性需求本身的潜台词就是,尽量别改动代码。如此,应对需求的变化手段就只剩下增量开发了。这是OCP的原则。
以结构型模式来说,一堆模式都在围绕着OCP原则打转。我们来逐步考查一下。
int x = 3; int y = 5; int z = x + y;
在变量这个级别,顺序罗列语句/表达式,这是我们最基本的编码方式了,现在,如果要将z的求和算法,变为求均值呢?你可能这样改动书写:
int z = (x + y) / 2;
这样当然没问题,也看不出什么影响来。然而符合OCP的书写应该是:
int r = z/2;
即定义新得变量,对原有变量进行引用、包装。
函数级别int calc(int x,int y){
return x + y;
}
现在同样要改变计算的规则,变为求均值,修改既有函数实现吗?像这样:
int calc(int x,int y){
return (x + y)/2;
}
要知道,这个既有函数可能具有高扇入性的,为了某一个客户端提出的需求而改动了它的实现,是极其危险得,你让其他客户端情何以堪?同样,正确的做法还是利用包装的思想:
int calcWrap(int x,int y){
return calc(x,y)/2;
}
此时,没有破坏原有实现,并且至少有求均值需求的客户端,只需要改动一行代码便可以满意了。然而,我们习惯了OO,习惯了多态性,自然希望连客户端代码都不要动。这时函数级别就不能解决问题了(高阶函数、函数式编程另说);使用方法重载当然也不能解决问题,毕竟方法名虽然不变,函数签名也是变动了的。这也是为何Java代码动辄一堆类,甚至说是“面向类编程”的原因。我们来考察类级别的好处。
类级别public interface Calc{
int calc(int x,int y);
}
class CalcImpl implements Calc{
int calc(int x,int y){
return x + y;
}
}
class CalcProxy implements Calc{
private CalcImpl calcImpl;
// ...getter/setter
int calc(int x, int y){
return calcImpl.calc(x,y)/2;
}
}
有经验的朋友看出来了,没错,就是简单的静态代理模式。利用接口特性的便利,父类对象指向子类实例的多态性,可以结合工厂模式等在运行时延迟、动态绑定实现,即保持了接口的一致性,使得客户端也可以只调用抽象API而无需再改动代码了。
上升到类级别,所谓的设计模式自然就出来了,你可以持有对抽象接口的引用,委托给具体实现类。对下层API调用并统一封装,提供外观类(门面模式);对上层接口又可继续进行转换适配(适配器模式)等。因此,在设计模式这种“术”的层面,你会发现代理模式、装饰器模式、适配器模式等结构型模式都很相像,实在没什么玄妙的。
我们从变量->函数->类级别的考察顺序,可以发现,OCP的原则——对修改关闭,对扩展开放,这种增量开发的思想核心就是包装。也就是OO三大特性之首的——封装。如果给OO三大特性继续排个高低辈分,我认为老大是封装。继承和多态性仍然是偏于技术层面得特性。而唯有封装,辨证得同时言明了独立性和扩展性之意,体现了面向对象得核心要义。



