在学习设计模式的时候,总是被推荐先学习一下面向对象的六大原则,学习后果然受益匪浅。以下完全是我对六大基本原则的理解,和官网解释可能有出路,而且我更多是站在设计模式的角度,而不是面向对象的角度理解,如果有什么错误,敬亲谅解。
1.开闭原则
很多教程都把开闭原则作为这六大原则中最基本的原则,也就是说他是各个原则的核心。开闭原则指的是,一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。
至于这个具体怎么理解,我也看了很多教程,有些教程说当我们遇到新的需求,就需要我们对我们模块继承的形式进行扩展,而不是修改代码。这样的解释貌似有道理,但是如果真的这样做了,程序结构只会更加复杂,业务逻辑只会更不清晰,完全是一种作死的做法。当业务发生改变的时候,肯定是要修改代码的,不需要的东西留着只会让程序臃肿,让维护者搞不清什么是有用的代码,什么是已经过时的代码。我不太相信开闭原则的真谛是让我们走向这样一个死胡同。
对于开闭原则,我的理解是,我们在设计软件的时候,首先要搞清楚程序当中什么是未来可能变化的,什么是未来不会变化的。对于可能变化的东西,我们要提前给与可以对应的扩展接口。当然实际开发中,即便是我们认为这些不会变化的地方,未来还是可能变化的,这种变化就只能改代码了,但是这种修改仅仅只是改变个别细节,整体架构往往不会变化。而对于可能变化的地方,我们要给出可以足够扩展的空间,让其能够自由扩展,基本发生了重大的需求变更,整体架构也不会受影响。
例如:工厂模式中,我们将创建对象的过程封装了起来,这样创建对象对的过程中,创建的代码就和调用的代码尽可能地解除了耦合。创建过程可能是变化的,而调用过程往往是不变的。我们创建一个对象之后,需要为其初始化,设定一些配置,这个过程需要我们给出可以扩展的余地,而且要求扩展的时候不能影响调用部分,所以需要使用工厂模式,将可变的创建过程封装起来,供不变的调用模块。
这样说来,开闭原则的核心是解耦了?没错,我认为开闭原则讲的就是解构,但是他要求我们在设计的时候,重点要预判出什么地方是会发生变化的,并要为变化的地方留出余地。他强调的是对于可变部分进行解耦,使用扩展的方式而不是修改的方式应对变化,这样可以保证程序整体不会发生大的变化。
开闭原则对于开发框架、可以被复用的组件(如jar、dll、js插件等待)尤为重要,因为这些组件必须留出足够的空间去让调用者去扩展自己的业务。所以我们在开发这种组件的时候api才是最难设计的,因为我们设计的api必须能满足调用者对他的全部扩展,这样才能实现调用者在不修改组件代码的情况下实现自己的需求。
2.里氏替换原则
这个原则挺简单,讲的就使用接口的时候,我们必须确保子类能够替换父类所出现的任何地方。纯粹就字面的意思来讲,就是父类接口必须确保所有子类都可以实现需求,而不是某一个子类。
例如,java中HashMap和LinkedHashMap都是Map的子类。但是HashMap的顺序是随机的,而LinkedHashMap是固定的。当我们需要使用一个map,此map不需要要求key顺序可控的话,我们可以声明:
Map createMap(){ return new HashMap(); }
但我们要求顺序可控是,如果是这样:
Map createMap(){ return new LinkedHashMap(); }
上述代码就不太好了,因为HashMap也是Map的一个子类,但是他不能满足我们的需求,所以此处必须声明返回值类型为LinkedHashMap。例如我们在设计接口的方法的时候,如果调用者需要的是一个LinkedHashMap,我们就不能以HashMap类型做接口的声明。
当然里氏替换原则也可以从设计角度出发,他强调我们设计的时候需要确保父类定义了的时候,就应该覆盖到这个接口抽象出的业务的所有方法,而不需要他的子类再添加额外的扩展,同时各个子类也都该实现父类中的所有接口,只有这样才能保证我们设计出的东西是可扩展的。
开闭原则讲究扩展,里氏替换原则可以确保通过继承这种方式的扩展是可行的,否则就无法使用继承去扩展程序了。
例如:我们使用模板模式,做了一个类作为父类算法模板,一个子类继承了这个模板,并且顺利地完成了运行;但是另一个子类也继承了模板类,却无法运行,最终发现程序无法确保所有继承模板的子类可以替换父类,这就是个失败的模板模式。
从面相对象的角度来说,里氏替换原则是子类可以替代父类,但是从面相组件的角度看,其实是确保组件的api是完整的、不变的,子类和外界也是完全解耦的,只有这样我们开发出的扩展才能在不破坏原有框架的基础上运行。
3.依赖倒置原则
这个原则也是讲究解耦,他指的是让高层模块不要依赖低层模块。这个是个纯粹的面向接口,面向模块开发思路了,因为面向对象而言,各个对象自己的东西和外界是解耦的,因为封装特性把它们自己的属性都封装起来了,所以是不会和其他对象有耦合关系的(如c++的友元除外);但是各个对象仍然是相互耦合的,最强的耦合就属于继承耦合了,对象组合起码还是轻耦合,继承是个高耦合呀。依赖倒置就是让各个对象耦合度降低,高层模块不能继承底层模块,需要底层的东西也是外界注入而不是自己创建得到;同时调用的时候也是使用接口调用,而不是依赖具体实现,并且因为是接口调用,具体实现模块可以被任意替换了。
这样做就可以降低各个模块的耦合,也可以确保里氏替换原则的实现。实现里氏替换原则有什么用?当然是可以方便扩展了。
456.职责单一原则、接口隔离原则、最小知道原则(迪米特原则)
这几个原则很像,就放到一起说吧。这几个类都是在强调解耦(当然开闭原则、里氏替换原则、依赖倒置原则也是)。这几个原则基本都是强调解耦,只是站的角度不一样。职责单一原则指的是一个模块(接口)的功能尽量是单一的,这样不同功能的接口就不会耦合在一起了,同时维护起来也方便。接口隔离原则强调每个类继承的接口一定要保存最少,不能继承无用的接口,保证接口隔离原则的前提是先要保证职责单一原则。最后一个最小知道原则指的是模块是所有的依赖都要保存最少,这一点和接口隔离原则有点重复,或者可以说接最小知道原则包含接口隔离原则,同时最小知道原则还有对外界影响最小的意味。这几个原则说的都是类和接口设计要尽量降低耦合的问题。
其实不止面向对象开发,其他的很多都需要实现这六大基本原则。例如写css,这个样式表的开发和面向对象开发本来就是风马牛不相及,但是同样需要能够将需求抽象出来的思路是一致的,所以同样需要能够支持面向对象的6大原则。例:
我们需要实现这样的需求,要能写一个table的样式,而且需要保证这个table样式能够复用。要保证复用,就需要保证这个table样式职责单一,不能够涉及到table以外的样式,这样维护起来简单,同时用户只需要table,我们就不能输出用户不需要的样式,否则很容易和其他样式冲突,这就是职责单一原则的体现。
同时在实现这个table样式的时候,要将各个子元素的样式依赖到自己下面,例如写的tr样式要确保仅对应用了这个样式的table里的tr有效,对于没有使用这个样式的table的tr无影响,不能因为改变了我们的tr样式而影响了其他table的tr样式,这体现了最小知道原则。
同时这个table所需要的其他依赖也是要越少越好,例如如果这个table依赖于bootstarp,就没法做了。因为如果我们依赖bootstarp,其实也仅仅是依赖很少的一部分功能,绝大部分功能是我们不依赖的,不过因为bootstarp没有单个table样式的css(类比面向对象的接口),我会引入很多不必要的功能,这样会对其他部分造成污染。所以我们需要引入职责单一的类库,不能引入不需要的功能,这也与接口隔离原则所阐述的吻合。
总结:和设计模式的关系
简单介绍了一下这几大基本原则,再说说我对于他们和设计模式的理解。我们在学习设计模式的时候,有没有想过为什么要学习这个,学习了设计模式有什么好处?我在工作中经常发现很多经验欠缺的程序员,为了学习设计模式而学习设计模式,为了使用设计模式而使用设计模式,而有经验的程序员则不会这样。其实程序员开发的每一个程序都是从需求出发的,只有搞清楚我们一个项目的根本需求才有使用设计模式的意义。
六大原则中,最重要的肯定就是开闭原则了,我对开闭原则的理解是,你无须在每个细节上都要做到对扩展开放,对修改关闭,而是应该为了面对扩展而扩展。当有这种需求的时候,例如一个项目业务、算法可能会有重大的变化,或者本身开发的就是一个组件,这是才需要扩展,而扩展的时候才应该使用设计模式来实现这种需求,也就是说使用设计模式和我们平时开发一样,都是满足需求。
使用了设计模式不一定能真正地提高代码的结构和可维护性,所以有经验的程序员会按需求而使用设计模式。六大基本原则可以更好地帮我们如何分析,这样才能确定是否使用设计模式。事实上设计模式的使用上我建议无招胜有招,满足项目的额外需求(例如利于扩展、可复用、高可维护性)都是好招,无需管他是什么设计模式。