【烈日炎炎战后端】设计模式(1.1万字)

设计模式

        • 1. 你知道那些常用的设计模式?
        • 2. 设计模式的六大原则?
        • 3. 如何理解设计模式的六大原则?
        • 4. 设计模式的分类
        • 5. 请手写一下单例模式?

1. 你知道那些常用的设计模式?

1. 单例模式:单例模式保证系统内存中该类只有一个对象。

优势:可以防止频繁的创建和销毁对象造成的资源浪费,以此来提高系统性能。

实现方式:懒汉模式创建和饿汉模式创建。

应用场景:我们JDK中,java.lang.Runtime就是百科全书的单例模式(饿汉式),spring里面就是用了单例模式

2.工厂模式:将实例化对象的代码提取出来,放到一个类里面统一管理,由子类决定是否实例化。

link

优势:工厂方法模式具有很好的封装性。客户端不需要知道创建对象的过程,只需要知道要创建的是哪个具体的产品即可。

实现方式:简单工厂模式,工厂方法模式,抽象工厂模式。

简单工厂模式:将被使用方的创建过程封装到一个类中,这样就在使用方和被使用方之间做了一个缓冲,也就是使用方和被使用方做了一个解耦,提高了软件的可扩展性和可维护性和可复用性.

工厂方法模式:将简单工厂的创建对象的方法写成一个抽象方法,也就是在工厂类或者接口中不知道如何创建该对象,创建具体对象交给工厂类的子类或者实现类去做.

抽象工厂模式:抽象工厂模式为创建一组相关或相互依赖的对象提供一个接口,而且无须指定它们的具体类。抽象工厂模式是工厂方法模式的升级版本,在有多个业务品种、业务分类时,通过抽象工厂模式产生需要的对象是一种非常好的解决方式。,抽象工厂方法不再是单独的接口,而是生产一类产品的接口,可以生产多个相关联产品。

抽象工厂模式和工厂方法模式的区别

如果产品单一,适合使用工厂模式。但是如果有多个业务品种、业务分类时,需要使用抽象工厂模式。也就是说,工厂模式针对的是一个产品等级结构 ,抽象工厂模式针对的是面向多个产品等级结构的。

3. 代理模式:将原类进行封装,客户端不能直接找到原类,必须通过代理角色。即代理是原类的一个替身,客户端要找原类,必须找代理才可以搞定。明星和经纪人的关系就是一种代理模式。

优势:为一个对象提供一个替身,以控制对这个对象的访问.即通过代理对象访问目标对象.这样做的好处 是:可以在目标对象实现的基础上,增强额外的功能操作,即扩展目标对象的功能.

实现方式:静态代理和动态代理。

应用场景:动态代理在实现阶段不用关心代理谁,而在运行阶段才指定代理哪一个对象。相对来说,自己写代理类的方式就是静态代理。

4. 装饰器模式

优势:动态的将新功能附加到对象上.在对象功能扩展方面,它比继承更有弹性,装饰者模式也体现了 开闭原则(ocp)

实现方式:静态代理和动态代理。

应用场景:String 常量池,数据库连接池

5. 享元模式:系统中有大量对象,这些对象消耗大量内存,并且对象的状态大部分可以外部化时,我们就可以考虑选用享元模式。在享元模式这样理解,“享”就表示共享,“元”表示对象

优势:享元模式大大减少了对象的创建,降低了程序内存的占用,提高效率。

实现方式:静态代理和动态代理。

应用场景:String 常量池,数据库连接池

6. 模板模式:定义一个操作中的算法的骨架,而将一些步骤延迟到子类中,使得子类可以不改变一 个算法的结构,就可以重定义该算法的某些特定步骤。

优势:实现了最大化代码复用。

实现方式:它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行.

应用场景:当要完成在某个过程,该过程要执行一系列步骤 ,这一系列的步骤基本相同,但其 个别步骤在实现时 可能不同,通常考虑用模板方法模式来处理

7. 策略模式:定义算法族(策略组),分别封装起来,让他们之间可以互相替换,此模式 让算法的变化独立于使用算法的客户

优势:体现了“对修改关闭,对扩展开放”原则,客户端增加行为不用修改原有代码,只要添加一种策略(或者行为) 即可,避免了使用多重转移语句

实现方式:策略模式将算法封装在独立的 Strategy 类中使得你可以独立于其 Context 改 变它,使它易于切换,易于理解,易于扩展

应用场景: JDK的数组的比较器就使用了策略模式

8. 原型模式:用原型实例指定创建对象的种类,并且通过拷贝这些原型,创建新的对象

优势:允许一个对象再创建另外一个可定制的对象,无需知道如何创建的细节

实现方式:通过将一个原型对象传给那个要发动创建的对象,这个要发动创建的对象通过请求原型对象拷贝它 们自己来实施创建,即 对象.clone()

应用场景:Spring 中原型 bean 的创建,就是原型模式的应用

2. 设计模式的六大原则?

https://www.jianshu.com/p/807bc228dbc2

https://www.cnblogs.com/pony1223/p/7594803.html

设计模式(Design pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。设计模式使代码编制真正工程化,设计模式是软件工程的基石,如同大厦的一块块砖石一样。只有精通了设计模式,才敢说真正理解了软件工程。可以说,设计模式是每一个架构师所必备的技能之一。想要精通设计模式,必须要先搞清楚设计模式的六大原则。

学习设计模式可以降低对象之间的耦合,增加程序的可复用性、可扩展性、可维护性。优秀的设计模式就是基于这些原则去实现的。

1、单一职责原则(Single Responsibility Principle,简称SRP )

  • 核心思想:应该有且仅有一个原因引起类的变更
  • 问题描述:假如有类Class1完成职责T1,T2,当职责T1或T2有变更需要修改时,有可能影响到该类的另外一个职责正常工作。
  • 好处:类的复杂度降低、可读性提高、可维护性提高、扩展性提高、降低了变更引起的风险。
  • 需注意:单一职责原则提出了一个编写程序的标准,用“职责”或“变化原因”来衡量接口或类设计得是否优良,但是“职责”和“变化原因”都是不可以度量的,因项目和环境而异。

2、里氏替换原则(Liskov Substitution Principle,简称LSP)

  • 核心思想:在使用基类的的地方可以任意使用其子类,能保证子类完美替换基类。
  • 通俗来讲:只要父类能出现的地方子类就能出现。反之,父类则未必能胜任。
  • 好处:增强程序的健壮性,即使增加了子类,原有的子类还可以继续运行。
  • 需注意:如果子类不能完整地实现父类的方法,或者父类的某些方法在子类中已经发生“畸变”,则建议断开父子继承关系 采用依赖、聚合、组合等关系代替继承。

3、依赖倒置原则(Dependence Inversion Principle,简称DIP)

  • 核心思想:高层模块不应该依赖底层模块,二者都该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象;
  • 说明:高层模块就是调用端,低层模块就是具体实现类。抽象就是指接口或抽象类。细节就是实现类。
  • 通俗来讲:依赖倒置原则的本质就是通过抽象(接口或抽象类)使个各类或模块的实现彼此独立,互不影响,实现模块间的松耦合。
  • 问题描述:类A直接依赖类B,假如要将类A改为依赖类C,则必须通过修改类A的代码来达成。这种场景下,类A一般是高层模块,负责复杂的业务逻辑;类B和类C是低层模块,负责基本的原子操作;假如修改类A,会给程序带来不必要的风险。
  • 解决方案:将类A修改为依赖接口interface,类B和类C各自实现接口interface,类A通过接口interface间接与类B或者类C发生联系,则会大大降低修改类A的几率。
  • 好处:依赖倒置的好处在小型项目中很难体现出来。但在大中型项目中可以减少需求变化引起的工作量。使并行开发更友好。

4、接口隔离原则(Interface Segregation Principle,简称ISP)

  • 核心思想:类间的依赖关系应该建立在最小的接口上
  • 通俗来讲:我觉得这里的隔离翻译为分离是更确切的。建立单一接口,不要建立庞大臃肿的接口,尽量细化接口,接口中的方法尽量少。也就是说,我们要为各个类建立专用的接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。
  • 问题描述:类A通过接口interface依赖类B,类C通过接口interface依赖类D,如果接口interface对于类A和类B来说不是最小接口,则类B和类D必须去实现他们不需要的方法。
  • 需注意
  • 接口尽量小,但是要有限度。对接口进行细化可以提高程序设计灵活性,但是如果过小,则会造成接口数量过多,使设计复杂化。所以一定要适度
  • 提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情
  • 为依赖接口的类定制服务。只暴露给调用的类它需要的方法,它不需要的方法则隐藏起来。只有专注地为一个模块提供定制服务,才能建立最小的依赖关系。

5、迪米特法则(Law of Demeter,简称LoD)

  • 核心思想:类间解耦。
  • 通俗来讲: 一个类对自己依赖的类知道的越少越好。自从我们接触编程开始,就知道了软件编程的总的原则:低耦合,高内聚。无论是面向过程编程还是面向对象编程,只有使各个模块之间的耦合尽量的低,才能提高代码的复用率。低耦合的优点不言而喻,但是怎么样编程才能做到低耦合呢?那正是迪米特法则要去完成的。

6、开放封闭原则(Open Close Principle,简称OCP)

  • 核心思想:尽量通过扩展软件实体来解决需求变化,而不是通过修改已有的代码来完成变化
  • 通俗来讲: 一个软件产品在生命周期内,都会发生变化,既然变化是一个既定的事实,我们就应该在设计的时候尽量适应这些变化,以提高项目的稳定性和灵活性。

3. 如何理解设计模式的六大原则?

我们知道,java中有类,父类,抽象类,接口这些概念,而设计模式的六大原则就解释了它们及其它们之间的关系是怎样的,接下来我们将由简到难一一剖析。

  • 开闭原则:是总纲,他告诉我们要对扩展开放,对修改关闭。即通过开闭原则,我们可以通过扩展行为来实现新的功能,而不是通过修改已有的代码。开闭原则可以帮助我们构建一个稳定,灵活的软件系统。

  • 单一职责原则:面向对象最基本的概念就是类,此告诉我们实现要职责单一。即每个类应该只负责一项规范,这是为在部分职责发生变化时,牵连到其他职责,是为了解耦合。

  • 迪米特法则:告诉我们类与类之间要要降低耦合,也叫最少知识原则,是指一个对象应该对其依赖的对象有最少的了解。一个类对于其他类知道的越少越好,就是说一个对象应当对其他对象有尽可能少的了解,只和朋友通信,不和陌生人说话。

  • 里氏替换原则:说完了类,就不得不提到子类和父类,如何保证子类和父类?而里氏替换原则规范。即子类可以替换父类,告诉我们只要父类能出现的地方子类就能出现。它体现了java三大特点中的继承,子类的权限不能低于父类。

  • 接口隔离原则:告诉我们在设计接口的时候要精简单一,通俗的说要建立单一接口,不要建立庞大臃肿的接口,尽量细化接口,接口中的方法尽量少。

  • 依赖倒置原则:对接口和抽象类的功能做了规范,通常情况的设计都是高层模块依赖于低层模块(比如说在盖楼过程中,需要打好地基,才可以修建好上层建筑)。这样看似顺理成章,低层模块不怎么变动,高层模块调用低层模块。但是事实上,事物总是在变化,经常低层模块变化,引起高层一系列的变化。即底层依赖于高层。细节应该依赖于抽象,因为抽象的东西要稳定的多。接口中制定好规范就好,不需要事先具体操作,即面向接口编程

4. 设计模式的分类

设计模式一般分为三类:创建型模式、结构型模式、行为型模式。

创建型模式

创建型模式简单来说就是用来创建对象的。一共有五种:单例模式、建造者模式、工厂方法模式、抽象工厂模式、原型模式。
单例模式 :确保某一个类只有一个实例,并且提供一个全局访问点。
建造者模式 : 用来创建复杂的复合对象。
工厂方法模式 :让子类来决定要创建哪个对象。
抽象工厂模式 :创建多个产品族中的产品对象。
原型模式 :通过复制原型来创建新对象。

行为型模式

行为型模式主要是描述类或者对象是怎样交互和怎样分配职责的。一共有十一种:策略模式、模板方法模式、观察者模式、迭代器模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。
策略模式 :封装不同的算法,算法之间能互相替换。
状态模式 :根据不同的状态做出不同的行为。
责任链模式 :将事件沿着链去处理。
观察者模式 :状态发生改变时通知观察者,一对多的关系。
模板方法模式 :定义一套流程模板,根据需要实现模板中的操作。
迭代器模式 :提供一种方法顺序访问一个聚合对象中的各个元素。
备忘录模式 :保存对象的状态,在需要时进行恢复。
访问者模式 :稳定数据结构中,定义新的操作行为。
中介者模式 :将网状结构转变为星型结构,所有行为都通过中介。
解释器模式 :定义语法,并对其进行解释。
命令模式 :将请求封装成命令,并记录下来,能够撤销与重做。

结构型模式

结构型模式主要是用于处理类或者对象的组合。一共有七种:适配器模式、装饰模式、代理模式、外观模式、桥接模式、组合模式、享元模式。
代理模式 :控制客户端对对象的访问。
组合模式 :将整体与局部(树形结构)进行递归组合,让客户端能够以一种的方式对其进行处理。
适配器模式 :将原来不兼容的两个类融合在一起。
装饰者模式 :为对象添加新功能。
享元模式 :使用对象池来减少重复对象的创建。
外观模式 :对外提供一个统一的接口用来访问子系统。
桥接模式 :将两个能够独立变化的部分分离开来。。

5. 请手写一下单例模式?

创建单例类的步骤:
1、私有化该类的构造函数。
2、通过new在本类中创建一个本类对象。
3、提供一个公有的静态方法,将创建的对象返回。
单例类因为不允许其他程序用new来创建该类对象,所以只能将单例类中的方法定义成静态的(随类的加载而加载),静态方法不能访问非静态的成员,故只能> > 将该类中new的本类对象变成静态的。

https://www.cnblogs.com/william-dai/p/10938666.html

单例模式懒汉式和饿汉式区别?

  1. 饿汉式天生就是线程安全的,可以直接用于多线程而不会出现问题,懒汉式本身是非线程安全的,为了实现线程安全可以加synchronized锁。
  2. 资源加载和性能:饿汉式在类创建的同时就实例化一个静态对象出来,不管之后会不会使用这个单例,都会占据一定的内存,但是相应的,在第一次调用时速度也会更快,因为其资源已经初始化完成。而懒汉式顾名思义,会延迟加载,在第一次使用该单例的时候才会实例化对象出来,第一次调用时要做初始化,如果要做的工作比较多,性能上会有些延迟,之后就和饿汉式一样了。

原文链接:https://blog.csdn.net/qq_35098526/article/details/79893628

1. 懒汉式:用到时再去创建

//非线程安全的单例模式
public class Singleton {
    //懒汉式单例,只有在调用getInstance时才会实例化一个单例对象
    public static Singleton singleton; 
    private Singleton(){}
    public static Singleton getInstance(){
    if(user==null){       //step 1.
    singleton = new Singleton();  //step 2
    }
    return singleton;
    }
}

看上去,这段代码没什么明显问题,但它不是线程安全的。假设当前有N个线程同时调用getInstance()方法,由于当前还没有对象生成,所以一部分同时都进入step 2,

那么就会由多个线程创建多个多个user对象。

解决办法:使用synchronized关键字。经改造上面代码展示如下:

//实现双重校验锁的单例模式【推荐】
class  Singleton{
    //volatile保证禁止指令重排序,防止另外一个线程返回没有初始化的对象.
    private volatile  static Singleton instance;
    //设置类的构造为私有,防止被其他类创建
    private  Singleton(){}
    public static  Singleton instance (){
        //第一层屏障,为了防止多次加锁,而造成的性能损耗
        if(instance==null){
            //step2
            //加锁防止多线程
            synchronized(Singleton.class){
                //第二层屏障,为了防止已经突破了第一层屏障位于step2位置的线程,重现创建对象
                if(instance==null){
                    instance=new Singleton();
            }
        }
        return instance;
    }

第一次校验:也就是第一个if(uniqueInstance==null),这个是为了代码提高代码执行效率,由于单例模式只要一次创建实例即可,所以当创建了一个实例之后,再次调用getUniqueInstance方法就不必要进入同步代码块,不用竞争锁。直接返回前面创建的实例即可。说白了假设第一次不检验,看似问题也不大,但是其实这里所用到的思想就如我们在学习hashmap时为什么需要先比较hashcode再比较equals方法,就一句话谁快选谁,这里看似多判断了一次,然而synchronzied同步锁会大大削减效率,开销很大,所以我们就任性地先比较一次,这样如果运气好的话可以通过if语句,跳过synchronized这个步骤。

第二次校验:也就是第二个if(uniqueInstancenull),这个校验是防止二次创建实例,假如有一种情况,当uniqueInstance还未被创建时,线程t1调用getUniqueInstance方法,由于第一次判断if(uniqueInstancenull),此时线程t1准备继续执行,但是由于资源被线程t2抢占了,此时t2页调用getUniqueInstance方法,同样的,由于singleton并没有实例化,t2同样可以通过第一个if,然后继续往下执行,同步代码块,第二个if也通过,然后t2线程创建了一个实例singleton。此时t2线程完成任务,资源又回到t1线程,t1此时也进入同步代码块,如果没有这个第二个if,那么,t1就也会创建一个singleton实例,那么,就会出现创建多个实例的情况,但是加上第二个if,就可以完全避免这个多线程导致多次创建实例的问题。
原文链接:https://blog.csdn.net/weixin_43914278/article/details/104451055

但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。

volatile:双重检查基础版中由重排序引发的问题

  • 做第一次检查的时候,如果lazyDoubleCheckSingleton != null,并不代表lazyDoubleCheckSingleton一定已经初始化完成了,造成这种情形的原因是指令重排序;
  • lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton(); 这句在底层经历了3个动作:
    1.分配内存给这个对象;
    2.初始化对象;
    3.设置 lazyDoubleCheckSingleton 指向刚分配的内存;
    这3个动作中,2和3的动作可能颠倒,其造成的结果就是:Thread-0第一次检查的时候,由于Thread-1先执行3,lazyDoubleCheckSingleton 指向刚分配的内存,导致Thread-0看到的 lazyDoubleCheckSingleton 不为空,直接返回 lazyDoubleCheckSingleton,但此时lazyDoubleCheckSingleton 在Thread-1中还没有初始化,所以造成程序出问题;
  • Java规范中有个 intra-thread semantics 的规定,它保证重排序不会改变单线程的执行结果,重排序可以提高程序的执行性能;

重排序问题的解决方案

  • 不允许重排序;
  • 允许重排序,但不允许另一个线程看到这个重排序;
    链接:https://www.jianshu.com/p/b4157c2e8fe5

不允许重排序的解决方案

  • 用 volatile 修饰 lazyDoubleCheckSingleton,就禁止了重排序;
  • 在多线程的时候,多CPU会共享内存,加了 volatile 后,所有的线程都能看到共享内存的最新状态,保证了内存的可见性;
  • 用 volatile 修饰的共享变量在进行写操作的时候,会将当前CPU缓存行的数据写进内存,使得其他CPU缓存了该内存地址的数据无效,从而迫使其他CPU重新从共享内存中获取数据,这样就保证了内存的可见性;
               //类的加载过程:
                                 //1.加载二进制数据到内存中,生成对应的Class数据结构
                                 //2.连接阶段:验证,准备(给类的静态变量赋默认),解析
                                 //3.初始化(给类的静态变量赋初值)
                                 //字节码层面(JIT,CPU对指令都会重新排序)
                                 // 正常       重序后
                                 //1.分配空间        1.分配空间
                                 //2.初始化(真值)    2. 引用赋值
                                 //3.引用赋值        3.初始化(真值)
                                 //由于s是静态变量,第二步和第三步可能引发问题
                             }

他最明显的原因是,初始化Helper对象的写操作和对Helper字段的写入不能正常工作。因此,调用getHelper()的线程可以看到对Helper对象的非空引用,但是可以看到Helper对象字段的默认值,而不是构造函数中设置的值。如果编译器内联了对构造函数的调用,那么如果编译器能够证明构造函数不能抛出异常或执行同步,那么初始化对象的写操作和写到助手字段的写入可以自由地重新排序。即使编译器没有重新排序这些写入,在多处理器上,处理器或内存系统也可以重新排序这些写入,就像运行在另一个处理器上的线程所看到的那样。

在给helper对象初始化的过程中,jvm做了下面3件事:

1.给helper对象分配内存

2.调用构造函数

3.将helper对象指向分配的内存空间

由于jvm的"优化",指令2和指令3的执行顺序是不一定的,当执行完指定3后,此时的helper对象就已经不在是null的了,但此时指令2不一定已经被执行。

假设线程1和线程2同时调用getHelper()方法,此时线程1执行完指令1和指令3,线程2抢到了执行权,此时helper对象是非空的。

所以线程2拿到了一个尚未初始化的helper对象,此时线程2调用这个helper就会抛出异常
————————————————
版权声明:本文为CSDN博主「Null_RuzZ」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/null_ruzz/article/details/72530826

2. 饿汉式:初始化时即创建,用到时直接返回

//天生的线程安全
public class Singleton {
   //1.私有化构造器
   private Singleton(){};
   //2.在类中创建一个类的实例,私有化,静态的
   private static  Singleton instance   = new Singleton();
  // 3.通过公共方法调用,此公共方法只能类调用,因为设置了 static
   public static Singleton getInstance(){
       return instance;
   }
}
  1. 静态内部类【推荐】
public class Singleton {
   private static class SingletonHolder{
       private static final Singleton INSTTANCE = new Singleton();
   }

   private Singleton(){};

   public static final Singleton getInstance(){
       return SingletonHolder.INSTTANCE;
   }
}

你可能感兴趣的:(Java后端)