设计模式
设计模式简介
设计模式(Design pattern)代表了最佳的实践,通常被有经验的面向对象的软件开发人员所采用。设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案。
设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了重用代码、让代码更容易被他人理解、保证代码可靠性。
1、设计模式的使用
设计模式在软件开发中的两个主要用途:
开发人员的共同平台。设计模式提供了一个标准的术语系统,且具体到特定的情景。例如,单例设计模式意味着使用单个对象,这样所有熟悉单例设计模式的开发人员都能使用单个对象,并且可以通过这种方式告诉对方,程序使用的是单例模式。
最佳的实践。设计模式已经经历了很长一段时间的发展,他们提供了软件开发过程中面临的一般问题的最佳解决方案。学习这些模式有助于经验不足的开发人员通过一种简单快捷的方式来学习软件设计。
2、设计模式的类型
根据设计模式的参考书 Design Patterns - Elements of Reusable Object-Oriented Software(中文译名:设计模式- 可复用的面向对象软件元素) 中所提到的,总共有23种设计模式。这些模式可以分为三大类:创建型模式(Creational Patterns),结构型模式(Structural Patterns),行为模式(Behavioral Patterns)。当然,我们还会讨论另一种设计模式:J2EE设计模式。
序号 | 模式 & 描述 | 包括 |
1 | 创建型模式 | · 工厂模式(Factory Pattern) · 抽象工厂模式(Abstract Factory Pattern) · 单例模式(Singleton Pattern) · 建造者模式(Builder Pattern) · 原型模式(Prototype Pattern) |
2 | 结构型模式 | · 适配器模式(Adapter Pattern) · 桥接模式(Bridge Pattern) · 过滤器模式(Filter、Criteria Pattern) · 组合模式(Composite Pattern) · 装饰器模式(Decorator Pattern) · 外观模式(Facade Pattern) · 享元模式(Flyweight Pattern) · 代理模式(Proxy Pattern) |
3 | 行为型模式 | · 责任链模式(Chain of Responsibility Pattern) · 命令模式(Command Pattern) · 解释器模式(Interpreter Pattern) · 迭代器模式(Iterator Pattern) · 中介者模式(Mediator Pattern) · 备忘录模式(Memento Pattern) · 观察者模式(Observer Pattern) · 状态模式(State Pattern) · 空对象模式(Null Object Pattern) · 策略模式(Strategy Pattern) · 模板模式(Template Pattern) · 访问者模式(Visitor Pattern) |
4 | J2EE 模式 | · MVC 模式(MVC Pattern) · 业务代表模式(Business Delegate Pattern) · 组合实体模式(Composite Entity Pattern) · 数据访问对象模式(Data Access Object Pattern) · 前端控制器模式(Front Controller Pattern) · 拦截过滤器模式(Intercepting Filter Pattern) · 服务定位器模式(Service Locator Pattern) · 传输对象模式(Transfer Object Pattern) |
下面用一个图片来整体描述一下设计模式之间的关系:
2、设计模式的六大原则
开闭原则(Open Close Principle)
开闭原则的意思是:对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有的代码,实现一个热插拔的效果。简言之,是为了使程序的扩展性好,易于维护和升级。想要达到这样的效果,我们需要使用接口和抽象类,后面的具体设计中我们会提到这点。
里氏代换原则(Liskov Substitution Principle)
里氏代换原则是面向对象设计的基本原则之一。里氏代换原则中说,任何基类可以出现的地方,子类一定可以出现。LSP是继承复用的基石,只有当派生类可以替换掉基类,且软件单位的功能不受到影响时,基类才能真正被复用,而派生类也能够在基类的基础上增加新的行为。里氏代换原则是对开闭原则的补充。实现开闭原则的关键步骤就是抽象化,而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。
依赖倒转原则(Dependence Inversion Principle)
这个原则是开闭原则的基础,具体内容:针对接口编程,依赖于抽象而不是依赖于具体。
接口隔离原则(Interface Segregation Principle)
这个原则的意思是:使用多个隔离的接口,比使用单个接口要好,他还有另外一个意思是:降低类之间的耦合度。由此可见,其实设计模式就是从大型软件架构出发、便于升级和维护的软件设计思想,他强调降低依赖,降低耦合。
迪米特法则,又称最少知道原则(Demeter Principle)
最少知道原则是指:一个实体应当尽量少地与其他实体之间发生相互作用,似的系统功能模块相对独立。
合成复用原则(Composite Reuse Principle)
合成复用原则是指:尽量使用合成/聚合的方式,而不是使用继承。
适配器模式
适配器模式是作为两个不兼容的接口之间的桥梁。这种类型的设计模式属于结构型模式,他结合了两个独立接口的功能。
这种模式涉及到一个单一的类,该类负责加入独立的或不兼容的接口功能。
1、介绍
意图:将一个类的接口转换成客户希望的另外一个接口。适配器模式使得原来由于接口不兼容而不能一起工作的那些类可以一起工作。
主要解决:主要解决在软件系统中,常常要将一些“现存的对象”放到新的环境中,而新环境要求的接口是现对象不能满足的。
何时使用:系统需要使用现有的类,而此类的接口不符合系统的需要;想要建立一个可以重复使用的类,用于与一些彼此之间没有太大关联的一些类,包括一些可能在将来引进的类一起工作,这些源类不一定有一致的接口;通过接口转换,将一个类插入另一个类系中。(比如老虎和飞禽,现在多了一个飞虎,在不增加实体的需求下,增加一个适配器,在里面包容一个虎对象,实现飞的接口)
如何解决:继承或依赖(推荐)
关键代码:适配器继承或依赖已有的对象,实现想要的目标接口。
优点:可以让任何两个没有关联的类一起运行;提高了类的复用;增加了类的透明度;灵活性好。
缺点:过多地使用适配器,会让系统非常零乱,不易整体进行把握,比如,明明看到调用的是A接口,其实内部适配成了B接口的实现,一个系统如果太多出现这种情况,无异于一场灾难,因此如果不是很有必要,可以不使用适配器,而是直接对系统进行重构;由于Java至多继承一个类,所以至多只能适配一个适配者类,而且目标类必须是抽象类。
使用场景:有动机地修改一个正常运行的系统的接口,这时应该考虑使用适配器模式。
注意事项:适配器不是在详细设计时添加的,而是解决正在服役的项目的问题。
2、实现
我们有一个MediaPlayer接口和一个实现了MediaPlayer接口的实体类AudioPlayer。默认情况下,AudioPlayer可以播放mp3格式的音频文件。
我们还有另一个接口AdvancedMediaPlayer和实现了AdvancedMediaPlayer接口的实体类。该类可以播放vlc和mp4格式的文件。
我们想要让AudioPlayer播放其他格式的音频文件。为了实现这个功能,我们需要创建一个实现了MediaPlayer接口的适配器类MediaAdapter,并使用AdvancedMediaPlayer对象来播放所需的格式。
AudioPlayer使用适配器类MediaAdapter传递所需的音频类型,不需要知道能播放所需格式音频的实际类。AdapterPatternDemo,我们的演示类使用AudioPlayer类来播放各种格式。
步骤 1
为媒体播放器和更高级的媒体播放器创建接口
MediaPlayer.java
public interface MediaPlayer {
public void play(String audioType, String fileName);
}
AdvancedMediaPlayer.java
public interface AdvancedMediaPlayer {
public void playVlc(String fileName);
public void playMp4(String fileName);
}
步骤2
创建实现了AdvancedMediaPlayer接口的实体类
VlcPlayer.java
public class VlcPlayer implements AdvancedMediaPlayer{
@Override
public void playVlc(String fileName) {
System.out.println("Playing vlc file. Name: "+ fileName);
}
@Override
public void playMp4(String fileName) {
//什么也不做
}
}
Mp4Player.java
public class Mp4Player implements AdvancedMediaPlayer{
@Override
public void playVlc(String fileName) {
//什么也不做
}
@Override
public void playMp4(String fileName) {
System.out.println("Playing mp4 file. Name: "+ fileName);
}
}
步骤3
创建实现了MediaPlayer接口的适配器类。
MediaAdapter.java
public class MediaAdapter implements MediaPlayer {
AdvancedMediaPlayer advancedMusicPlayer;
public MediaAdapter(String audioType){
if(audioType.equalsIgnoreCase("vlc") ){
advancedMusicPlayer = new VlcPlayer();
} else if (audioType.equalsIgnoreCase("mp4")){
advancedMusicPlayer = new Mp4Player();
}
}
@Override
public void play(String audioType, String fileName) {
if(audioType.equalsIgnoreCase("vlc")){
advancedMusicPlayer.playVlc(fileName);
}else if(audioType.equalsIgnoreCase("mp4")){
advancedMusicPlayer.playMp4(fileName);
}
}
}
步骤4
创建实现了MediaPlayer接口的实体类
AudioPlayer.java
public class AudioPlayer implements MediaPlayer {
MediaAdapter mediaAdapter;
@Override
public void play(String audioType, String fileName) {
//播放 mp3 音乐文件的内置支持
if(audioType.equalsIgnoreCase("mp3")){
System.out.println("Playing mp3 file. Name: "+ fileName);
}
//mediaAdapter 提供了播放其他文件格式的支持
else if(audioType.equalsIgnoreCase("vlc")
|| audioType.equalsIgnoreCase("mp4")){
mediaAdapter = new MediaAdapter(audioType);
mediaAdapter.play(audioType, fileName);
}
else{
System.out.println("Invalid media. "+
audioType + " format not supported");
}
}
}
步骤5
使用AudioPlayer来播放不同类型的音频格式。
AdapterPatternDemo.java
public class AdapterPatternDemo {
public static void main(String[] args) {
AudioPlayer audioPlayer= new AudioPlayer();
audioPlayer.play("mp3", "beyond the horizon.mp3");
audioPlayer.play("mp4", "alone.mp4");
audioPlayer.play("vlc", "far far away.vlc");
audioPlayer.play("avi", "mind me.avi");
}
}
步骤6
验证输出
Playing mp3 file. Name: beyond the horizon.mp3
Playing mp4 file. Name: alone.mp4
Playing vlc file. Name: far far away.vlc
Invalid media. avi formatnot supported
桥接模式
桥接是用于把抽象化与实现化解耦,使得二者可以独立变化。这种类型的设计模式属于结构型模式,他通过提供抽象化和实现化之间的桥接结构,来实现二者的解耦。
这种模式涉及到一个作为桥接的接口,使得实体类的功能独立于接口实现类。这两种类型的类可被结构化改变而互不影响。
1、介绍
意图:将抽象部分与实现部分分离,使他们都可以独立的变化
主要解决:在有多种可能会变化的情况下,用继承会造成类爆炸问题,扩展起来不灵活
何时使用:实现系统可能有多个角度分类,每一种角度都可能变化。
如何解决:把这种多角度分类分离出来,让他们独立变化,减少他们之间耦合
关键代码:抽象类依赖实现类
应用实例:猪八戒从天蓬元帅转世投胎到猪,转世投胎的机制将尘世划分为两个等级,即:灵魂和肉体,前者相当于抽象化,后者相当于实现化。生灵通过功能的委派,调用肉体对象的功能,使得生灵可以动态地选择;墙上的开关,可以看到的开关是抽象的,不用管里面具体怎么实现的。
优点:抽象和实现的分离;优秀的扩展能力;实现细节对客户透明
缺点:桥接模式的引入会增加系统的理解与设计难度,由于聚合关联关系建立在抽象层,要求开发者对抽象进行设计与编程。
使用场景:如果一个系统需要在构件的抽象化角色和具体角色之间增加更多的灵活性,避免在两个层次之间建立静态的继承关系,通过桥接模式可以使他们在抽象层建立一个关联关系;对于那些不希望使用继承或多层次继承导致系统类的个数急剧增加的系统,桥接模式尤为适用;一个类存在两个独立变化的维度,且这两个维度都需要进行扩展
注意事项:对于两个独立变化的维度,适用桥接模式再适合不过了
2、实现
我们有一个作为桥接实现的DrawAPI接口和实现了DrawAPI接口的实体类RedCircle、GreenCircle。Shape是一个抽象类,将使用DrawAPI的对象。BridgePatternDemo,我们的演示类使用Shape类来画出不同颜色的圆。
步骤1
创建桥接实现接口
DrawAPI.java
public interface DrawAPI {
public void drawCircle(int radius, int x, int y);
}
外观模式
外观模式隐藏系统的复杂性,并向客户端提供了一个客户端可以访问系统的接口。这种类型的设计模式属于结构型模式,他向现有的系统添加了一个接口,来隐藏系统的复杂性。
这种模式涉及到一个单一的类,该类提供了客户端请求的简化方法和对现有系统类方法的委托调用。
1、介绍
意图:为子系统中的一组接口提供一个一致的界面,外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。
主要解决:降低访问负责系统的内部子系统时的复杂度,简化客户端与之的接口。
何时使用:1、客户端不需要知道系统内部的复杂联系,整个系统只需提供一个"接待员"即可。2、定义系统的入口。
如何解决:客户端不与系统耦合,外观类与系统耦合。
关键代码:在客户端和复杂系统之间再加一层,这一层将调用顺序、依赖关系等处理好。
应用实例:1、去医院看病,可能要去挂号、门诊、划价、取药,让患者或患者家属觉得很复杂,如果有提供接待人员,只让接待人员来处理,就很方便。2、JAVA的三层开发模式。
优点:1、减少系统相互依赖。 2、提高灵活性。3、提高了安全性。
缺点:不符合开闭原则,如果要改东西很麻烦,继承重写都不合适。
使用场景:1、为复杂的模块或子系统提供外界访问的模块。 2、子系统相对独立。 3、预防低水平人员带来的风险。
注意事项:在层次化结构中,可以使用外观模式定义系统中每一层的入口。
2、实现
我们将创建一个 Shape 接口和实现了 Shape 接口的实体类。下一步是定义一个外观类 ShapeMaker。
ShapeMaker 类使用实体类来代表用户对这些类的调用。FacadePatternDemo,我们的演示类使用 ShapeMaker 类来显示结果。
步骤1
创建一个接口
Shape.java
public interface Shape {
void draw();
}
步骤2
创建实现接口的实体类
Rectangle.java
public class Rectangle implements Shape {
@Override
public void draw() {
System.out.println("Rectangle::draw()");
}
}
Square.java
public class Square implements Shape {
@Override
public void draw() {
System.out.println("Square::draw()");
}
}
Circle.java
public class Circle implements Shape {
@Override
public void draw() {
System.out.println("Circle::draw()");
}
}
步骤3
创建一个外观类
ShapeMaker.java
public class ShapeMaker {
private Shape circle;
private Shape rectangle;
private Shape square;
public ShapeMaker() {
circle = new Circle();
rectangle = new Rectangle();
square = new Square();
}
public void drawCircle(){
circle.draw();
}
public void drawRectangle(){
rectangle.draw();
}
public void drawSquare(){
square.draw();
}
}
步骤4
使用该外观类画出各种类型的形状
FacadePatternDemo.java
public class FacadePatternDemo {
public static void main(String[] args) {
ShapeMaker shapeMaker= new ShapeMaker();
shapeMaker.drawCircle();
shapeMaker.drawRectangle();
shapeMaker.drawSquare();
}
}
步骤5
验证输出
Circle::draw()
Rectangle::draw()
Square::draw()
享元模式
享元模式主要用于减少创建对象的数量,以减少内存占用和提高性能。这种类型的设计模式属于结构型模式,他提供了减少对象数量从而改善应用所需的对象结构的方式。
享元模式尝试重用现有的同类对象,如果未找到匹配的对象,则创建新对象。我们将通过创建5个对象来画20个分布于不同位置的圆来演示这种模式。由于只有5种可用的颜色,所以color属性被用例检查现有的Circle对象。
1、介绍
意图:运用共享技术有效地支持大量细粒度的对象。
主要解决:在有大量对象时,有可能会造成内存溢出,我们把其中共同的部分抽象出来,如果有相同的业务请求,直接返回在内存中已有的对象,避免重新创建。
何时使用:1、系统中有大量对象。2、这些对象消耗大量内存。3、这些对象的状态大部分可以外部化。4、这些对象可以按照内蕴状态分为很多组,当把外蕴对象从对象中剔除出来时,每一组对象都可以用一个对象来代替。5、系统不依赖于这些对象身份,这些对象是不可分辨的。
如何解决:用唯一标识码判断,如果在内存中有,则返回这个唯一标识码所标识的对象。
关键代码:用 HashMap 存储这些对象。
应用实例:1、JAVA中的 String,如果有则返回,如果没有则创建一个字符串保存在字符串缓存池里面。2、数据库的数据池。
优点:大大减少对象的创建,降低系统的内存,使效率提高。
缺点:提高了系统的复杂度,需要分离出外部状态和内部状态,而且外部状态具有固有化的性质,不应该随着内部状态的变化而变化,否则会造成系统的混乱。
使用场景: 1、系统有大量相似对象。2、需要缓冲池的场景。
注意事项:1、注意划分外部状态和内部状态,否则可能会引起线程安全问题。2、这些类必须有一个工厂对象加以控制。
2、实现
我们将创建一个 Shape 接口和实现了 Shape 接口的实体类 Circle。下一步是定义工厂类 ShapeFactory。
ShapeFactory 有一个 Circle 的 HashMap,其中键名为 Circle 对象的颜色。无论何时接收到请求,都会创建一个特定颜色的圆。ShapeFactory 检查它的 HashMap 中的circle 对象,如果找到 Circle 对象,则返回该对象,否则将创建一个存储在hashmap 中以备后续使用的新对象,并把该对象返回到客户端。
FlyWeightPatternDemo,我们的演示类使用 ShapeFactory 来获取 Shape 对象。它将向 ShapeFactory 传递信息(red / green / blue/ black / white),以便获取它所需对象的颜色。
步骤1
创建一个接口
Shape.java
public interface Shape {
void draw();
}
步骤2
创建实现接口的实体类
Circle.java
public class Circle implements Shape {
private String color;
private int x;
private int y;
private int radius;
public Circle(String color){
this.color= color;
}
public void setX(int x) {
this.x= x;
}
public void setY(int y) {
this.y= y;
}
public void setRadius(int radius) {
this.radius= radius;
}
@Override
public void draw() {
System.out.println("Circle: Draw() [Color : " + color
+", x : " + x+", y :" + y+", radius :" + radius);
}
}
步骤3
创建一个工厂,生成基于给定信息的实体类的对象
ShapeFactory.java
import java.util.HashMap;
public class ShapeFactory {
private static final HashMap<String, Shape> circleMap= new HashMap();
public static Shape getCircle(String color) {
Circle circle= (Circle)circleMap.get(color);
if(circle== null) {
circle = new Circle(color);
circleMap.put(color, circle);
System.out.println("Creating circle of color : " + color);
}
return circle;
}
}
步骤4
使用该工厂,通过传递颜色信息来获取实体类的对象
FlyweightPatternDemo.java
public class FlyweightPatternDemo {
private static final String colors[] =
{ "Red", "Green", "Blue", "White", "Black" };
public static void main(String[] args) {
for(int i=0; i< 20; ++i) {
Circle circle=
(Circle)ShapeFactory.getCircle(getRandomColor());
circle.setX(getRandomX());
circle.setY(getRandomY());
circle.setRadius(100);
circle.draw();
}
}
private static String getRandomColor() {
return colors[(int)(Math.random()*colors.length)];
}
private static int getRandomX() {
return (int)(Math.random()*100 );
}
private static int getRandomY() {
return (int)(Math.random()*100);
}
}
步骤5
验证输出
代理模式
在代理模式中,一个类代表另一个类的功能。这种类型的设计模式属于结构型模式。
在代理模式中,我们创建具有现有对象的对象,以便向外界提供功能接口。
1、介绍
意图:为其他对象提供一种代理以控制对这个对象的访问。
主要解决:在直接访问对象时带来的问题,比如说:要访问的对象在远程的机器上。在面向对象系统中,有些对象由于某些原因(比如对象创建开销很大,或者某些操作需要安全控制,或者需要进程外访问),直接访问会给使用者或者系统结构带来很多麻烦,我们可以在访问此对象时加上一个对此对象的访问层。
何时使用:想在访问一个类时做一些控制。
如何解决:需要增加中间层。
关键代码:实现与被代理类组合
应用实例:1、Windows里面的快捷方式。 2、猪八戒去找高翠兰结果是孙悟空变的,可以这样理解:把高翠兰的外貌抽象出来,高翠兰本人和孙悟空都实现了这个接口,猪八戒访问高翠兰的时候看不出来这个是孙悟空,所以说孙悟空是高翠兰代理类。3、买火车票不一定在火车站买,也可以去代售点。4、一张支票或银行存单是账户中资金的代理。支票在市场交易中用来代替现金,并提供对签发人账号上资金的控制。5、spring aop。
优点:职责清晰;高扩展性;智能化
缺点:由于在客户端和真实主题之间增加了代理对象,因此有些类型的代理模式可能会造成请求的处理速度变慢;实现代理模式需要额外的工作,有些代理模式的实现非常复杂。
使用场景:按职责来划分,通常有以下使用场景: 1、远程代理。2、虚拟代理。3、Copy-on-Write代理。 4、保护(Protect or Access)代理。5、Cache代理。6、防火墙(Firewall)代理。7、同步化(Synchronization)代理。8、智能引用(Smart Reference)代理。
注意事项:1、和适配器模式的区别:适配器模式主要改变所考虑对象的接口,而代理模式不能改变所代理类的接口。2、和装饰器模式的区别:装饰器模式为了增强功能,而代理模式是为了加以控制。
2、实现
我们将创建一个Image接口和实现了Image接口的实体类。ProxyImage是一个代理类,减少RealImage对象加载的内存占用。
ProxyPatternDemo,我们的演示类使用ProxyImage来获取要加载的Image对象,并按照需求进行显示。
步骤1
创建一个接口
Image.java
public interface Image {
void display();
}
步骤2
创建实现接口的实体类
RealImage.java
public class RealImage implements Image {
private String fileName;
public RealImage(String fileName){
this.fileName= fileName;
loadFromDisk(fileName);
}
@Override
public void display() {
System.out.println("Displaying " + fileName);
}
private void loadFromDisk(String fileName){
System.out.println("Loading " + fileName);
}
}
ProxyImage.java
public class ProxyImage implements Image{
private RealImage realImage;
private String fileName;
public ProxyImage(String fileName){
this.fileName= fileName;
}
@Override
public void display() {
if(realImage== null){
realImage = new RealImage(fileName);
}
realImage.display();
}
}
步骤3
当请求时,使用ProxyImage来获取RealImage类的对象
ProxyPatternDemo.java
public class ProxyPatternDemo {
public static void main(String[] args) {
Image image= new ProxyImage("test_10mb.jpg");
//图像将从磁盘加载
image.display();
System.out.println("");
//图像将无法从磁盘加载
image.display();
}
}
步骤4
验证输出
Loading test_10mb.jpg
Displaying test_10mb.jpg
Displaying test_10mb.jpg