23种设计模式
1.设计模式概念
1.1 什么地方可以用到设计模式
面向对象(OO)=>功能模块[设计模式+算法(数据结构)]=>框架[使用多种设计模式]=>架构[服务器集群] 复制代码
1.2 使用设计模式的好处
- 使用设计模式,软件具有很好的可扩展性(可以增加新的功能)
- 使用开发模式,具有很好的维护性(可读性、规范性)
1.3 设计模式的目的
- 设计模式是为了让程序,具有更好的代码重复性、可读性(编程规范性)、可扩展性(可维护性)、可靠性、是程序呈现高内聚,低耦合的特征。(模块内部逻辑关系非常紧密,模块与模块之间的关系非常的松散)
- 分享金句:"懂了设计模式,你就懂了面向对象分析和面向对象设计(OOA/OOD)的精要"。
- C++老手与C++新手的区别就是,前者手背上有很多的伤疤。
2.设计模式的七大原则
- 设计模式原则,其实就是程序员在编译时,应当遵守的原则,也就是各种设计模式的基础(即:设计模式为什么这样设计的依据)
设计模式常用的七大原则:
网络异常,图片无法展示
|
- 单一职责原则
- 接口隔离原则
- 依赖倒转(倒置)原则
- 里氏替换原则
- 开闭原则
- 迪米特原则
- 合成复用原则(在一些地方不写这个原则)
2.1 单一职责原则
- 对于类来说,即一个类应该只负责一项职责。如果A类负责两个不同的职责:职责1、职责2。当职责1发生变化而改变A时,可能会对职责2造成影响使职责2运行错误,所以需要将类A的粒度分解为A1、A2。
- 如果再类中没有满足单一职责原则,在一个类的方法中遵守单一职责原则也是可以的(交通工具)
- 标准的单一职责原则,是在类的级别上进行拆分,而不是方法级别。
- 通常情况下,我们要遵守单一职责原则,只有当逻辑足够简单,才可以在代码级别违反单一职责原则;只有类中的方法数量足够少,可以在方法级别保持单一职责原则。
- 优秀的代码中使用类来区分多个分支,而不使用 if...else if()....else(耦合度高)
2.2 接口隔离原则
- 客户端不应该依赖它不需要接口,即一个类对另一个类的依赖应该建立在最小的接口上。
- 处理方式:将接口Interface拆分为独立的几个接口,类A与类C分别于他们需要的接口建立依赖关系。这就是使用的接口隔离原则。
没有使用接口隔离原则时的实现类图:(此时A、C要实现接口里的所有方法)
使用接口隔离原则时的实现类图:(此时将接口进行了拆分,A此时只需要实现它要使用的方法对应的接口即可,而不用将接口中的方法全部实现)
2.3 依赖倒转(倒置)原则
- 在Java中,抽象是指接口或者抽象类,细节是指具体的实现类。
- 高层模块不应该依赖低层模块,二者都应该依赖其抽象(接口、抽象类)。
- 抽象类不应该依赖细节,细节应该依赖抽象类。
- 依赖倒倒转(倒置)的中心思想是面向接口编程。
依赖倒转原则是基于这样的设计理念:
- 相对于细节的多变性,抽象的东西要稳定的多。以抽象为基础搭建的架构比细节为基础的架构要稳定的多。
- 使用接口或者抽象类的目的是制定好规范,而不涉及任何具体的操作,把展示细节的任务交给他们的实现类去完成。
注意:在一个类文件中可以声明其他类、接口,只是这些都不能使用public修饰。但是声明的这些类和方法还是可以被其他的类继承或者实现的。
依赖关系传递的三种方式
- 接口传递
- 构造方法传递
- setter方式传递
依赖原则要注意的地方
- 底层模块尽量都要有抽象类和接口,或者两者都有,程序稳定性更好。
- 变量的声明类型尽量是抽象类和接口,这样我们的变量引用个实际对象间,就曾在一个缓冲层,利于程序的扩展和优化。(就比如你和对象吵架,你先找丈母娘来劝说对象,而不是与对象直接沟通)
- 继承时遵循里氏替换原则。
2.4 里氏替换原则
- 使用继承的时候,父类会对子类进行约束。并且如果父类中的方法发生改变的时候,可能会对所有的子类造成影响。
里氏替换原则
- 里氏替换原则是在1988年麻省理工学院的一个姓李的女士提出的。
- 所有引用基类的地方必须先是透明的使用其子类的对象。
- 在继承中,遵循里氏替换原则,在子类中尽量不在重写父类的方法。
- 里氏替换原则告诉我们,继承实际上让两个类耦合性增强了,在适当的情况下,可以通过聚合、组合、依赖来解决问题。
解决问题的办法
- 原来的父类和子类都继承一个更通俗的基类,原有的继承关系去掉,采用依赖、聚合、组合等关系替代。
2.5 开闭原则(ocp原则)
- 开闭原则是编程中最基础、最重要的设计原则。
- 一个软件的实体如类、模块和函数应该对扩展开放(针对提供方),对修改关闭(对使用者)。
- 用抽象构建架构,用实现扩展细节。
- 当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现。
- 编程中遵循其他原则,以及使用设计模式的的目的就是遵循开闭原则。
2.6 迪米特法则
- 一个对象应该对其他对象保持最少的了解。
- 类与类关系越密切,耦合度越大。
- 迪米特法则又叫最少知道原则,即一个类对自己依赖的类知道的越少越好。也就是说,对于依赖的类不管多么的复杂,都尽量将逻辑封装在类的内部。对外除了了提供public 方法,不对外泄露任何信息。
- 迪米特法则还有个人更简单的定义:只与直接的朋友通信。
- 直接的朋友: 每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系,我们就说这两个对象之间是朋友关系。耦合的方式有很多,依赖、关联、组合、聚合等。其中 ,我们称出现在成员变量、方法参数、方法的返回值中的类为直接的朋友,而出现在局部变量中的类不是直接的朋友。也就是说,陌生的类不要以局部变量的形式出现在类的内部。
迪米特法则
- 核心:降低类之间的耦合
- 注意:由于每个类都减少了不必要的依赖,因此迪米特法则只是要求降低类间(对象间)耦合关系,并不是要求完全没有依赖关系。
2.7 合成复用原则
- 基本介绍:尽量使用合成/聚合的方式,而不是使用继承。(依赖、聚合、组合)
- 找出应用中可能需要变化之处,把他们独立出来,不要和那些不需要变化的代码混在一起。
- 针对接口编程,而不是针对实现编程。
- 为了交互对象间的松耦合设计而努力
4.设计模式
4.1 设计模式概念和分类
- 概念:设计模式的本质提高软件的维护性、通用性和扩展性,并降低软件的复杂度。
- 《设计模式》是经典的书,作者俗称“四人组 GOF”。
- 设计模式并不是局限于某种语言的,Java、C++、PHP都有设计模式。
4.2 设计模式的分类
- 创建型模式:单列模式、抽象工厂模式、原型模式、建造者模式、工厂模式。
- 结构型模式:适配器模式、桥接模式、装饰模式、组合模式、外观模式、享元模式、代理模式。
- 行为型模式:模板方法模式、命令模式、访问者模式、迭代器模式、观察者模式、中介者模式、备忘录模式、解释器模式(Interperter模式)、状态模式、策略模式、职责链模式(责任链模式)。
注意:不同书籍对哦分类和名称略有差别。
5. 单例模式
- 所谓类的单例设计模式,就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法(静态方法)。
- 以上是使用单例模式实现的步骤。
5.1 单例(静态常量饿汉式)推荐使用
- 之所以叫饿汉式,不论用不用这个类的对象,只要加载类的时候就会创建出来这个类的一个对象。
- 饿汉式是线程安全的。
- 使用饿汉式对象在类加载的时候就会被创建。
- 使用类名.静态方法 获取到类的唯一对象。
- 这种单例模式可能会造成内存的浪费,因为这个对象在类加载的时候就创建出来了,如果在主方法中没有用到这个对象,就相当于白创建了这个对象,此时会造成内存的浪费。
5.2 单列(静态代码块饿汉式)推荐使用
- 这种写法和上边的一样,可能会造成内存浪费。
5.3 单例(线程不安全懒汉式)
- 之所以叫懒加载是因为只有用到的时候才会加载,不用到的时候不会去加载这个对象。
- 使用懒汉式可以解决内存浪费问题,只有调用getInstance() 方法的时候才会创建对象,并且在第二次调用方法的时候,不会在从新创建一个新的对象,而是返回第一次创建的对象,保证单例模式。
- 以上编写的代码,虽然起到了懒汉式的作用,但是只能在单线程之下使用。
- 如果在多线程下,一个线程刚刚进入到 if(instance == null) 判断语句块,还没来得及往下执行,也就是还没有创建出一个对象,另一个线程也通过了这个判断语句,这会便会产生多个实例对象。所以在多线程下是不安全的。
- 在实际开发中,不要使用这种方式。
5.4 单例(懒汉式:线程安全,同步方法)
- 将创建对象的方法使用 synchronized关键字来修饰,这样就可以保证线程安全,此时多个线程调用getInstance()方法的时候需要排队,等待上一线程结束才可以进行下一个线程的调用,因为此时上一个线程已创建出一个对象,此时就不会在创建出一个新的对象。不仅保证了线程安全,还可以实现单例模式。
- 以上这种方式实现的效率太低,因为每次调用方法的时候都需要排队。
5.5 单例(懒汉式:线程安全,同步代码块)
- 此时将 synchronized 写到 if() 条件判断中,只要多个线程都进入if()判断中,一定是线程不安全的,在实际开发种不可以使用这种方式。
5.6 双重检查(线程安全,效率高,懒汉式模式)推荐使用
- 在实际开发中,推荐使用这种方式。
5.7 单例(静态内部类)推荐使用
- 推荐使用。
- JVM在装载类的时候是线程安全的。
- 可以保证线程安全、实现了懒加载、保证了效率。
- 这里保证只创建一个实例对象,使用的机制是静态内部类只会加载一次,所以只执行一次对象的创建。
- 这里保证懒加载是因为在加载 Singleton 类的时候不会创建对象,只有调用 getInstance() 方法的时候会使用静态内部类来创建对象。
5.8 单例(枚举)推荐使用
- 在JDK的RunTime的源码中使用到了单例模式。
- 单例模式的特点之一:不是new出来的,而是使用方法调用出来的。
5.9 JDK中源码分析
- 在 Java.lang.Runtime 中使用到了单例模式。
6.工厂模式
6.1 简单工厂模式(静态工厂模式)
- 简单工厂模式属于创建型模式,是工厂模式的一种。简单工厂模式是由一个工厂对象决定创建出哪一种产品类型的实例。 简单工厂模式是工厂模式家族中最简单实用的模式。
- 简单工厂模式:定义一个创建对象的类,由这个类来 来封装实例化对象的行为(代码)
- 在软件开发中,当我们会用到大量的创建某种、某类或者某批对象时,就会使用到工厂模式。
实现一个需求:客户可以点任意口味的披萨,奶酪披萨、胡椒等等。
实现原理:
- 我们创建一个 SimpleFactory 工厂类,这个类负责创建出实例对象,用户给这个工厂传递需求(比如需要创建的对象),这个工厂类会通过自己类中封装的代码(行为)来创建出对应的实例对象
- 以下就是这个简单工厂类,将这个工厂使用聚合或者组合的方式为每一个小店铺添加。这样的话不需要改变小店铺中的代码,只需要改变工厂类中的行为即可。
- 简单工厂模式要比静态工厂模式灵活,简单工厂模式可以任意的改变不同对象的不同行为,而静态工厂模式中行为是一样的,每个对象使用的都是这几个行为。
6.2 工厂方法模式
- 工厂方法模式设计方案: 将披萨项目的实例化功能抽象成方法,在不同的口味点餐子类中具体实现。
- 工厂方法模式:定义了一个创建对象的抽象方法,由子类决定要实例化的类。工厂方法模式将对象的实例化推迟到子类。
实现原理:
- 将OrderPizza类声明为抽象类。BJOrderPizza和LDOrderPizza定义为实现这个抽象类的子类,在这个子类中文成披萨的创建。
- 定义不同地区不同类型的披萨。(定义四个类,按照以下格式依此类推)
- 以下就是创建的这个抽象的父类,这个类来声明创建披萨对象的方法,使用其子类具体实现这个工厂类。
- 让子类继承这个抽象工厂类,在这个子类中创建出关于北京不同的披萨类型。
- 实现顾客选定披萨
6.3 抽象工厂模式
- 抽象工厂模式:定义了一个interface用于创建相关或者有依赖关系的对象簇,而无需指明具体的类。
- 抽象工厂类可以将 简单工厂模式和 工厂方法模式进行整合。
- 从设计层面看,抽象工厂模式就是对简单工厂模式的改进(或者称为进一步的抽象)
- 将工厂抽象为两层,AbsFactory(抽象工厂)和具体实现的工厂子类。程序员可以根据创建对象类型使用对应的工厂子类。这样讲单个的加单工厂类变成了工厂簇。更利于代码的维护和扩展。
- 以上的四个披萨类和上边的简单工厂是一样的。
- 以下是抽象工厂类。
- 以下是两个工厂子类,分别实现不同地区的披萨。
- 以下是一个实现选择披萨的类。
- 以下是最终实现用户选择披萨的类。
工厂模式小结:
- 工厂模式的意义:将实例化对象的代码提取出来,放到一个类中统一管理和维护,达到和主项目的依赖关系解耦。从而提高项目的扩展性和维护性。
- 三种工厂模式(简单工厂模式、工厂方法模式、抽象工厂模式)
工厂模式使用了设计模式的依赖抽象原则:
- 创建对象实例时,不要直接new类,而是把这个new类的动作放在一个工厂的方法中,并返回。有的书上说,变量不要直接持有具体类的引用。
- 不要让类继承具体类,而是继承抽象类或者是实现interface(接口)。
- 不要覆盖基类中已实现的方法。
6.4 JDK中源码分析
- Java中的 Calendar类 使用到了工厂模式