如何理解这6种常见设计模式?

image.png

一 前言

最近在改造一些历史的代码,发现一个很明显的特点,大部分代码是记叙文,按照事件的发展过程将故事平铺直叙的讲解出来。

这种方式的好处是比较符合人类的思维习惯,一条主线讲到底,代码阅读起来没有太大难度,只要顺着藤就能摸到瓜,但是缺点也很明显,一旦故事线中需要插入一些新的元素,比如:加入一个新的人物角色、新的时间线,都会需要大量更改故事线以配合这个新元素的融入,甚至对原有文章造成破坏性的影响。

为了解决这个问题,人们总结出了很多种文章结构,例如:总-分结构,并列结构,总-分-总结构等等,有了这些结构,在加入新元素的时候,甚至不必考虑新元素与原故事情节的关联性,直接单拉一个分支故事线独立去讲就好了,只要能够在整体故事结束前,与汇聚到主线故事就可以了(是不是很像git?)。

在软件开发领域,也有很多这样的非常有用的实践总结,我们称之为设计模式。对于设计模式,大家都不陌生,随便找个人,估计都能讲出N个设计模式来,但是除了这些设计模式的概念,很多人不知道如何灵活运用这些设计模式。所以借这篇文章和大家共同学习设计模式的思想。

二 理解设计模式

我尽量用最通俗易懂的示例和语言来讲述我理解的设计模式,希望能对大家有所帮助。

另外也无需精通所有的设计模式,只要能够融汇贯通常见的设计模式,就能让你的代码变得优雅。就像程咬金只会三板斧,但是熟练度无人能及,照样能横行天下。

1 工厂模式(Factory)

简单工厂(Simple Factory)

小明追妹子的时候,请她喝了不少咖啡,她爱喝卡布奇诺,每次去咖啡店,只要跟服务员说“来杯卡布奇诺”就行了,虽然各家的口味有些不同,但是不管是星爸爸还是Costa,都能够提供卡布奇诺这种咖啡。这里的星爸爸和Costa就是生产咖啡的工厂。

(1)简单工厂模式结构

简单工厂模式包含如下角色:

  • Factory:工厂角色-负责实现创建所有实例的内部逻辑.

  • Product:抽象产品角色-是所创建的所有对象的父类,负责描述所有实例所共有的公共接口。

  • ConcreteProduct:具体产品角色-是创建目标,所有创建的对象都充当这个角色的某个具体类的实例。

结构图:

image.png

时序图:
image.png

(2)优缺点


    • 优点:客户类和工厂类分开。消费者任何时候需要某种产品,只需向工厂请求即可。消费者无须修改就可以接纳新产品。

  • 缺点:是当产品修改时,工厂类也要做相应的修改。

工厂方法(Factory Method)

以前经常带老婆去优衣库(简单工厂)买衣服,就那么多款式,逛的次数多了,她就烦了。后来我改变策略,带老婆去逛商场(抽象工厂),商场里有各式品牌的店铺,不用我管,她自己就能逛上一整天。
区别于简单工厂,核心工厂类(商场)不再负责所有产品的创建,而是将具体创建的工作交给子类(服装店)去做,成为一个抽象工厂角色,仅负责给出具体工厂类必须实现的接口(门店),而不接触哪一个产品类应当被实例化这种细节。

(1)工厂方法模式结构

工厂方法模式包含如下角色:

  • Product:抽象产品

  • ConcreteProduct:具体产品

  • Factory:抽象工厂

  • ConcreteFactory:具体工厂

结构图:
image.png

时序图:
image.png

工厂模式总结

(1)适用场景

输出的产品是标准品,谁来做都可以。

(2)举例

常见的数据库连接工厂,SqlSessionFactory,产品是一个数据库连接,至于是oracle提供的,还是mysql提供的,我并不需要关心,因为都能让我通过sql来操作数据。

(3)注意事项

项目初期,软件结构和需求都没有稳定下来时,不建议使用此模式,因为其劣势也很明显,增加了代码的复杂度,增加了调用层次,增加了内存负担。所以要注意防止模式的滥用。

(4)简单实现

package FactoryMethod;
public class FactoryPattern
{
    public static void main(String[] args)
{
        Factory factory = new ConcreteFactoryA();
        Product product = factory.createProduct();
        product.use();
    }
}
//抽象产品:提供了产品的接口
interface Product
{
    public void use;
}
//具体产品A:实现抽象产品中的抽象方法
class ConcreteProductA implements Product
{
    public void use()
{
        System.out.println("具体产品A显示...");
    }
}
//具体产品B:实现抽象产品中的抽象方法
class ConcreteProductB implements Product
{
    public void use()
{
        System.out.println("具体产品B显示...");
    }
}
//抽象工厂:提供了厂品的生成方法
interface Factory
{
    public Product createProduct();
}
//具体工厂A:实现了厂品的生成方法
class ConcreteFactoryA implements AbstractFactory
{
    public Product createProduct()
{
        System.out.println("具体工厂A生成-->具体产品A.");
        return new ConcreteProductA();
    }
}
//具体工厂B:实现了厂品的生成方法
class ConcreteFactoryB implements AbstractFactory
{
    public Product createProduct()
{
        System.out.println("具体工厂B生成-->具体产品B.");
        return new ConcreteProductB();
    }
}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354

2 单例模式(Singleton)

韦小宝有7个老婆,但是每个都只有他这一个老公,他的所有老婆叫老公时,指的都是他,他就是一个单例。

单例模式结构

单例模式包含如下角色:

  • Singleton:单例

结构图:
image.png

时序图:

image.png

优缺点

  • 优点:全局只有一个实例,便于统一控制,同时减少了系统资源开销。

  • 缺点:没有抽象层,扩展困难。

应用场景

适合需要做全局统一控制的场景,例如:全局唯一的编码生成器。

注意事项

只对外提供公共的getInstance方法,不提供任何公共构造函数。

简单实现

public class Singleton
{
    private static volatile Singleton instance=null;    //保证 instance 在所有线程中同步
    private Singleton(){}    //private 避免类在外部被实例化
    public static synchronized Singleton getInstance()
{
        //getInstance 方法前加同步
        if(instance == null)
        {
            instance = new Singleton();
        }
        return instance;
    }
}1234567891011121314

3 装饰模式(Decorator)

大学毕业,想要送给室友一个有纪念意义的礼物,就找到一张大家的合照,在上面写上“永远的兄弟!”,然后拿去礼品店装了个相框,再包上礼盒。这里的我和礼品店都是装饰器,都没有改变照片本身,却都让照片变得更适合作为礼物送人。

装饰模式结构

装饰模式包含如下角色:

  • Component:抽象构件

  • ConcreteComponent:具体构件

  • Decorator:抽象装饰类

  • ConcreteDecorator:具体装饰类

结构图:

image.png

时序图:

image.png

优缺点

  • 优点:比继承更加灵活(继承是耦合度很大的静态关系),可以动态的为对象增加职责,可以通过使用不同的装饰器组合为对象扩展N个新功能,而不会影响到对象本身。

  • 缺点:当一个对象的装饰器过多时,会产生很多的装饰类小对象和装饰组合策略,增加系统复杂度,增加代码的阅读理解成本。

适用场景

  • 适合需要(通过配置,如:diamond)来动态增减对象功能的场景。

  • 适合一个对象需要N种功能排列组合的场景(如果用继承,会使子类数量爆炸式增长)

注意事项

  • 一个装饰类的接口必须与被装饰类的接口保持相同,对于客户端来说无论是装饰之前的对象还是装饰之后的对象都可以一致对待。

  • 尽量保持具体构件类Component作为一个“轻”类,也就是说不要把太多的逻辑和状态放在具体构件类中,可以通过装饰类。

简单实现

package decorator;
public class DecoratorPattern
{
    public static void main(String[] args)
{
        Component component = new ConcreteComponent();
        component.operation();
        System.out.println("---------------------------------");
        Component decorator = new ConcreteDecorator(component);
        decorator.operation();
    }
}
//抽象构件角色
interface  Component
{
    public void operation();
}
//具体构件角色
class ConcreteComponent implements Component
{
    public ConcreteComponent()
{
        System.out.println("创建具体构件角色");       
    }   
    public void operation()
{
        System.out.println("调用具体构件角色的方法operation()");           
    }
}
//抽象装饰角色
class Decorator implements Component
{
    private Component component;   
    public Decorator(Component component)
{
        this.component=component;
    }   
    public void operation()
{
        component.operation();
    }
}
//具体装饰角色
class ConcreteDecorator extends Decorator
{
    public ConcreteDecorator(Component component)
{
        super(component);
    }   
    public void operation()
{
        super.operation();
        addBehavior();
    }
    public void addBehavior()
{
        System.out.println("为具体构件角色增加额外的功能addBehavior()");           
    }
}1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859

4 策略模式(Strategy)

男生追妹子时,一般都会用到这种模式,常见的策略有这些:约会吃饭;看电影;看演唱会;逛街;去旅行……,虽然做的事情不同,但可以相互替换,唯一的目标都是捕获妹子的芳心。

策略模式结构

  • Context: 环境类

  • Strategy: 抽象策略类

  • ConcreteStrategy: 具体策略类

结构图:
image.png

时序图:

image.png

优缺点

  • 优点:策略模式提供了对“开闭原则”的完美支持,用户可以在不修改原有系统的基础上选择算法或行为。干掉复杂难看的if-else。

  • 缺点:调用时,必须提前知道都有哪些策略模式类,才能自行决定当前场景该使用何种策略。

试用场景

一个系统需要动态地在几种可替换算法中选择一种。不希望使用者关心算法细节,将具体算法封装进策略类中。

注意事项

一定要在策略类的注释中说明该策略的用途和适用场景。

简单实现

package strategy;
public class StrategyPattern
{
    public static void main(String[] args)
{
        Context context = new Context();
        Strategy strategyA = new ConcreteStrategyA();
        context.setStrategy(strategyA);
        context.algorithm();
        System.out.println("-----------------");
        Strategy strategyB = new ConcreteStrategyB();
        context.setStrategy(strategyB);
        context.algorithm();
    }
}
//抽象策略类
interface Strategy
{   
    public void algorithm();    //策略方法
}
//具体策略类A
class ConcreteStrategyA implements Strategy
{
    public void algorithm()
{
        System.out.println("具体策略A的策略方法被访问!");
    }
}
//具体策略类B
class ConcreteStrategyB implements Strategy
{
  public void algorithm()
{
      System.out.println("具体策略B的策略方法被访问!");
  }
}
//环境类
class Context
{
    private Strategy strategy;
    public Strategy getStrategy()
{
        return strategy;
    }
    public void setStrategy(Strategy strategy)
{
        this.strategy=strategy;
    }
    public void algorithm()
{
        strategy.algorithm();
    }
}1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253

5 代理模式(Proxy)

淘宝店客服总是会收到非常多的重复问题,例如:有没有现货?什么时候发货?发什么快递?大量回答重复性的问题太烦了,于是就出现了小蜜机器人,他来帮客服回答那些已知的问题,当碰到小蜜无法解答的问题时,才会转到人工客服。这里的小蜜机器人就是客服的代理。

代理模式结构

代理模式包含如下角色:

  • Subject: 抽象主题角色

  • Proxy: 代理主题角色

  • RealSubject: 真实主题角色

结构图:

image.png

时序图:

image.png

优缺点

  • 优点:代理可以协调调用方与被调用方,降低了系统的耦合度。根据代理类型和场景的不同,可以起到控制安全性、减小系统开销等作用。

  • 缺点:增加了一层代理处理,增加了系统的复杂度,同时可能会降低系统的相应速度。

试用场景

理论上可以代理任何对象,常见的代理模式有:

  • 远程(Remote)代理:为一个位于不同的地址空间的对象提供一个本地的代理对象,这个不同的地址空间可以是在同一台主机中,也可是在另一台主机中,远程代理又叫做大使(Ambassador)。

  • 虚拟(Virtual)代理:如果需要创建一个资源消耗较大的对象,先创建一个消耗相对较小的对象来表示,真实对象只在需要时才会被真正创建。

  • Copy-on-Write代理:它是虚拟代理的一种,把复制(克隆)操作延迟到只有在客户端真正需要时才执行。一般来说,对象的深克隆是一个开销较大的操作,Copy-on-Write代理可以让这个操作延迟,只有对象被用到的时候才被克隆。

  • 保护(Protect or Access)代理:控制对一个对象的访问,可以给不同的用户提供不同级别的使用权限。

  • 缓冲(Cache)代理:为某一个目标操作的结果提供临时的存储空间,以便多个客户端可以共享这些结果。

  • 防火墙(Firewall)代理:保护目标不让恶意用户接近。

  • 同步化(Synchronization)代理:使几个用户能够同时使用一个对象而没有冲突。

  • 智能引用(Smart Reference)代理:当一个对象被引用时,提供一些额外的操作,如将此对象被调用的次数记录下来等。

简单实现

package proxy;
public class ProxyPattern
{
    public static void main(String[] args)
{
        Proxy proxy = new Proxy();
        proxy.request();
    }
}
//抽象主题
interface Subject
{
    void request();
}
//真实主题
class RealSubject implements Subject
{
    public void request()
{
        System.out.println("访问真实主题方法...");
    }
}
//代理
class Proxy implements Subject
{
    private RealSubject realSubject;
    public void request()
{
        if (realSubject==null)
        {
            realSubject=new RealSubject();
        }
        preRequest();
        realSubject.request();
        afterRequest();
    }
    public void preRequest()
{
        System.out.println("访问真实主题之前的预处理。");
    }
    public void afterRequest()
{
        System.out.println("访问真实主题之后的后续处理。");
    }
}123456789101112131415161718192021222324252627282930313233343536373839404142434445

6 观察者模式(Observer)

出差在外,想了解孩子在家的情况,这时候只要加入“相亲相爱一家人”群,老爸老妈会经常把孩子的照片和视频发到群里,你要做的就是作为一个观察者,刷一刷群里的信息就能够了解一切了。

观察者模式结构

观察者模式包含如下角色:

  • Subject:目标

  • ConcreteSubject:具体目标

  • Observer:观察者

  • ConcreteObserver:具体观察者

结构图:

image.png

时序图:

image.png

优缺点

  • 优点:将复杂的串行处理逻辑变为单元化的独立处理逻辑,被观察者只是按照自己的逻辑发出消息,不用关心谁来消费消息,每个观察者只处理自己关心的内容。逻辑相互隔离带来简单清爽的代码结构。

  • 缺点:观察者较多时,可能会花费一定的开销来发消息,但这个消息可能仅一个观察者消费。

适用场景

适用于一对多的的业务场景,一个对象发生变更,会触发N个对象做相应处理的场景。例如:订单调度通知,任务状态变化等。

注意事项

避免观察者与被观察者之间形成循环依赖,可能会因此导致系统崩溃。

简单实现

package observer;
import java.util.*;
public class ObserverPattern
{
    public static void main(String[] args)
    {
        Subject subject = new ConcreteSubject();
        Observer obsA = new ConcreteObserverA();
        Observer obsb = new ConcreteObserverB();
        subject.add(obsA);
        subject.add(obsB);
        subject.setState(0);
    }
}
//抽象目标
abstract class Subject
{
    protected List<Observer> observerList = new ArrayList<Observer>();   
    //增加观察者方法
    public void add(Observer observer)
    {
        observers.add(observer);
    }    
    //删除观察者方法
    public void remove(Observer observer)
    {
        observers.remove(observer);
    }   
    public abstract void notify(); //通知观察者方法
}
//具体目标
class ConcreteSubject extends Subject
{
   private Integer state;
   public void setState(Integer state){
        this.state = state;

        // 状态改变通知观察者
        notify();
    }
    public void notify()
    {
        System.out.println("具体目标状态发生改变...");
        System.out.println("--------------");       

        for(Observer obs:observers)
        {
            obs.process();
        }

    }          
}
//抽象观察者
interface Observer
{
    void process(); //具体的处理
}
//具体观察者A
class ConcreteObserverA implements Observer
{
    public void process()
    {
        System.out.println("具体观察者A处理!");
    }
}
//具体观察者B
class ConcreteObserverB implements Observer
{
    public void process()
    {
        System.out.println("具体观察者B处理!");
    }
}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172

热门文章

暂无图片
编程学习 ·

【MPI编程实现梯形积分法】

MPI编程实现梯形积分法1.1问题描述1.2算法实现1.3结果实现与分析1.4源码实现 1.1问题描述 实现课件中的梯形积分法的MPI编程熟悉并掌握MPI编程方法,规模自行设定,可探讨不同规模对不同实现方式的影响。实验环境:联想笔记本,集成开发环境:Visual Studio 1.2算法实现 每个进…
暂无图片
编程学习 ·

超星尔雅《中庸》精读

超星学习通app《中庸》精读 答案 孟琢“四书”的形成(一)1【单选题】著作( )是一部论述了人生修养境界的道德哲学专著。 答案:《中庸》A、《周易》B、《史记》C、《诗经》D、《中庸》2【单选题】中国古代思想家( )是儒家学派的创始人,被后世尊称为万世师表。 答案:孔子A、孔…
暂无图片
编程学习 ·

Day 11 武装飞船

《外星人入侵》游戏要实现的是:玩家控制一艘出现在屏幕底部中央的飞船,可以使用箭头左右移动飞船,还可以使用空格来进行射击,游戏开始时候一群外星人出现在天空,他们在屏幕中向下移动,玩家的任务是射杀这些外星人,玩家将所有外星人都消灭干净后,会出现一群新的外星人,…
暂无图片
编程学习 ·

7-9 1.2.5 双重回文数 (70分)

如果一个数从左往右读和从右往左读都是一样,那么这个数就叫做“回文数”.例如,12321 就是一个回文数,而 77778 就不是. 当然,回文数的首和尾都应是非零的,因此 0220 就不是回文数. 事实上,有一些数(如 21),在十进制时不是回文数,但在其它进制(如二进制时为 10101)时就是 回…
暂无图片
编程学习 ·

DAY14 Javaweb Servlet、Response、Request

以下讲的都是最底层的内容,以后会被新的方法顶替掉一、Servlet,是sun公司开发的一门技术,如果要开发sevlet程序(网页java),只需要1、实现这个接口就可以 2、把开发好的java类部署到web服务器中。把实现了Servlet接口的Java程序叫做Servlet,一个请求地址对应一个servlet…
暂无图片
编程学习 ·

解决vue项目在IE中请求缓存的问题

IE中如果本次请求和上次请求一样,会优先使用缓存我碰到的问题是,我删除了某列的数据,需要重新刷新列表,但是删除成功以后重新请求IE优先使用了缓存解决方法就是在每个url上添加一个随机数,使得每次请求不一样,就不存在缓存问题了PS:垃圾IE
暂无图片
编程学习 ·

ITEST考试助手 --- 记一次我与ITEST的拉锯战

文章目录0x0 前言0x1 1.0版本 -- 解除限制我方进攻0x2 2.0版本 - 自动翻译与解析听力我方进攻ITEST方防御0x3 3.0版本 -- 解除切屏限制与添加翻译助手反制防御我方进攻ITEST防御0x4 4.0版本 - 全随机与ajax拦截反制防御我方进攻ITEST防御0x5 5.0版本 - 只读属性的胜利反制防御我…
暂无图片
编程学习 ·

培训网站比较-CSDN-51CTO-慕课网

本人是从事互联网行业,从码农到部门负责人,一路走来,最让我感受深刻的是,技术每天在更新迭代,自己一定要跟上脚步,不然很容易被淘汰。不管是作为技术人员还是部门管理者,技术能力必须得到重视。作为部门负责人,必须督促大家学习技术。我讲讲这几年在这方面的经历:一开…
暂无图片
编程学习 ·

POM 标签大全详解

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0http://maven.apache.org/maven-v4_0_0.xsd"><!--父项目的坐标。…
暂无图片
编程学习 ·

常用排序:冒泡排序与快速排序详解,看完这篇就够了!风马博客

常用排序:冒泡排序与快速排序详解。在排序算法中,冒泡排序和快速排序可以算是排序算法入门必会的两种排序了,今天和大家来分析一下如何快速理解并掌握这两种排序。首先冒泡排序是初学者最常用的排序,所以我们先来详解下冒泡排序。1.冒泡排序冒泡排序,看字面意义就是有大泡泡…
暂无图片
编程学习 ·

MySql简单入门_第四篇(2)_存储

5、存储过程:为以后的使用而保存的一条或多条MySql语句的集合存储过程(Stored Procedure)是一种在数据库中存储复杂程序,以便外部程序调用的一种数据库对象。存储过程是为了完成特定功能的SQL语句集,经编译创建并保存在数据库中,用户可通过指定存储过程的名字并给定参数(…
暂无图片
编程学习 ·

JAVA学习之路(3) request的总结

文章目录引言Request对请求行数据的操作Request对请求头数据的操作Request对请求头数据的操作通用方式中文乱码问题请求转发 引言 在httpServlet类中,我们只需要调用doGet和doPost方法即可以实现对应的功能。对应这两个方法,有两个穿进去的参数对象,一个是response,一个是r…
暂无图片
编程学习 ·

Java解决问题_7

java解决关于“完数”的问题 问题描述: 一个正整数的因子是所有可以整除它的正整数。而一个数如果恰好等于除它本身外的因子之和,这个数就称为完数。例如6=1+2+3(6的因子是1,2,3)。 现在,你要写一个程序,读入两个正整数n和m(1<=n<m<1000),输出[n,m]范围内所有…
暂无图片
编程学习 ·

[TypeScript] - TypeScript官方文档学习笔记-接口-上(二)

前言 接口只是在语法层面限制写法,从而使部分语句写法不出现,本质是语法规范 接口 TypeScript中接口用来定义结构类型,出于类型检查需要 编译转换后接口消失,仅用于语法检查 普通对象传入: function printLabel(labeledObj: { label: string }) {console.log(labeledObj.l…
暂无图片
编程学习 ·

esp8266 system_partition_table_regist fail 蓝灯闪一下就灭

在添加了如下函数后,蓝灯闪一下就灭,并且串口打印system_partition_table_regist fail: void ICACHE_FLASH_ATTR user_pre_init(void) {if(!system_partition_table_regist(at_partition_table, sizeof(at_partition_table)/sizeof(at_partition_table[0]),SPI_FLASH_SIZE_M…
暂无图片
编程学习 ·

城轨交通系统中智能化管理的作用分析

3D可视化集成系统是根据虚拟现实技术的情景在软件系统中,展现数据信息,把平时的软件业务流程在虚拟场景中完成。计划方案可运用于地铁站、轻轨站、单轨、有轨电车、磁浮等这种轨道交通系统的管理方法上。 轨道交通系统中,地铁已经中国成为大城市标准配置的交通工具,到目前为…
暂无图片
编程学习 ·

吃一堑长一智

序号 问题 原因 解决方法1 同Service内部调用this.xxx()会使AOP失效 AOP是基于代理的,调用this.xxx()不会经过代理创建的类 使用applicationContext.getBean(XXX.class)获取自身的代理类再调用即可23
暂无图片
编程学习 ·

Python电影票房数据可视化分析基础实践

数据可视化一直是很多数据分析或者是建模挖掘任务里面经常会用到的一项功能,今天我们基于某电影网站中公开发布的电影票房数据进行一些基础的数据可视化分析实践,下面是部分的数据样例:叶问.,20160304,33151,2193,196.9万,33.96%,46 捉妖记,20150718,17860,995,192.71万,64.…