这篇博文算是对《设计模式之禅》的读书笔记。这本书写得非常好,通俗易懂,强烈推荐!另外,也参考了很多其他的资料,包括http://www.runoob.com/design-pattern/design-pattern-tutorial.html以及网上一些博客等,再次表示感谢!之后,我会针对几个重点的设计模式,写一些代码,自己操作熟悉一遍,而其他一些设计模式就在概念上知道即可。
一、设计模式
设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结。使用设计模式的目的是为了代码可重用性、让代码更容易被他人理解、保证代码可靠性。 设计模式使代码编写真正工程化。设计模式的基础是面向对象编程(Object Oriented Programming,OOP),只要是提及设计模式,肯定都是基于面向对象编程的。因此,设计模式也要遵循OOP的基本原则。设计模式不是凭空变出来的。在实际的软件工程中,很少会一上来就用设计模式。设计模式都是在软件迭代的过程中,渐渐地根据需求,要对代码进行重构,才会用一些设计模式重写代码。归根到底,设计模式其实就是在一个软件工程中,彻底地贯彻OOP思想,而抛弃顺序的、结构化的编程思想。
二、OOP的四个基本特征
1、抽象
Java关于抽象,最常被讨论的就是abstract类和interfaces。
抽象是把系统中需要处理的数据和在这些数据上的操作结合在一起,根据功能、性质和用途等因素抽象成不同的抽象数据类型。每个抽象数据类型既包含了数据,又包含了针对这些数据的授权操作。在面向对象的程序设计中,抽象数据类型是用“类”这种结构来实现的。
2、封装
封装从字面上来理解就是包装的意思,专业点就是信息隐藏,是指利用抽象数据类型将数据和基于数据的操作封装在一起,使其构成一个不可分割的独立实体,数据被保护在抽象数据类型的内部,尽可能地隐藏内部的细节,只保留一些对外接口使之与外部发生联系。系统的其他对象只能通过包裹在数据外面的已经授权的操作来与这个封装的对象进行交流和交互。也就是说用户是无需知道对象内部的细节,但可以通过该对象对外的提供的接口来访问该对象。在编程中,与封装密切相关的就是属性和方法的访问权限private,public等。
3、继承
继承就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法,或子类从父类继承方法,使得子类具有父类相同的行为。继承的方法允许在不改动原程序的基础上对其进行扩充,这样使得原功能得以保存,而新功能也得以扩展。这有利于减少重复编码,提高软件的开发效率。继承是一种强耦合关系。
4、多态
所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的哪个方法,必须在由程序运行期间才能决定。
要注意,对于面向对象而言,多态分为编译时多态和运行时多态这两个内容。其中编译时多态是静态的,主要是指方法的重载,它是根据参数列表的不同来区分不同的函数,通过编译之后会变成两个不同的函数。这个时候,在编译时候已经知道要运行哪个函数了。而运行时多态(其实就是动态绑定)是动态的。他是指在执行期间(而不是编译期间)判断所引用对象的实际类型,并且根据其实际类型调用相应实际使用的方法。我们其实一般习惯上所说的多态,大部分时候都指的是运行时多态。在Java中,有两种形式可以实现多态,继承和接口。
(1)编译时多态
是通过方法重载实现的。重载,是指允许存在多个同名函数,而这些函数的参数表不同(或许参数个数不同,或许参数类型不同(就算是在一个继承链上下的类型,也认为是不同的),或许两者都不同)。其实严格来说,重载的概念并不属于“面向对象编程”,重载的实现是:编译器根据函数不同的参数表,对同名函数的名称做修饰,然后这些同名函数就成了不同的函数(至少对于编译器来说是这样的)。
(2)运行时多态
是通过覆盖(重写)实现的,也就是override。覆盖,是指子类重新定义父类的函数。方法覆盖需要子类方法和父类方法的名称、参数类型和返回类型都完全一致(其实返回类型不一定要一致,子类的方法返回类型比父类缩小也允许)。一般可以在子类的覆盖的方法前面加上@override来保证这个方法确实是覆盖。使用父类引用指向子类对象,再调用某一父类中的方法时,不同子类会表现出不同结果。如果通过一个父类的引用来调用某方法,实际上他会对应到内存中真正的对象,他会判断内存中真正的对象是子类对象还是父类对象,然后判断要调用哪个方法。查找顺序是先在子类中找,有就使用,没有就在父类中找,有就使用,再没有就报错了。
三、设计模式的六大原则(SOLIDD)
1、开闭原则(Open Close Principle,OCP)
OCP的意思是,软件应该对扩展开放,对修改关闭。也就是说,在程序需要进行拓展的时候,不能去修改原有的代码,而是应该通过扩展,实现一个热插拔的效果。OCP是最基础的一个原则,后面的另外五个原则,其实都是开闭原则的具体形态,都是为了实现开闭原则的工具和方法。
2、单一职责原则(Single Responsibility Principle,SRP),有些地方也叫合成复用原则(Composite Reuse Principle,CRP)
在软件系统中,一个类(大到模块,小到方法)承担的职责越多,它被复用的可能性就越小,而且一个类承担的职责过多,就相当于将这些职责耦合在一起,当其中一个职责变化时,可能会影响其他职责的运作,因此要将这些职责进行分离,将不同的职责封装在不同的类中,即将不同的变化原因封装在不同的类中,如果多个职责总是同时发生改变则可将它们封装在同一类中。也就是所谓的“有且仅有一个原因导致类的变更”。他要求尽量使用合成/聚合的方式,而不是使用继承。单一职责原则是实现高内聚、低耦合的指导方针,它是最简单但又最难运用的原则,需要设计人员发现类的不同职责并将其分离,而发现类的多重职责需要设计人员具有较强的分析设计能力和相关实践经验。
3、里氏替换原则(Liskov Substitution Principle,LSP)
任何基类(父类)可以出现的地方,子类一定可以出现。LSP 是继承复用的基石,只有当子类(派生类)可以替换掉基类,且软件单位的功能不受到影响时,基类才能真正被复用,而派生类也能够在基类的基础上增加新的行为。其实这里面包含了四层意思:
(1)子类必须完全实现父类的方法;
(2)子类可以有自己的个性,对于LSP来说,子类可以替换掉父类,但是反之不行,父类不一定能胜任子类的工作;
(3)重载或实现父类方法时输入参数可以被放大范围。方法中的输入参数被称为前置条件,而返回就是后置条件。在软件开发中,有一个契约优先的原则。这是因为软件开发是一个很多人参与的大工程,必须要约定一些大家都必须遵守的契约才能通力合作进行开发。这种设计方法叫做契约优先设计,这个设计思想是和LSP融合在一起的。LSP规定了一个契约,就是通过父类和接口设计。前置条件就是你要让我执行,必须满足某个条件。后置条件就是我执行完了,必然满足某个条件。
为了实现子类可以替换父类,只有两种情况。对于覆盖(重写)来说,当然是可以通过动态绑定完全进行替换的。对于重载来说,子类某个方法重载了父类的某个方法,这个子类的方法一范围一定要比父类扩大而不是缩小。
(4)覆盖或实现父类方法时返回结果可以被缩小范围。
4、接口隔离原则(Interface Segregation Principle,ISP)
这个原则的意思是:使用多个隔离的接口,比使用单个接口要好。也就是说,尽量建立多个单一接口,而不是一个臃肿庞大的接口。它还有另外一个意思是:降低类之间的耦合度。由此可见,其实设计模式就是从大型软件架构出发、便于升级和维护的软件设计思想,它强调降低依赖,降低耦合。
5、依赖倒转原则(Dependence Inversion Principle,DIP)
这个原则是开闭原则的基础,具体内容:针对接口编程,依赖于抽象而不依赖于具体。他主要有两个方面的内容:
(1)高层次的模块不应该依赖于低层次的模块,他们都应该依赖于抽象。当高层的模块不依赖于低层的模块时,这些高层模块就很容易在不同的环境中复用。其实直观上来说,就是高层次模块在编程的时候,应该使用的是低层次模块的抽象,也就是接口等,就算低层次模块的具体实现改变了,高层次的模块也不需要改动。
(2)抽象不应该依赖于具体实现,具体实现应该依赖于抽象。即使实现细节不断变动,只要抽象不变,用户程序就不需要变化。
6、迪米特法则,又称最少知道原则(Demeter Principle,DP)
最少知道原则是指:一个实体应当尽量少地与其他实体之间发生相互作用,使得系统功能模块相对独立。