常见设计模式
<h1>创建型模式</h1>
<h2>1 单例模式</h2>
<p>意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点。</p>
<p>主要解决:一个全局使用的类频繁地创建与销毁。</p>
<p>何时使用:当您想控制实例数目,节省系统资源的时候。</p>
<p>如何解决:判断系统是否已经有这个单例,如果有则返回,如果没有则创建。</p>
<p>关键代码:构造函数是私有的。</p>
<p>优点:</p>
<ol>
<li>在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)。</li>
<li>避免对资源的多重占用(比如写文件操作)。</li>
</ol>
<p>缺点:</p>
<ol>
<li>没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。</li>
</ol>
<p>使用场景:</p>
<ol>
<li>要求生产唯一序列号。</li>
<li>WEB 中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来。</li>
<li>创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等。</li>
</ol>
<p>注意事项:getInstance() 方法中需要使用同步锁 synchronized (Singleton.class) 防止多线程同时进入造成 instance 被多次实例化。</p>
<h2>实现</h2>
<p>我们将创建一个 Singleton 类。Singleton 类有它的私有构造函数和本身的一个静态实例。Singleton 类提供了一个静态方法,供外界获取它的静态实例。</p>
<h3>1.1 饿汉式</h3>
<pre><code class="language-java">public class Singleton {
private static Singleton instance = new Singleton();
private Singleton();
public static Singleton getInstance() {
return instance;
}
}</code></pre>
<h3>1.2 双检锁</h3>
<pre><code class="language-java">public class Singleton {
private static volatile Singleton instance;
private Singleton();
public static Singleton getInstance() {
if (null == instance) {
synchronized(Singleton.class){
if (null == instance) {
instance = new Singleton();
}
}
}
return instance;
}
}</code></pre>
<h3>1.3 枚举式</h3>
<pre><code class="language-java">public enum Singleton {
INSTANCE;
public void doSomething(){
System.out.println("xxx");
}
}</code></pre>
<h2>2 工厂模式</h2>
<p>意图:定义一个创建对象的接口,让其子类自己决定实例化哪一个工厂类,工厂模式使其创建过程延迟到子类进行。</p>
<p>主要解决:主要解决接口选择的问题。</p>
<p>何时使用:我们明确地计划不同条件下创建不同实例时。</p>
<p>如何解决:让其子类实现工厂接口,返回的也是一个抽象的产品。</p>
<p>关键代码:创建过程在其子类执行。</p>
<p>优点: </p>
<ol>
<li>一个调用者想创建一个对象,只要知道其名称就可以了。 </li>
<li>扩展性高,如果想增加一个产品,只要扩展一个工厂类就可以。 </li>
<li>屏蔽产品的具体实现,调用者只关心产品的接口。</li>
</ol>
<p>缺点:</p>
<ol>
<li>每次增加一个产品时,都需要增加一个具体类和对象实现工厂,使得系统中类的个数成倍增加,在一定程度上增加了系统的复杂度;</li>
<li>同时也增加了系统具体类的依赖,这并不是什么好事。</li>
</ol>
<p>使用场景: </p>
<ol>
<li>日志记录器:记录可能记录到本地硬盘、系统事件、远程服务器等,用户可以选择记录日志到什么地方。 </li>
<li>数据库访问,当用户不知道最后系统采用哪一类数据库,以及数据库可能有变化时。 </li>
<li>设计一个连接服务器的框架,需要三个协议,"POP3"、"IMAP"、"HTTP",可以把这三个作为产品类,共同实现一个接口。</li>
</ol>
<p>注意事项:作为一种创建类模式,在任何需要生成复杂对象的地方,都可以使用工厂方法模式。有一点需要注意的地方就是复杂对象适合使用工厂模式,而简单对象,特别是只需要通过 new 就可以完成创建的对象,无需使用工厂模式。如果使用工厂模式,就需要引入一个工厂类,会增加系统的复杂度。</p>
<h2>实现</h2>
<p>我们将创建一个 Shape 接口和实现 Shape 接口的实体类。下一步是定义工厂类 ShapeFactory。使用 ShapeFactory 来创建获取 Shape 对象。
<img src="https://s2.ax1x.com/2020/01/10/lhOfEV.jpg" alt="工厂模式" /></p>
<h3>步骤1</h3>
<p>创建一个形状接口</p>
<h3>步骤2</h3>
<p>创建Shape接口的实现类</p>
<h3>步骤3</h3>
<p>创建一个ShapeFactory工厂,生成具体的实现类</p>
<h3>步骤4</h3>
<p>使用工厂,创建实现类</p>
<pre><code class="language-java">public interface Shape {
void draw();
}
public class Rectangle implements Shape {
@Override
public draw(){
System.out.println("draw a rectangle !");
}
}
public class Square implements Shape {
@Override
public void draw() {
System.out.println("draw s square !");
}
}
public class Circle {
@Override
public void draw() {
System.out.println("draw a circle !");
}
}
public class ShapeFactory {
public Shape getShape(String shape) {
if (shape.equalsIgnoreCase("rectangle")) {
return new Rectangle();
} else if (shape.equalsIgnoreCase("square")) {
return new Square();
} else if ((shape.equalsIgnoreCase("circle"))) {
return new Circle();
} else {
System.out.println("未找到对应的实现类");
}
}
}</code></pre>
<h1>结构型模式</h1>
<h2>3 适配器模式</h2>
<p>意图:将一个类的接口转换成客户希望的另外一个接口。适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。</p>
<p>主要解决:主要解决在软件系统中,常常要将一些"现存的对象"放到新的环境中,而新环境要求的接口是现对象不能满足的。</p>
<p>何时使用: 1、系统需要使用现有的类,而此类的接口不符合系统的需要。 2、想要建立一个可以重复使用的类,用于与一些彼此之间没有太大关联的一些类,包括一些可能在将来引进的类一起工作,这些源类不一定有一致的接口。 3、通过接口转换,将一个类插入另一个类系中。(比如老虎和飞禽,现在多了一个飞虎,在不增加实体的需求下,增加一个适配器,在里面包容一个虎对象,实现飞的接口。)</p>
<p>如何解决:继承或依赖(推荐)。</p>
<p>关键代码:适配器继承或依赖已有的对象,实现想要的目标接口。</p>
<p>优点: </p>
<ol>
<li>可以让任何两个没有关联的类一起运行。 </li>
<li>提高了类的复用。 </li>
<li>增加了类的透明度。 </li>
<li>灵活性好。</li>
</ol>
<p>缺点: </p>
<ol>
<li>过多地使用适配器,会让系统非常零乱,不易整体进行把握。比如,明明看到调用的是 A 接口,其实内部被适配成了 B 接口的实现,一个系统如果太多出现这种情况,无异于一场灾难。因此如果不是很有必要,可以不使用适配器,而是直接对系统进行重构。 </li>
<li>由于 JAVA 至多继承一个类,所以至多只能适配一个适配者类,而且目标类必须是抽象类。</li>
</ol>
<p>使用场景:有动机地修改一个正常运行的系统的接口,这时应该考虑使用适配器模式。</p>
<p>注意事项:适配器不是在详细设计时添加的,而是解决正在服役的项目的问题。</p>
<h2>实现</h2>
<p>我们有一个 MediaPlayer 接口和一个实现了 MediaPlayer 接口的实体类 AudioPlayer。默认情况下,AudioPlayer 可以播放 mp3 格式的音频文件。</p>
<p>我们还有另一个接口 AdvancedMediaPlayer 和实现了 AdvancedMediaPlayer 接口的实体类。该类可以播放 vlc 和 mp4 格式的文件。</p>
<p>我们想要让 AudioPlayer 播放其他格式的音频文件。为了实现这个功能,我们需要创建一个实现了 MediaPlayer 接口的适配器类 MediaAdapter,并使用 AdvancedMediaPlayer 对象来播放所需的格式。</p>
<p>AudioPlayer 使用适配器类 MediaAdapter 传递所需的音频类型,不需要知道能播放所需格式音频的实际类。AdapterPatternDemo,我们的演示类使用 AudioPlayer 类来播放各种格式。
<img src="https://s2.ax1x.com/2020/01/10/lhOICF.jpg" alt="适配器模式" /></p>
<h2>4 装饰者模式</h2>
<p>意图:动态地给一个对象添加一些额外的职责。就增加功能来说,装饰器模式相比生成子类更为灵活。</p>
<p>主要解决:一般的,我们为了扩展一个类经常使用继承方式实现,由于继承为类引入静态特征,并且随着扩展功能的增多,子类会很膨胀。</p>
<p>何时使用:在不想增加很多子类的情况下扩展类。</p>
<p>如何解决:将具体功能职责划分,同时继承装饰者模式。</p>
<p>关键代码: 1、Component 类充当抽象角色,不应该具体实现。 2、修饰类引用和继承 Component 类,具体扩展类重写父类方法。</p>
<p>优点:装饰类和被装饰类可以独立发展,不会相互耦合,装饰模式是继承的一个替代模式,装饰模式可以动态扩展一个实现类的功能。</p>
<p>缺点:多层装饰比较复杂。</p>
<p>使用场景: 1、扩展一个类的功能。 2、动态增加功能,动态撤销。3、Java的IO流。</p>
<p>注意事项:可代替继承。</p>
<h2>实现</h2>
<p>我们将创建一个 Shape 接口和实现了 Shape 接口的实体类。然后我们创建一个实现了 Shape 接口的抽象装饰类 ShapeDecorator,并把 Shape 对象作为它的实例变量。</p>
<p>RedShapeDecorator 是实现了 ShapeDecorator 的实体类。</p>
<p>DecoratorPatternDemo,我们的演示类使用 RedShapeDecorator 来装饰 Shape 对象。</p>
<p><img src="https://s2.ax1x.com/2020/01/10/lhOo34.jpg" alt="装饰者模式" /></p>
<h2>5 外观模式</h2>
<p>意图:为子系统中的一组接口提供一个一致的界面,外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。</p>
<p>主要解决:降低访问复杂系统的内部子系统时的复杂度,简化客户端与之的接口。</p>
<p>何时使用: 1、客户端不需要知道系统内部的复杂联系,整个系统只需提供一个"接待员"即可。 2、定义系统的入口。</p>
<p>如何解决:客户端不与系统耦合,外观类与系统耦合。</p>
<p>关键代码:在客户端和复杂系统之间再加一层,这一层将调用顺序、依赖关系等处理好。</p>
<p>优点: 1、减少系统相互依赖。 2、提高灵活性。 3、提高了安全性。</p>
<p>缺点:不符合开闭原则,如果要改东西很麻烦,继承重写都不合适。</p>
<p>使用场景: 1、为复杂的模块或子系统提供外界访问的模块。 2、子系统相对独立。 3、预防低水平人员带来的风险。</p>
<p>注意事项:在层次化结构中,可以使用外观模式定义系统中每一层的入口。</p>
<h2>实现</h2>
<p>我们将创建一个 Shape 接口和实现了 Shape 接口的实体类。下一步是定义一个外观类 ShapeMaker。</p>
<p>ShapeMaker 类使用实体类来代表用户对这些类的调用。FacadePatternDemo,我们的演示类使用 ShapeMaker 类来显示结果。</p>
<p><img src="https://s2.ax1x.com/2020/01/10/lhOhNT.jpg" alt="外观模式" /></p>
<h2>6 代理模式</h2>
<p>意图:为其他对象提供一种代理以控制对这个对象的访问。</p>
<p>主要解决:在直接访问对象时带来的问题,比如说:要访问的对象在远程的机器上。在面向对象系统中,有些对象由于某些原因(比如对象创建开销很大,或者某些操作需要安全控制,或者需要进程外的访问),直接访问会给使用者或者系统结构带来很多麻烦,我们可以在访问此对象时加上一个对此对象的访问层。</p>
<p>何时使用:想在访问一个类时做一些控制。</p>
<p>如何解决:增加中间层。</p>
<p>关键代码:实现与被代理类组合。
优点: 1、职责清晰。 2、高扩展性。 3、智能化。</p>
<p>缺点: 1、由于在客户端和真实主题之间增加了代理对象,因此有些类型的代理模式可能会造成请求的处理速度变慢。 2、实现代理模式需要额外的工作,有些代理模式的实现非常复杂。</p>
<p>使用场景:按职责来划分,通常有以下使用场景: 1、远程代理。 2、虚拟代理。 3、Copy-on-Write 代理。 4、保护(Protect or Access)代理。 5、Cache代理。 6、防火墙(Firewall)代理。 7、同步化(Synchronization)代理。 8、智能引用(Smart Reference)代理。</p>
<p>注意事项: 1、和适配器模式的区别:适配器模式主要改变所考虑对象的接口,而代理模式不能改变所代理类的接口。 2、和装饰器模式的区别:装饰器模式为了增强功能,而代理模式是为了加以控制。</p>
<h2>实现</h2>
<p>我们将创建一个 Image 接口和实现了 Image 接口的实体类。ProxyImage 是一个代理类,减少 RealImage 对象加载的内存占用。</p>
<p>ProxyPatternDemo,我们的演示类使用 ProxyImage 来获取要加载的 Image 对象,并按照需求进行显示。
<img src="https://s2.ax1x.com/2020/01/10/lhOqD1.jpg" alt="代理模式" /></p>
<h1>行为型模式</h1>
<h2>7 模板模式</h2>
<p>意图:定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。</p>
<p>主要解决:一些方法通用,却在每一个子类都重新写了这一方法。</p>
<p>何时使用:有一些通用的方法。</p>
<p>如何解决:将这些通用算法抽象出来。</p>
<p>关键代码:在抽象类实现,其他步骤在子类实现。</p>
<p>应用实例: 1、在造房子的时候,地基、走线、水管都一样,只有在建筑的后期才有加壁橱加栅栏等差异。 2、西游记里面菩萨定好的 81 难,这就是一个顶层的逻辑骨架。 3、spring 中对 Hibernate 的支持,将一些已经定好的方法封装起来,比如开启事务、获取 Session、关闭 Session 等,程序员不重复写那些已经规范好的代码,直接丢一个实体就可以保存。</p>
<p>优点: 1、封装不变部分,扩展可变部分。 2、提取公共代码,便于维护。 3、行为由父类控制,子类实现。</p>
<p>缺点:每一个不同的实现都需要一个子类来实现,导致类的个数增加,使得系统更加庞大。</p>
<p>使用场景: 1、有多个子类共有的方法,且逻辑相同。 2、重要的、复杂的方法,可以考虑作为模板方法。</p>
<p>注意事项:为防止恶意操作,一般模板方法都加上 final 关键词。</p>
<h2>实现</h2>
<p>我们将创建一个定义操作的 Game 抽象类,其中,模板方法设置为 final,这样它就不会被重写。Cricket 和 Football 是扩展了 Game 的实体类,它们重写了抽象类的方法。</p>
<p>TemplatePatternDemo,我们的演示类使用 Game 来演示模板模式的用法。
<img src="https://s2.ax1x.com/2020/01/10/lhOv4O.jpg" alt="模板模式" /></p>
<h2>8 命令模式</h2>
<p>意图:将一个请求封装成一个对象,从而使您可以用不同的请求对客户进行参数化。</p>
<p>主要解决:在软件系统中,行为请求者与行为实现者通常是一种紧耦合的关系,但某些场合,比如需要对行为进行记录、撤销或重做、事务等处理时,这种无法抵御变化的紧耦合的设计就不太合适。</p>
<p>何时使用:在某些场合,比如要对行为进行"记录、撤销/重做、事务"等处理,这种无法抵御变化的紧耦合是不合适的。在这种情况下,如何将"行为请求者"与"行为实现者"解耦?将一组行为抽象为对象,可以实现二者之间的松耦合。</p>
<p>如何解决:通过调用者调用接受者执行命令,顺序:调用者→接受者→命令。</p>
<p>关键代码:定义三个角色:1、received 真正的命令执行对象 2、Command 3、invoker 使用命令对象的入口</p>
<p>应用实例:struts 1 中的 action 核心控制器 ActionServlet 只有一个,相当于 Invoker,而模型层的类会随着不同的应用有不同的模型类,相当于具体的 Command。</p>
<p>优点: 1、降低了系统耦合度。 2、新的命令可以很容易添加到系统中去。</p>
<p>缺点:使用命令模式可能会导致某些系统有过多的具体命令类。</p>
<p>使用场景:认为是命令的地方都可以使用命令模式,比如: 1、GUI 中每一个按钮都是一条命令。 2、模拟 CMD。</p>
<p>注意事项:系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作,也可以考虑使用命令模式,见命令模式的扩展。</p>
<h2>实现</h2>
<p>我们首先创建作为命令的接口 Order,然后创建作为请求的 Stock 类。实体命令类 BuyStock 和 SellStock,实现了 Order 接口,将执行实际的命令处理。创建作为调用对象的类 Broker,它接受订单并能下订单。</p>
<p>Broker 对象使用命令模式,基于命令的类型确定哪个对象执行哪个命令。CommandPatternDemo,我们的演示类使用 Broker 类来演示命令模式。</p>
<p><img src="https://s2.ax1x.com/2020/01/10/lhO44U.jpg" alt="命令模式" /></p>
<h2>观察者模式</h2>
<p>意图:定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。</p>
<p>主要解决:一个对象状态改变给其他对象通知的问题,而且要考虑到易用和低耦合,保证高度的协作。</p>
<p>何时使用:一个对象(目标对象)的状态发生改变,所有的依赖对象(观察者对象)都将得到通知,进行广播通知。</p>
<p>如何解决:使用面向对象技术,可以将这种依赖关系弱化。</p>
<p>关键代码:在抽象类里有一个 ArrayList 存放观察者们。</p>
<p>应用实例: 1、拍卖的时候,拍卖师观察最高标价,然后通知给其他竞价者竞价。 2、西游记里面悟空请求菩萨降服红孩儿,菩萨洒了一地水招来一个老乌龟,这个乌龟就是观察者,他观察菩萨洒水这个动作。</p>
<p>优点: 1、观察者和被观察者是抽象耦合的。 2、建立一套触发机制。</p>
<p>缺点: 1、如果一个被观察者对象有很多的直接和间接的观察者的话,将所有的观察者都通知到会花费很多时间。 2、如果在观察者和观察目标之间有循环依赖的话,观察目标会触发它们之间进行循环调用,可能导致系统崩溃。 3、观察者模式没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而仅仅只是知道观察目标发生了变化。</p>
<p>使用场景:</p>
<ul>
<li>一个抽象模型有两个方面,其中一个方面依赖于另一个方面。将这些方面封装在独立的对象中使它们可以各自独立地改变和复用。</li>
<li>一个对象的改变将导致其他一个或多个对象也发生改变,而不知道具体有多少对象将发生改变,可以降低对象之间的耦合度。</li>
<li>一个对象必须通知其他对象,而并不知道这些对象是谁。</li>
<li>需要在系统中创建一个触发链,A对象的行为将影响B对象,B对象的行为将影响C对象……,可以使用观察者模式创建一种链式触发机制。</li>
</ul>
<p>注意事项: 1、JAVA 中已经有了对观察者模式的支持类。 2、避免循环引用。 3、如果顺序执行,某一观察者错误会导致系统卡壳,一般采用异步方式。</p>
<h2>实现</h2>
<p>观察者模式使用三个类 Subject、Observer 和 Client。Subject 对象带有绑定观察者到 Client 对象和从 Client 对象解绑观察者的方法。我们创建 Subject 类、Observer 抽象类和扩展了抽象类 Observer 的实体类。</p>
<p>ObserverPatternDemo,我们的演示类使用 Subject 和实体类对象来演示观察者模式。</p>
<p><img src="https://s2.ax1x.com/2020/01/10/lhOTgJ.jpg" alt="观察者模式" /></p>
<h2>10 策略模式</h2>
<p>意图:定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换。</p>
<p>主要解决:在有多种算法相似的情况下,使用 if...else 所带来的复杂和难以维护。</p>
<p>何时使用:一个系统有许多许多类,而区分它们的只是他们直接的行为。</p>
<p>如何解决:将这些算法封装成一个一个的类,任意地替换。</p>
<p>关键代码:实现同一个接口。</p>
<p>应用实例: 1、诸葛亮的锦囊妙计,每一个锦囊就是一个策略。 2、旅行的出游方式,选择骑自行车、坐汽车,每一种旅行方式都是一个策略。 3、JAVA AWT 中的 LayoutManager。</p>
<p>优点: 1、算法可以自由切换。 2、避免使用多重条件判断。 3、扩展性良好。</p>
<p>缺点: 1、策略类会增多。 2、所有策略类都需要对外暴露。</p>
<p>使用场景: 1、如果在一个系统里面有许多类,它们之间的区别仅在于它们的行为,那么使用策略模式可以动态地让一个对象在许多行为中选择一种行为。 2、一个系统需要动态地在几种算法中选择一种。 3、如果一个对象有很多的行为,如果不用恰当的模式,这些行为就只好使用多重的条件选择语句来实现。</p>
<p>注意事项:如果一个系统的策略多于四个,就需要考虑使用混合模式,解决策略类膨胀的问题。</p>
<h2>实现</h2>
<p>我们将创建一个定义活动的 Strategy 接口和实现了 Strategy 接口的实体策略类。Context 是一个使用了某种策略的类。</p>
<p>StrategyPatternDemo,我们的演示类使用 Context 和策略对象来演示 Context 在它所配置或使用的策略改变时的行为变化。
<img src="https://s2.ax1x.com/2020/01/10/lhOXE6.jpg" alt="策略模式" /></p>