设计模式内容介绍(设计模式是解决软件设计中存在反复出现的问题,提出的解决方案,实用了设计模式具有很好的可扩展性,实用了设计模式,设计模式让我们的软件有很好的维护性)

设计原则的核心思想:(1.找出应用中可能需要变化的地方,独立出来。2.针对接口编程,不针对实现编程。3.为了交互对象之间的松耦合)


设计模式概述(设计模式体现了代码的松耦合:在代码中,类与类之间的依赖关系简单清晰。高内聚:就是指相近的功能应该放到同一个类中)
设计模式总共有 23 种,
通俗的说是有六大原则
image.png
设计模式的七大原则

  1. 单一职责原则(Single Responsibility Principle,SRP):定义:不要存在多于一个导致类变更的原因。通俗的说,即一个类只负责一项职责。
  2. 开闭原则(Open Closed Principle,OCP):对扩展开放,对修改关闭
  3. 里氏代换原则(Liskov Substitution Principle,LSP):任何基类可以出现的地方,子类一定可以出现,基类不能改变父类方法的具体实现
  4. 依赖倒转原则(Dependency Inversion Principle,DIP):当有新需求的时候,我们只需要在底层扩展新的具体实现,而不需要对高层做任何改动,工厂模式就用到了该原则;
  5. 迪米特法则(Law ofDemeter,LOD)又叫最小知道原则(least knowledge principle,LKP):一个实体应当尽量少地与其他实体之间发生相互作用,使得系统功能模块相对独立。
  6. 该原则同时也体现了面向对象的最重要原则看,数据与行为应该封装在一起,最重要的是,我们根本不需要知道这些数据,我们只关心结果,处理这些数据的行为应该在数据原始的类中就,被封装好
  7. 接口隔离原则(Interface Segregation Principle,ISP):一个类对另外一个类的依赖性应当是建立在最小的接口上的。,使用多个隔离的接口,比使用单个接口要好,降低类之间的耦合度
  8. 合成复用原则(多组合少继承)(Composite Reuse Principle):在一个新的对象里通过关联关系(组合关系、聚合关系)来使用一些已有的对象,使之成为新对象的一部分;新对象通过委派调用已有对象的方法达到复用功能的目的。简而言之,尽量使用 组合/聚合 的方式,而不是使用继承。 | 设计模式原则名称 | 简单定义 | | —- | —- | | 开闭原则 | 对扩展开放,对修改关闭 | | 单一职责原则 | 一个类只负责一个功能领域中的相应职责 | | 里氏代换原则 | 所有引用基类的地方必须能透明地使用其子类的对象 | | 依赖倒置原则 | 依赖于抽象,不能依赖于具体实现 | | 接口隔离原则 | 类之间的依赖关系应该建立在最小的接口上 | | 合成/聚合复用原则 | 尽量使用合成/聚合,而不是通过继承达到复用的目的 | | 迪米特法则 | 一个软件实体应当尽可能少的与其他实体发生相互作用 |

1. 单一职责原则(SRP)
定义:就一个类而言,应该仅有一个引起它变化的原因。
从这句定义我们很难理解它的含义,通俗讲就是我们不要让一个类承担过多的职责。如果一个类承担的职责过多,就等于把这些职责耦合在一起,一个职责的变化可能会削弱或者抑制这个类完成其他职责的能力。这种耦合会导致脆弱的设计,当变化发生时,设计会遭受到破坏。
比如我经常看到一些Android开发在Activity中写Bean文件,网络数据处理,如果有列表的话Adapter 也写在Activity中,问他们为什么除了好找也没啥理由了,把他们拆分到其他类岂不是更好找,如果Activity过于臃肿行数过多,显然不是好事,如果我们要修改Bean文件,网络处理和Adapter都需要上这个Activity来修改,就会导致引起这个Activity变化的原因太多,我们在版本维护时也会比较头疼。也就严重违背了定义“就一个类而言,应该仅有一个引起它变化的原因”。
当然如果想争论的话,这个模式是可以引起很多争论的,但请记住一点,你写代码不只是为了你也是为了其他人。


2. 开放封闭原则(OCP):对扩展开放,对修改关闭
定义:类、模块、函数等等等应该是可以拓展的,但是不可修改。
开放封闭有两个含义,一个是对于拓展是开放的,另一个是对于修改是封闭的。对于开发来说需求肯定是要变化的,但是新需求一来,我们就要把类重新改一遍这显然是令人头疼的,所以我们设计程序时面对需求的改变要尽可能的保证相对的稳定,尽量用新代码实现拓展来修改需求,而不是通过修改原有的代码来实现。
假设我们要实现一个列表,一开始只有查询的功能,如果产品又要增加添加功能,过几天又要增加删除功能,大多数人的做法是写个方法然后通过传入不同的值来控制方法来实现不同的功能,但是如果又要新增功能我们还得修改我们的方法。用开发封闭原则解决就是增加一个抽象的功能类,让增加和删除和查询的作为这个抽象功能类的子类,这样如果我们再添加功能,你会发现我们不需要修改原有的类,只需要添加一个功能类的子类实现功能类的方法就可以了。

开闭原则:前面学的各种原则,最终是为了实现开闭原则的效果

开闭原则理解:一个软件的实体,类,函数,应该达到对扩展开放(对提供方来说阔开开放),对修改关闭(针对使用方修改是关闭的):通俗的说是:当有一个功能扩展了,增加了一个功能,原先使用的功能不会影响,意思就是,新增任何功能模块尽量不要对原有的功能有影响
在具体项目中:如果需要增加一个功能,代码做到尽量增加,而不是修改以前的代码

开闭原则 优点 :好理解,容易操作。
开闭原则 缺点: 违反了设计模式的cop原则,对扩展开放(提供方),对修改关闭(使用方修改关闭)


3.里氏替换原则(LSP)
定义:所有引用基类(父类)的地方必须能透明地使用其子类的对象
里氏代换原则告诉我们,在软件中将一个基类对象替换成它的子类对象,程序将不会产生任何错误和异常,反过来则不成立,如果一个软件实体使用的是一个子类对象的话,那么它不一定能够使用基类对象。
里氏代换原则是实现开闭原则的重要方式之一,由于使用基类对象的地方都可以使用子类对象,因此在程序中尽量使用基类类型来对对象进行定义,而在运行时再确定其子类类型,用子类对象来替换父类对象。
在使用里氏代换原则时需要注意如下几个问题:

子类的所有方法必须在父类中声明,或子类必须实现父类中声明的所有方法。根据里氏代换原则,为了保证系统的扩展性,在程序中通常使用父类来进行定义,如果一个方法只存在子类中,在父类中不提供相应的声明,则无法在以父类定义的对象中使用该方法。
我们在运用里氏代换原则时,尽量把父类设计为抽象类或者接口,让子类继承父类或实现父接口,并实现在父类中声明的方法,运行时,子类实例替换父类实例,我们可以很方便地扩展系统的功能,同时无须修改原有子类的代码,增加新的功能可以通过增加一个新的子类来实现。里氏代换原则是开闭原则的具体实现手段之一。
Java语言中,在编译阶段,Java编译器会检查一个程序是否符合里氏代换原则,这是一个与实现无关的、纯语法意义上的检查,但Java编译器的检查是有局限的。

里氏替换原则:简单说就是(子类可以扩展父类的功能,但是不能改变父类原有的功能)

子类可以实现父类的抽象方法,在编程中如何正确的实用继承,(里氏替换原则来标准化继承)
1.里氏替换原则需要达到的效果:所有引用基类的地方必须能透明的使用其子类的对象
2.在使用继承时,一定要遵循里氏替换原则:在子类中尽量不要重写父类的方法
3.如果需要改写父类的方法:可以通过聚合,组合,依赖,来解决问题
案例
Java设计模式 算法 - 图2


4.依赖倒置原则(DIP)
定义:高层模块不应该依赖低层模块,两个都应该依赖于抽象。抽象不应该依赖于细节,细节应该依赖于抽象。
在Java中,抽象就是指接口或者抽象类,两者都是不能直接被实例化的;细节就是实现类,实现接口或者继承抽象类而产生的就是细节,也就是可以加上一个关键字new产生的对象。高层模块就是调用端,低层模块就是具体实现类。
依赖倒置原则在Java中的表现就是:模块间通过抽象发生,实现类之间不发生直接依赖关系,其依赖关系是通过接口或者抽象类产生的。如果类与类直接依赖细节,那么就会直接耦合,那么当修改时,就会同时修改依赖者代码,这样限制了可扩展性。

依赖倒置(DIP):意思是设计代码结构时,高层模块不应该依赖底层模块,
二者都应该依赖其抽象(依赖导致的三种实现方式:接口传递,构造方法传递,setter方式传递),把子类产生的一个实例交给父类的应用:好处是

  1. //依赖倒置
  2. public class Wmyskxz {
  3. public void studyJavaCourse() {
  4. System.out.println("「3」同学正在学习「Java」课程");
  5. }
  6. public void studyDesignPatternCourse() {
  7. System.out.println("「3」同学正在学习「设计模式」课程");
  8. }
  9. }
  10. //模拟调用
  11. public static void main(String[] args) {
  12. Wmyskxz wmyskxz = new Wmyskxz();
  13. wmyskxz.studyJavaCourse();
  14. wmyskxz.studyDesignPatternCourse();
  15. }

5.迪米特原则(LOD):又叫做最少知道原则,意思是一个对象对其他的对象尽可能的少了解其内容实现,简单的说陌生的类最好不要已局部变量的方式出现在类的内部(简称:要求降低而不是完全避免)
定义:一个软件实体应当尽可能少地与其他实体发生相互作用。
也称为最少知识原则。如果一个系统符合迪米特法则,那么当其中某一个模块发生修改时,就会尽量少地影响其他模块,扩展会相对容易,这是对软件实体之间通信的限制,迪米特法则要求限制软件实体之间通信的宽度和深度。迪米特法则可降低系统的耦合度,使类与类之间保持松散的耦合关系。
迪米特法则要求我们在设计系统时,应该尽量减少对象之间的交互,如果两个对象之间不必彼此直接通信,那么这两个对象就不应当发生任何直接的相互作用,如果其中的一个对象需要调用另一个对象的某一个方法的话,可以通过第三者转发这个调用。简言之,就是通过引入一个合理的第三者来降低现有对象之间的耦合度。
在将迪米特法则运用到系统设计中时,要注意下面的几点:在类的划分上,应当尽量创建松耦合的类,类之间的耦合度越低,就越有利于复用,一个处在松耦合中的类一旦被修改,不会对关联的类造成太大波及;在类的结构设计上,每一个类都应当尽量降低其成员变量和成员函数的访问权限;在类的设计上,只要有可能,一个类型应当设计成不变类;在对其他类的引用上,一个对象对其他对象的引用应当降到最低。


6.接口隔离原则(ISP)
定义:一个类对另一个类的依赖应该建立在最小的接口上。
建立单一接口,不要建立庞大臃肿的接口,尽量细化接口,接口中的方法尽量少。也就是说,我们要为各个类建立专用的接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。
采用接口隔离原则对接口进行约束时,要注意以下几点:

接口尽量小,但是要有限度。对接口进行细化可以提高程序设计灵活性,但是如果过小,则会造成接口数量过多,使设计复杂化。所以一定要适度。
为依赖接口的类定制服务,只暴露给调用的类它需要的方法,它不需要的方法则隐藏起来。只有专注地为一个模块提供定制服务,才能建立最小的依赖关系。
提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情。
这六个原则,可以使我们在应用的迭代维护中更加方便、轻松的应对,让我们的软件更加灵活。在后续的文章中我会给大家介绍其他的设计模式。


7.合成复用原则
如果要使用继承关系,则必须严格遵循里氏替换原则。合成复用原则同里氏替换原则相辅相成的,两者都是开闭原则的具体实现规范。


总体来说可以分为三大类:创建型模式( Creational Patterns )、结构型模式( Structural Patterns )和行为型模式( Behavioral Patterns )。
Java设计模式 算法 - 图3


单例模式(推荐使用枚举来实现单例模式)

什么是单例模式

确保一个类只有一个实例并提供全局访问点。

懒汉式,线程不安全

当被问到要实现一个单例模式时,很多人的第一反应是写出如下的代码,包括教科书上也是这样教我们的。

public class Singleton { private static Singleton instance; private Singleton (){} public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; }}

这段代码简单明了,而且使用了懒加载模式,但是却存在致命的问题。当有多个线程并行调用 getInstance() 的时候,就会创建多个实例。也就是说在多线程下不能正常工作。

懒汉式,线程安全

为了解决上面的问题,最简单的方法是将整个 getInstance() 方法设为同步(synchronized)。

public static synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance;}

虽然做到了线程安全,并且解决了多实例的问题,但是它并不高效。因为在任何时候只能有一个线程调用 getInstance() 方法。但是同步操作只需要在第一次调用时才被需要,即第一次创建单例实例对象时。这就引出了双重检验锁。

双重检验锁

双重检验锁模式(double checked locking pattern),是一种使用同步块加锁的方法。程序员称其为双重检查锁,因为会有两次检查 instance == null,一次是在同步块外,一次是在同步块内。为什么在同步块内还要再检验一次?因为可能会有多个线程一起进入同步块外的 if,如果在同步块内不进行二次检验的话就会生成多个实例了。

public static Singleton getSingleton() { if (instance == null) { //Single Checked synchronized (Singleton.class) { if (instance == null) { //Double Checked instance = new Singleton(); } } } return instance ;}

这段代码看起来很完美,很可惜,它是有问题。主要在于instance = new Singleton()这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情。

  1. 给 instance 分配内存
  2. 调用 Singleton 的构造函数来初始化成员变量
  3. 将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了)

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

public class Singleton { private volatile static Singleton instance; //声明成 volatile private Singleton (){} public static Singleton getSingleton() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } }

有些人认为使用 volatile 的原因是可见性,也就是可以保证线程在本地不会存有 instance 的副本,每次都是去主内存中读取。但其实是不对的。使用 volatile 的主要原因是其另一个特性:禁止指令重排序优化。也就是说,在 volatile 变量的赋值操作后面会有一个内存屏障(生成的汇编代码上),读操作不会被重排序到内存屏障之前。比如上面的例子,取操作必须在执行完 1-2-3 之后或者 1-3-2 之后,不存在执行到 1-3 然后取到值的情况。从「先行发生原则」的角度理解的话,就是对于一个 volatile 变量的写操作都先行发生于后面对这个变量的读操作(这里的“后面”是时间上的先后顺序)。
但是特别注意在 Java 5 以前的版本使用了 volatile 的双检锁还是有问题的。其原因是 Java 5 以前的 JMM (Java 内存模型)是存在缺陷的,即时将变量声明成 volatile 也不能完全避免重排序,主要是 volatile 变量前后的代码仍然存在重排序问题。这个 volatile 屏蔽重排序的问题在 Java 5 中才得以修复,所以在这之后才可以放心使用 volatile。
相信你不会喜欢这种复杂又隐含问题的方式,当然我们有更好的实现线程安全的单例模式的办法。

饿汉式 static final field

这种方法非常简单,因为单例的实例被声明成 static 和 final 变量了,在第一次加载类到内存中时就会初始化,所以创建实例本身是线程安全的。

public class Singleton{ //类加载时就初始化 private static final Singleton instance = new Singleton(); private Singleton(){} public static Singleton getInstance(){ return instance; }}

这种写法如果完美的话,就没必要在啰嗦那么多双检锁的问题了。缺点是它不是一种懒加载模式(lazy initialization),单例会在加载类后一开始就被初始化,即使客户端没有调用 getInstance()方法。饿汉式的创建方式在一些场景中将无法使用:譬如 Singleton 实例的创建是依赖参数或者配置文件的,在 getInstance() 之前必须调用某个方法设置参数给它,那样这种单例写法就无法使用了。

静态内部类 static nested class

我比较倾向于使用静态内部类的方法,这种方法也是《Effective Java》上所推荐的。

public class Singleton { private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton(); } private Singleton (){} public static final Singleton getInstance() { return SingletonHolder.INSTANCE; } }

这种写法仍然使用JVM本身机制保证了线程安全问题;由于 SingletonHolder 是私有的,除了 getInstance() 之外没有办法访问它,因此它是懒汉式的;同时读取实例的时候不会进行同步,没有性能缺陷;也不依赖 JDK 版本。

枚举 Enum

用枚举写单例实在太简单了!这也是它最大的优点。下面这段代码就是声明枚举实例的通常做法。

public enum EasySingleton{ INSTANCE;}

我们可以通过EasySingleton.INSTANCE来访问实例,这比调用getInstance()方法简单多了。创建枚举默认就是线程安全的,所以不需要担心double checked locking,而且还能防止反序列化导致重新创建新的对象。但是还是很少看到有人这样写,可能是因为不太熟悉吧。

总结

一般来说,单例模式有五种写法:懒汉、饿汉、双重检验锁、静态内部类、枚举。上述所说都是线程安全的实现,文章开头给出的第一种方法不算正确的写法。
就我个人而言,一般情况下直接使用饿汉式就好了,如果明确要求要懒加载(lazy initialization)会倾向于使用静态内部类,如果涉及到反序列化创建对象时会试着使用枚举的方式来实现单例。


简单工厂模式

简单工厂模式属于创建型模式又叫做静态工厂方法模式,它属于类创建型模式。在简单工厂模式中,可以根据参数的不同返回不同类的实例。
简单工厂模式专门定义一个类来负责创建其他类的实例,被创建的实例通常都具有共同的父类。

简单工厂模式简单实现

这里我们用生产电脑来举例,假设有一个电脑的代工生产商,它目前已经可以代工生产联想电脑了,随着业务的拓展,这个代工生产商还要生产惠普和华硕的电脑,这样我们就需要用一个单独的类来专门生产电脑,这就用到了简单工厂模式。下面我们来实现简单工厂模式:
工厂模式总结
1.简单工厂:简单工厂属于创建的一种模式,也是工厂模式最简单最实用的一种模式(核心思想:会封装实例化对象的代码)创建一个对象时候把创建的对象实例封装起来


原型模式

浅拷贝:浅拷贝只会将基本类型数据copy出去
实现:直接实现Cloneable接口并重写clone方法即可,使用的时候直接 instance.clone() 即可,默认调Object的clone()方法就可以

  1. package com.design.prototype.deepclone;
  2. import java.io.Serializable;
  3. public class DeepCloneableTarget implements Serializable,Cloneable{
  4. private static final long serialVersionUID = 1L;
  5. private String cloneName;
  6. private String cloneClass;
  7. public String getCloneName() {
  8. return cloneName;
  9. }
  10. public void setCloneName(String cloneName) {
  11. this.cloneName = cloneName;
  12. }
  13. public String getCloneClass() {
  14. return cloneClass;
  15. }
  16. public void setCloneClass(String cloneClass) {
  17. this.cloneClass = cloneClass;
  18. }
  19. @Override
  20. public String toString() {
  21. return "DeepCloneableTarget [cloneName=" + cloneName + ", cloneClass=" + cloneClass + "]";
  22. }
  23. @Override
  24. protected Object clone() throws CloneNotSupportedException {
  25. // TODO Auto-generated method stub
  26. return super.clone();
  27. }
  28. public DeepCloneableTarget(String cloneName, String cloneClass) {
  29. super();
  30. this.cloneName = cloneName;
  31. this.cloneClass = cloneClass;
  32. }
  33. }

深拷贝:不仅仅是拷贝对象的基本类型参数,对象中的引用数据也会copy出去
实现:可以使用序列化和clone()两种方式来实现
clone方式深克隆:

  1. package com.design.prototype.deepclone;
  2. import java.io.ByteArrayInputStream;
  3. import java.io.ByteArrayOutputStream;
  4. import java.io.IOException;
  5. import java.io.ObjectInputStream;
  6. import java.io.ObjectOutputStream;
  7. import java.io.Serializable;
  8. public class DeepProtoType implements Cloneable,Serializable{
  9. private static final long serialVersionUID = 1L;
  10. public String name;
  11. public DeepCloneableTarget deep;
  12. public DeepProtoType() {
  13. super();
  14. }
  15. @Override
  16. public String toString() {
  17. return "DeepProtoType [name=" + name + ", deep=" + deep + "]";
  18. }
  19. //深拷贝1:clone()
  20. @Override
  21. protected Object clone() throws CloneNotSupportedException {
  22. // TODO Auto-generated method stub
  23. DeepProtoType deepProtoType = null;
  24. deepProtoType = (DeepProtoType) super.clone();
  25. deepProtoType.deep = (DeepCloneableTarget) deep.clone();
  26. return deepProtoType;
  27. }
  28. }

序列化方式深克隆:

  1. package com.design.prototype.deepclone;
  2. import java.io.ByteArrayInputStream;
  3. import java.io.ByteArrayOutputStream;
  4. import java.io.IOException;
  5. import java.io.ObjectInputStream;
  6. import java.io.ObjectOutputStream;
  7. import java.io.Serializable;
  8. public class DeepProtoType implements Cloneable,Serializable{
  9. private static final long serialVersionUID = 1L;
  10. public String name;
  11. public DeepCloneableTarget deep;
  12. public DeepProtoType() {
  13. super();
  14. }
  15. @Override
  16. public String toString() {
  17. return "DeepProtoType [name=" + name + ", deep=" + deep + "]";
  18. }
  19. //序列化深克隆
  20. public Object deepClone() throws Exception {
  21. ByteArrayInputStream bais = null;
  22. ByteArrayOutputStream baos = null;
  23. ObjectInputStream ois = null;
  24. ObjectOutputStream oos = null;
  25. try {
  26. //序列化
  27. baos = new ByteArrayOutputStream();
  28. oos = new ObjectOutputStream(baos);
  29. oos.writeObject(this);
  30. //反序列化
  31. bais = new ByteArrayInputStream(baos.toByteArray());
  32. ois = new ObjectInputStream(bais);
  33. Object copyObj = ois.readObject();
  34. return copyObj;
  35. }catch(Exception e) {
  36. System.out.println(e.getMessage());
  37. return null;
  38. }finally {
  39. ois.close();
  40. bais.close();
  41. oos.close();
  42. baos.close();
  43. }
  44. }
  45. }

建造模式

描述:将一个复杂对象的构建与其表示分离,使得同样的构建过程可以创建不同的表示
是将一个复杂的对象的构建与它的表示分离,使
得同样的构建过程可以创建不同的表示。创建者模式隐藏了复杂对象的创建过程,它把复杂对象的创建过程加以抽象,通过子类继承或者重载的方式,动态的创建具有复合属性的对象。
使用场景:当一个类的构造函数参数个数超过4个,而且这些参数有些是可选的参数,考虑使用构造者模式