设计模式之开篇
设计模式之开篇
最近开始学习设计模式,为了防止忘记效果不好,就写了设计模式系列的博客,以方便日后可以复习。在具体学习各个设计模式之前想理清楚下面的概念。
1、设计模式简介?
2、设计模式的作用?
3、设计模式指导原则?
一、设计模式简介
最开始接触设计模式的时候,是之前的实习面试时,当时很懵逼,第一次听到这个词。┗|`O′|┛ 嗷~~ 后来才去百度百科查了下:
设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结。
反复使用:这个特点是确实的,在spring源码当中就出现了很多模式,记忆中比较深刻的有模板模式,代理模式,单例模式,工厂模式等等。
分类编目:各种设计模式之间各有各的特点可以进行分类。
代码设计经验:设计模式,是为了指导设计而从经验中总结出来的套路。
还有一种说法是说,设计模式是可以解决特定场景的问题的一系列方法,其实我觉得这个解释更贴切一点。
二、设计模式的作用?
有过一定工作经验的人应该都知道,特别是那些在维护一个项目的人更是体会的贴切,像我就是其中一个,有的时候,一个很简单的需求,或者说,本来应该是很快就可以实现的需求,但是由于系统当初设计的时候没有考虑这些需求的变化,或者随着需求的累加,系统越来越臃肿,导致随便修改一处都可能造成不可预料的后果,或者是我本来可以修改下配置文件或者改一处代码就可以解决的事情,结果需要修改N处代码才可以达到我的目的。
以上都是非常可怕的后果,这些我已经深深体会过了。相反设计模式可以帮助我们改善系统的设计,增强系统的健壮性、可扩展性,为以后铺平道路。
但是,这些是我当初第一次接触设计模式时的感受,现在我并不这么认为,设计模式可以改善系统的设计是没错,但是过多的模式也会系统变的复杂。所以当我们第一次设计一个系统时,请将你确定的变化点处理掉,不确定的变化点千万不要假设它存在,如果你曾经这么做过,那么请改变你的思维,让这些虚无的变化点在你脑子中彻底消失。
因为我们完全可以使用另外一种手法来容纳我们的变化点,那就是重构,不过这是我们在讨论过设计模式之后的事情,现在我们就是要把这些设计模式全部理解,来锻炼我们的设计思维,而不是只做一个真正的码农。
三、设计模式指导原则?
面向对象有几个原则:单一职责原则、开闭原则、里氏代换原则、依赖倒转原则、接口隔离原则。
单一职责原则:意思是每个类都只负责单一的功能,切不可太多,并且一个类应当尽量的把一个功能做到极致。
举个简单的栗子,比如我要实现一个计算器:
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
public class Calculator {
public int add() throws NumberFormatException, IOException{
File file = new File("E:/caculator.txt");
BufferedReader br = new BufferedReader(new FileReader(file));
int a = Integer.valueOf(br.readLine());
int b = Integer.valueOf(br.readLine());
return a+b;
}
public static void main(String[] args) throws NumberFormatException, IOException {
Calculator calculator = new Calculator();
System.out.println("result:" + calculator.add());
}
}
上面有个计算器,有个方法从文本中读取两个数字然后做加法运算。很明显的看出这个方法存在多职责问题。万一后期需求说我要加个减法运算,怎么做呢?
很明显直接copy一份把加好改成减号,这时你明显感觉到,麻蛋,万一后面还有乘法、除法、取模那我不是得复制多份了,既造成代码的冗余也不符合系统设计啊。这时我们把代码改善下,分离出一个Reader来专门负责读取数据,Caculator 只负责运算。
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
public class Calculator {
public int add() throws NumberFormatException, IOException{
File file = new File("E:/calculator.txt");
BufferedReader br = new BufferedReader(new FileReader(file));
int a = Integer.valueOf(br.readLine());
int b = Integer.valueOf(br.readLine());
return a+b;
}
public static void main(String[] args) throws NumberFormatException, IOException {
Calculator calculator = new Calculator();
System.out.println("result:" + calculator.add());
}
}
下面是单独的计算器类:
package com.test;
import java.io.IOException;
public class Calculator {
public int add(int a,int b){
return a + b;
}
public static void main(String[] args) throws NumberFormatException, IOException {
Reader reader = new Reader("E:/data.txt");
Calculator calculator = new Calculator();
System.out.println("result:" + calculator.add(reader.getA(),reader.getB()));
}
}
这样就简单体现了类的单一职责。其实还可以具体把Reader抽象出来,因为我们的数据不止从文件里面取,还可能是网络等。
里氏代换原则:这个原则表达的意思是一个子类应该可以替换掉父类并且可以正常工作。
那么翻译成比较容易理解的话,就是说,子类一般不该重写父类的方法,因为父类的方法一般都是对外公布的接口,是具有不可变性的,你不该将一些不该变化的东西给修改掉。
上述只是通常意义上的说法,很多情况下,我们不必太理解里氏替换这个玩意,比如模板方法模式,缺省适配器,装饰器模式等一些设计模式,就完全不搭理这个玩意。
比如我们有某一个类,其中有一个方法,调用了某一个父类的方法。
//某一个类
public class SomeoneClass {
//有某一个方法,使用了一个父类类型
public void someoneMethod(Parent parent){
parent.method();
}
}
父类代码如下。
public class Parent {
public void method(){
System.out.println("parent method");
}
}
结果我有一个子类把父类的方法给覆盖了,并且抛出了一个异常。
public class SubClass extends Parent{
//结果某一个子类重写了父类的方法,说不支持该操作了
public void method() {
throw new UnsupportedOperationException();
}
}
这个异常是运行时才会产生的,也就是说,我的SomeoneClass并不知道会出现这种情况,结果就是我调用下面这段代码的时候,本来我们的思维是Parent都可以传给someoneMethod完成我的功能,我的SubClass继承了Parent,当然也可以了,但是最终这个调用会抛出异常。
public class Client {
public static void main(String[] args) {
SomeoneClass someoneClass = new SomeoneClass();
someoneClass.someoneMethod(new Parent());
someoneClass.someoneMethod(new SubClass());
}
}
这就相当于埋下了一个个陷阱,因为本来我们的原则是,父类可以完成的地方,我用子类替代是绝对没有问题的,但是这下反了,我每次使用一个子类替换一个父类的时候,我还要担心这个子类有没有给我埋下一个上面这种炸弹。
所以里氏替换原则是一个需要我们深刻理解的原则,因为往往有时候违反它我们可以得到很多,失去一小部分,但是有时候却会相反,所以要想做到活学活用,就要深刻理解这个原则的意义所在。
依赖倒转原则:这个原则描述的是一个现实当中的事实,即实现都是易变的,而只有抽象是稳定的,所以当依赖于抽象时,实现的变化并不会影响客户端的调用。
比如上述的计算器例子,我们的计算器其实是依赖于数据读取类的,这样做并不是很好,因为如果我的数据不是文件里的了,而是在数据库里,这样的话,为了不影响你现有的代码,你就只能将你的Reader类整个改头换面。
或者还有一种方式就是,你再添加一个DBReader类,然后把你所有使用Reader读取的地方,全部手动替换成DBReader,这样其实也还可以接受,那假设我有的从文件读取,有的从数据库读取,有的从XML文件读取,有的从网络中读取,有的从标准的键盘输入读取等等。
你想怎么办呢?
所以我们最好的做法就是抽象出一个抽象类或者是接口,来表述数据读取的行为,然后让上面所有的读取方式所实现的类都实现这个接口,而我们的客户端,只使用我们定义好的接口,当我们的实现变化时,我只需要设置不同的实际类型就可以了,这样对于系统的扩展性是一个大大的提升。
针对上面简单的数据读取,我们可以定义如下接口去描述。
public interface Reader {
public int getA();
public int getB();
}
接口隔离原则:也称接口最小化原则,强调的是一个接口拥有的行为应该尽可能的小。
比如我们设计一个手机的接口时,就要手机哪些行为是必须的,要让这个接口尽量的小,或者通俗点讲,就是里面的行为应该都是这样一种行为,就是说只要是手机,你就必须可以做到的。
上面就是接口隔离原则所挑剔的地方,假设你没有满足她,你或许会写出下面这样的手机接口。
public interface Mobile {
public void call();//手机可以打电话
public void sendMessage();//手机可以发短信
public void playBird();//手机可以玩愤怒的小鸟?
}
上面第三个行为明显就不是一个手机应该有的,或者说不是一个手机必须有的,那么上面这个手机的接口就不是最小接口,假设我现在的非智能手机去实现这个接口,那么playBird方法就只能空着了,因为它不能玩。
所以们更好的做法是去掉这个方法,让Mobile接口最小化,然后再建立下面这个接口去扩展现有的Mobile接口。
public interface SmartPhone extends Mobile{
public void playBird();//智能手机的接口就可以加入这个方法了
}
这样两个接口就都是最小化的了,这样我们的非智能手机就去实现Mobile接口,实现打电话和发短信的功能,而智能手机就实现SmartPhone接口,实现打电话、发短信以及玩愤怒的小鸟的功能,两者都不会有多余的要实现的方法。
最小接口原则一般我们是要尽量满足的,如果实在有多余的方法,我们也有补救的办法,而且有的时候也确实不可避免的有一些实现类无法全部实现接口中的方法,这时候就轮到缺省适配器上场了,这个在后面再介绍。
开闭原则:它是面向对象设计的终极目标。其他几条,则可以看做是开闭原则的实现方法。
设计模式就是实现了这些原则,从而达到了代码复用、增加可维护性的目的。
在《大话设计模式》一书中,提到一句话与各位共勉,我觉得很有说服力,即用抽象构建框架,用细节实现扩展。