设计模式
创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。
单例模式
-
概述:确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例
-
类别:饿汉式(线程安全,浪费内存)、懒汉式(线程安全)、双重检查、静态内部类(推荐使用)、枚举
-
使用场景:
- 要求生成唯一序列号的环境;
- 在整个项目中需要一个共享访问点或共享数据,例如一个Web页面上的计数器,可以不用把每次刷新都记录到数据库中,使用单例模式保持计数器的值,并确保是线程安全的;
- 创建一个对象需要消耗的资源过多,如要访问IO和数据库等资源;
- 需要定义大量的静态常量和静态方法(如工具类)的环境,可以采用单例模式(当然,也可以直接声明为static的方式)。
- 其他:静态属性在类加载时就被初始化,静态方法需要调用,volatile防治指令重排(如执行一段语句时可能不同的顺序也会得到相同的结果)
工厂模式
-
概述:定义一个用于创建对象的接口,让子类决定实例化哪一个类。工厂方法使一个类的实例化延迟到其子类(创建对象实例时,不要直接new,而是把new类的动作放在一个工厂的方法中)
-
类别:
- 简单工厂:是在一个工厂类的一个方法里面通过传入字符串的不同来创建不同的对象;一个模块仅需要一个工厂类,没有必要把它产生出来,使用静态的方法。
- 工厂方法:一个具体工厂只能创建一种具体产品,每个创建者独立负责创建对应的产品对象,非常符合单一职责原则
-
使用场景:jdbc连接数据库(例如需要设计一个连接邮件服务器的框架,有三种网络协议可供选择:POP3、IMAP、HTTP,我们就可以把这三种连接方法作为产品类,定义一个接口如IConnectMail,然后定义对邮件的操作方法,三个具体的产品类(也就是连接方式)进行不同的实现,再定义一个工厂方法,按照不同的传入条件,选择不同的连接方式。),硬件访问,降低对象的产生和销毁
-
扩展:
-
延迟初始化:一个对象被消费完毕后,并不立刻释放,工厂类保持其初始状态,等待再次被使用(备忘录模式)
-
延迟加载框架是可以扩展的,例如限制某一个产品类的最大实例化数量,可以通过判断Map中已有的对象数量来实现,这样的处理是非常有意义的,例如JDBC连接数据库,都会要求设置一个MaxConnections最大连接数量,该数量就是内存中最大实例化的数量。
延迟加载还可以使用在一个对象初始化比较复杂的情况,例如硬件访问,涉及多方面的交互,则可以通过延迟加载降低对象的产生和销毁带来的复杂性。
- 反射机制加配置文件可以进行优化
-
抽象工厂模式
- 概述:为创建一组相关或相互依赖的对象提供一个接口,而且无须指定它们的具体类。把工厂类作为一个接口,由多个实现该接口的子工厂类来创建不同的对象,扩展性能更好,符合我们的开闭模式
- 使用场景:一个对象族(或是一组没有任何关系的对象)都有相同的约束(形状和颜色)。涉及不同操作系统的时候,都可以考虑使用抽象工厂模式
建造者模式
-
概述:将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示(盖房子:打地基、砌墙、盖屋顶)
-
角色:
- Product产品类
- Builder抽象建造者:规范产品的组建,一般是由子类实现
- ConcreteBuilder具体建造者:实现抽象类定义的所有方法,并且返回一个组建好的对象
- Director导演类
-
使用场景:
- 相同的方法,不同的执行顺序,产生不同的事件结果时,可以采用建造者模式。
- 多个部件或零件,都可以装配到一个对象中,但是产生的运行结果又不相同时,则可以使用该模式
- 产品类非常复杂,或者产品类中的调用顺序不同产生了不同的效能,这个时候使用建造者模式非常合适
-
扩展:工厂类模式提供的是创建单个类的模式,而建造者模式则是将各种产品集中起来管理,用来创建符合对象;
-
建造者模式最主要的功能是基本方法的调用顺序安排,这些基本方法已经实现了,顺序不同产生的对象也不同;
工厂方法则重点是创建,创建零件是它的主要职责,组装顺序则不是它关心的
原型模式
- 概述:用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象 原型模式实际上就是实现Cloneable接口,重写clone()方法
- 优点:原型模式是在内存二进制流的拷贝,要比直接new一个对象性能好很多,直接在内存中拷贝,构造函数是不会执行的
- 使用场景:
- 类初始化需要消化非常多的资源,这个资源包括数据、硬件资源等
- 通过new产生一个对象需要非常繁琐的数据准备或访问权限
- 一个对象需要提供给其他对象访问,而且各个调用者可能都需要修改其值时,可以考虑使用原型模式拷贝多个对象供调用者使用
- 扩展:
- 浅拷贝:Object类提供的方法clone只是拷贝本对象,其对象内部的数组、引用对象等都不拷贝,还是指向原生对象的内部元素地址
- 深拷贝: 复制对象的所有数据类型的成员变量值,为所有引用数据类型的成员变量申请存储空间,并复制每个引用数据类型成员变量所引用的对象,直到该对象可达的所有对象也就是说,对象进行深拷贝要对整个对象进行拷贝
- 深拷贝实现方式:重写clone来实现深拷贝、通过对象序列化实现深拷贝
结构型模式,共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。
适配器模式
-
概述(电压问题):将一个类的接口变换成客户端所期待的另一种接口,从而使原本因接口不匹配而无法在一起工作的两个类能够在一起工作
-
角色:
- Target目标角色:该角色定义把其他类转换为何种接口,也就是我们的期望接口
- Adaptee源角色:你想把谁转换成目标角色,这个“谁”就是源角色,它是已经存在的、运行良好的类或对象,经过适配器角色的包装,它会成为一个崭新、靓丽的角色。
- Adapter适配器角色:适配器模式的核心角色,其他两个角色都是已经存在的角色,而适配器角色是需要新建立的,它的职责非常简单:把源角色转换为目标角色,怎么转换?通过继承或是类关联的方式
-
类别:
- 对象适配器: 以对象给到,在Adapter里,就是将src当做对象,持有 根据复合聚合关系代替继承
- 类适配器 :以类给到,在Adapter里,就是将src当做一个类,继承
- 接口适配器: 以接口给到,在Adapter里,将src作为一个接口,实现
-
使用场景:你有动机修改一个已经投产中的接口时,适配器模式可能是最适合你的模式。比如系统扩展了,需要使用一个已有或新建立的类,但这个类又不符合系统的接口,怎么办?使用适配器模式
-
扩展:对象适配器和类适配器的区别:
类适配器是类间继承,对象适配器是对象的合成关系,也可以说是类的关联关系,这是两者的根本区别。(实际项目中对象适配器使用到的场景相对比较多)。
装饰器模式
- 概述(咖啡加奶加调味品不同价格问题):动态地给一个对象添加一些额外的职责。就增加功能来说,装饰模式相比生成子类更为灵活。
- 角色
- Component抽象构件:Component是一个接口或者是抽象类,就是定义我们最核心的对象,也就是最原始的对象(咖啡)
- ConcreteComponent 具体构件:ConcreteComponent是最核心、最原始、最基本的接口或抽象类的实现,你要装饰的就是它
- Decorator装饰角色:一般是一个抽象类,做什么用呢?实现接口或者抽象方法,它里面可不一定有抽象的方法呀,在它的属性里必然有一个private变量指向Component抽象构件。
- 具体装饰角色:ConcreteDecoratorA和ConcreteDecoratorB是两个具体的装饰类,你要把你最核心的、最原始的、最基本的东西装饰成其他东西(把牛奶装饰成咖啡加牛奶加糖)
- 使用场景:
- 需要扩展一个类的功能,或给一个类增加附加功能
- 需要动态地给一个对象增加功能,这些功能可以再动态地撤销
- 需要为一批的兄弟类进行改装或加装功能,当然是首选装饰模式
代理模式
-
概述(面向aop的切面编程):为其他对象提供一种代理以控制对这个对象的访问
-
角色:
- Subject抽象主题角色:抽象主题类可以是抽象类也可以是接口,是一个最普通的业务类型定义,无特殊要求
- RealSubject具体主题角色:也叫做被委托角色、被代理角色。它才是冤大头,是业务逻辑的具体执行者
- Proxy代理主题角色:也叫做委托类、代理类。它负责对真实角色的应用,把所有抽象主题类定义的方法限制委托给真实主题角色实现,并且在真实主题角色处理完毕前后做预处理和善后处理工作
-
类别:
-
静态代理:得为每一个服务都得创建代理类,工作量太大,不易管理。同时接口一旦发生改变,代理类也得相应修改。
-
动态代理:再需要再手动的创建代理类,我们只需要编写一个动态处理器就可以了。真正的代理对象由JDK再运行时为我们动态的来创建。 缺点:仅支持interface代理的桎梏
-
`//说明`
``//newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)`
`//1.loader指定当前目标对象使用的类加载器,获取类加载器的方法的固定`
`//2.interfaces:目标对象实现的接口类型,使用泛型方法确定类型`
`//3.InvocationHandler:事件处理,执行目标对象的方法时,会触发事情处理器方法,`
`//会把当前执行的目标对象方法作为参数传入`
`return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(),`
`new InvocationHandler() {`
`@Override`
`public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {`
`System.out.println("JDK代理开始");`
`//反射机制调用目标对象方法`
`Object returnVal = method.invoke(target, args);`
`System.out.println("JDK代理提交");`
`return returnVal;`
`}`
});`
-
cglib代理:JDK实现动态代理需要实现类通过接口定义业务方法,对于没有接口的类,如何实现动态代理呢,这就需要CGLib了。CGLib采用了非常底层的字节码技术,其原理是通过字节码技术为一个类创建子类,并在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑。但因为采用的是继承,所以不能对final修饰的类进行代理。JDK动态代理与CGLib动态代理均是实现Spring AOP的基础
-
扩展:CGLIB创建的动态代理对象比JDK创建的动态代理对象的性能更高,但是CGLIB创建代理对象时所花费的时间却比JDK多得多。所以对于单例的对象,因为无需频繁创建对象,用CGLIB合适,反之使用JDK方式要更为合适一些。同时由于CGLib由于是采用动态创建子类的方法,对于final修饰的方法无法进行代理。
外观模式
- 概述(dvd,窗帘等同时打开、运作或关闭):要求一个子系统的外部与其内部的通信必须通过一个统一的对象进行。门面模式提供一个高层次的接口,使得子系统更易于使用
- 角色:
- Facade门面角色(外观类):客户端可以调用这个角色的方法。此角色知晓子系统的所有功能和责任。一般情况下,本角色会将所有从客户端发来的请求委派到相应的子系统去,也就说该角色没有实际的业务逻辑,只是一个委托类
- subsystem子系统角色(功能的实际提供者):可以同时有一个或者多个子系统。每一个子系统都不是一个单独的类,而是一个类的集合。子系统并不知道门面的存在。对于子系统而言,门面仅仅是另外一个客户端而已
- 使用场景:
- 为一个复杂的模块或子系统提供一个供外界访问的接口
- 子系统相对独立——外界对子系统的访问只要黑箱操作即可
- 预防低水平人员带来的风险扩散
- 注意:一个子系统可以有多个外观类,外观类不参与子系统内的业务逻辑
桥接模式
-
概述(手机样式加品牌问题):将抽象和实现解耦,使得两者可以独立地变化(样式是抽象,品牌是实现)
-
角色:
- Abstraction——抽象化角色:它的主要职责是定义出该角色的行为,同时保存一个对实现化角色的引用(品牌)
- Implementor——实现化角色:它是接口或者抽象类,定义角色必需的行为和属性。
- RefinedAbstraction——修正抽象化角色:它引用实现化角色对抽象化角色进行修正
- ConcreteImplementor——具体实现化角色:它实现接口或抽象类定义的方法和属性。
-
使用场景:
-
一个类存在两个独立变化的维度,且这两个维度都需要进行扩展
-
不希望或不适用使用继承的场景
-
接口或抽象类不稳定的场景
-
重用性要求较高的场景
-
-
扩展:桥接模式和适配器模式用于设计的不同阶段,桥接模式用于系统的初步设计,对于存在两个独立变化维度的类可以将其分为抽象化和实现化两个角色,使它们可以分别进行变化;而在初步设计完成之后,当发现系统与已有类无法协同工作时,可以采用适配器模式。但有时候在设计初期也需要考虑适配器模式,特别是那些涉及到大量第三方应用接口的情况。
组合模式
- 概述(大学、系,专业问题):将对象组合成树形结构以表示“部分-整体”的层次结构,使得用户对单个对象和组合对象的使用具有一致性
- 角色:
- Component抽象构件角色:定义参加组合对象的共有方法和属性,可以定义一些默认的行为或属性,
- Leaf叶子构件:叶子对象,其下再也没有其他的分支,也就是遍历的最小单位
- Composite树枝构件:树枝对象,它的作用是组合树枝节点和叶子节点形成一个树形结构
- 使用场景:
- 维护和展示部分-整体关系的场景,如树形菜单、文件和文件夹管理
- 从一个整体中能够独立出部分模块或功能的场景
- 只要是树形结构,就考虑使用组合模式
- 扩展:要求有较高的抽象性,如果叶子和节点有很多差异性的话,比如很多方法和属性都不一样,不适合使用组合模式
享元模式
-
概述(JDK中的Integer.valueof):使用共享对象可有效地支持大量的细粒度的对象
-
状态:
- 内部状态:内部状态是对象可共享出来的信息,存储在享元对象内部并且不会随环境改变而改变
- 外部状态:外部状态是对象得以依赖的一个标记,是随环境改变而改变的、不可以共享的状态
- 网站发布的形式是内部状态,而发表的人则是外部状态
-
角色:
-
Flyweight——抽象享元角色:它简单地说就是一个产品的抽象类,同时定义出对象的外部状态和内部状态的接口或实现
-
ConcreteFlyweight——具体享元角色(发布网站的形式):具体的一个产品类,实现抽象角色定义的业务。该角色中需要注意的是内部状态处理应该与环境无关,不应该出现一个操作改变了内部状态,同时修改了外部状态,这是绝对不允许的
-
unsharedConcreteFlyweight——不可共享的享元角色(用户):不存在外部状态或者安全要求(如线程安全)不能够使用共享技术的对象,该对象一般不会出现在享元工厂中
-
FlyweightFactory——享元工厂:职责非常简单,就是构造一个池容器,同时提供从池中获得对象的方法。
-
-
使用场景:
- 系统中存在大量的相似对象
- 细粒度的对象都具备较接近的外部状态,而且内部状态与环境无关,也就是说对象没有特定身份,细粒度模型,通俗的讲就是将业务模型中的对象加以细分,从而得到更科学合理的对象模型,直观的说就是划分出很多对象。
- 需要缓冲池的场景
-
注意:
- 享元模式提高了系统的复杂度,需要分离出内部状态和外部状态,而外部状态具有固化特性,不应该随着内部状态的变化而变化,并且还需要加一个工厂来控制
- 享元模式是线程不安全的,只有依靠经验,在需要的地方考虑一下线程安全,在大部分场景下不用考虑。对象池中的享元对象尽量多,多到足够满足为止
- 性能安全:外部状态最好以java的基本类型作为标志,如String,int,可以提高效率
行为型模式,共十一种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。
策略模式
- 概述(鸭子问题):定义一组算法,将每个算法都封装起来,并且使它们之间可以互换
- 角色:
- Context封装角色(鸭子这个抽象类):它也叫做上下文角色,起承上启下封装作用,屏蔽高层模块对策略、算法的直接访问,封装可能存在的变化
- Strategy抽象策略角色(鸭子飞的接口和游泳的接口):策略、算法家族的抽象,通常为接口,定义每个策略或算法必须具有的方法和属性
- ConcreteStrategy具体策略角色(多个)(实现接口):实现抽象策略中的操作,该类含有具体的算法
- 使用场景:
- 多个类只有在算法或行为上稍有不同的场景
- 算法需要自由切换的场景
- 需要屏蔽算法规则的场景
模板方法模式
- 概述(制作豆浆,第一步,第二步):定义一个操作中的算法的框架,而将一些步骤延迟到子类中。使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤
- 方法:
- 基本方法:基本方法也叫做基本操作,是由子类实现的方法,并且在模板方法被调用
- 模板方法:可以有一个或几个,一般是一个具体方法,也就是一个框架,实现对基本方法的调度,完成固定的逻辑。为了防止恶意的操作,一般模板方法都加上final关键字,不允许被覆写
- 使用场景:
- 多个子类有公有的方法,并且逻辑基本相同时
- 重要、复杂的算法,可以把核心算法设计为模板方法,周边的相关细节功能则由各个子类实现
- 重构时,模板方法模式是一个经常使用的模式,把相同的代码抽取到父类中,然后通过钩子函数约束其行为
- 一般模板方法的扩展,就是使用所谓的钩子方法,就是声明额外的public方法,不同的子类有不同的实现并有不同的返回值,这些返回值会影响模板方法的执行逻辑或者顺序
观察者模式
- 概述(天气:百度网站,搜狐网站):定义对象间一种一对多的依赖关系,使得每当一个对象改变状态,则所有依赖于它的对象都会得到通知并被自动更新
- 角色:
- Subject被观察者(通知观察者):定义被观察者必须实现的职责,它必须能够动态地增加、取消观察者。它一般是抽象类或者是实现类,仅仅完成作为被观察者必须实现的职责:管理观察者并通知观察者。
- Observer观察者(更新方法):观察者接收到消息后,即进行update(更新方法)操作,对接收到的信息进行处理
- ConcreteSubject具体的被观察者(天气数据Weatherdata 包含观察者集合):定义被观察者自己的业务逻辑,同时定义对哪些事件进行通知
- ConcreteObserver具体的观察者(百度,搜狐):每个观察在接收到消息后的处理反应是不同,各个观察者有自己的处理逻辑
- 使用场景:
- 关联行为场景。需要注意的是,关联行为是可拆分的,而不是“组合”关系
- 事件多级触发场景
- 跨系统的消息交换场景,如消息队列的处理机制
- 注意:
- 广播链的问题:在一个观察者模式中最多出现一个对象既是观察者也是被观察者,也就是说消息最多转发一次(传递两次)
- 异步处理问题:观察者比较多,而且处理时间比较长,采用异步处理来考虑线程安全和队列的问题