1.软件设计模式的概念
软件设计模式(Software Design Pattern),又称设计模式,是一套被==反复使用==、==多数人知晓的==、==代码设计经验的总结==。它描述了在软件设计过程中的一些不断重复发生的问题,以及该问题的解决方案。也就是说,它是解决特定问题的一系列套路,是==前辈们的代码设计经验的总结==,具有一定的普遍性,可以反复使用。
2. 设计模式分类--记住
-
创建型模式
用于描述“怎样创建对象”,它的主要特点是“==将对象的创建与使用分离==”。GoF(四人组)书中提供了==单例、原型、工厂方法、抽象工厂、建造者==等 5 种创建型模式。
-
结构型模式
用于描述如何将类或对象按某种布局组成更大的结构,GoF(四人组)书中提供了==代理、适配器、桥接、装饰、外观、享元、组合==等 7 种结构型模式。
-
行为型模式
用于描述类或对象之间怎样相互协作共同完成单个对象无法单独完成的任务,以及怎样分配职责。GoF(四人组)书中提供了==模板方法、策略、命令、职责链、状态、观察者、中介者、迭代器、访问者、备忘录、解释器==等 11 种行为型模式
3.单例设计模式
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于==创建型模式==,它提供了一种==创建对象的最佳方式==。
这种模式涉及到一个单一的类,==该类负责创建自己的对象==,同时==确保只有单个对象被创建==。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
3.1 单例模式的结构
单例模式的主要有以下角色:
-
单例类。只能创建一个实例的类.
-
访问类。使用单例类
3.2 单例模式的实现
单例设计模式分类两种:
- 饿汉式:类加载就会导致该单实例对象被创建
- 懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建
1.饿汉式(静态变量方法)
public class Test01 {
//单例--饿汉式
public static void main(String[] args) {
Singleton1 instance1 = Singleton1.getInstance();
Singleton1 instance2 = Singleton1.getInstance();
System.out.println(instance1);
System.out.println(instance2);
}
}
class Singleton1 {
//在成员位置创建该类的对象
private static Singleton1 instance = new Singleton1();
//构造方法
private Singleton1() {}
//对外提供静态方法获取该对象
public static Singleton1 getInstance() {
return instance;
}
}
说明:
该方式在成员位置声明Singleton类型的静态变量,并创建Singleton类的对象instance。instance对象是随着类的加载而创建的。如果该对象足够大的话,而一直没有使用就会造成内存的浪费。
2. 懒汉式(线程不安全)
public class Test02 {
public static void main(String[] args) {
Singleton2 instance1 = Singleton2.getInstance();
Singleton2 instance2 = Singleton2.getInstance();
System.out.println(instance1);
System.out.println(instance2);
}
}
//不安全
class Singleton2{
//创建该类的对象
private static Singleton2 instance;
//私有化构造方法
private Singleton2(){}
//获取该类的对象
public static Singleton2 getInstance(){
if(instance == null){
instance = new Singleton2();
}
return instance;
}
}
说明:
从上面代码我们可以看出该方式在成员位置声明Singleton类型的静态变量,并没有进行对象的赋值操作,那么什么时候赋值的呢?当调用getInstance()方法获取Singleton类的对象的时候才创建Singleton类的对象,这样就实现了懒加载的效果。但是,如果是多线程环境,会出现线程安全问题。
3. 懒汉式(线程安全,效率低)
//线程安全
class Singleton3{
//创建该类的对象
private static Singleton3 instance;
//私有化构造方法
private Singleton3(){}
//获取该类的对象
//执行效率低
public static synchronized Singleton3 getInstance(){
if(instance == null){
instance = new Singleton3();
}
return instance;
}
}
说明:
该方式也实现了懒加载效果,同时又解决了线程安全问题。但是在getInstance()方法上添加了synchronized关键字,导致该方法的执行效果特别低。从上面代码我们可以看出,其实就是在初始化instance的时候才会出现线程安全问题,一旦初始化完成就不存在了。
4. 懒汉式(线程安全且效率高)
再来讨论一下懒汉模式中加锁的问题,对于 getInstance()
方法来说,绝大部分的操作都是读操作,读操作是线程安全的,所以我们没必让每个线程必须持有锁才能调用该方法,我们需要调整加锁的时机。由此也产生了一种新的实现模式:双重检查锁模式
//双重检查锁
/**
* 使用双检锁实现的单例类。
* 这种实现方式在多线程环境下保证了单例的唯一性,同时尽可能地提高了实例化的效率。
*/
class Singleton4{
// 静态实例变量,用于保存单例实例
private static Singleton4 instance;
// 私有构造方法,防止外部实例化对象
private Singleton4(){}
/**
* 获取单例实例的方法。
* 使用双检锁机制,在第一个线程通过检查后才进行同步,减少了同步的开销,提高了效率。
*
* @return 单例实例
*/
public static Singleton4 getInstance(){
// 第一次检查,如果实例已经存在,则直接返回,不进入同步代码块
if(instance == null){
// 当多个线程同时通过第一个if检查后,只有一个线程能进入同步代码块
synchronized (Singleton4.class){
// 第二次检查,确保在同步环境下仍然只有一个实例被创建
if(instance == null){
// 在这里安全地创建单例实例
instance = new Singleton4();
}
}
}
// 返回单例实例
return instance;
}
}
双重检查锁模式是一种非常好的单例实现模式,解决了单例、性能、线程安全问题,上面的双重检测锁模式看上去完美无缺,其实是存在问题,在多线程的情况下,可能会出现空指针问题,出现问题的原因是JVM在实例化对象的时候会进行优化和指令重排序操作。
要解决双重检查锁模式带来空指针异常的问题,只需要使用 volatile
关键字, volatile
关键字可以保证可见性和有序性。
/**
* 单例模式的实现类。
* 保证一个类只有一个实例,并提供一个全局访问点。
*/
//双重检查锁
class Singleton{
// 使用volatile修饰符确保多线程环境下的可见性和一致性
private static volatile Singleton instance;
// 私有构造方法防止外部实例化
private Singleton(){};
/**
* 获取单例实例。
* 使用双检锁机制,在确保线程安全的同时提高实例化的效率。
*
* @return 单例实例
*/
public static Singleton getInstance(){
// 检查实例是否已经存在,如果不存在则进入同步块
if (instance==null){
// 使用类锁进行同步,防止多线程同时进入创建实例
synchronized (Singleton.class){
// 再次检查实例是否存在,避免多线程环境下重复创建实例
if (instance==null){
// 创建单例实例
instance=new Singleton();
}
}
}
// 返回单例实例
return instance;
}
}
-
小结:
添加
volatile
关键字之后的双重检查锁模式是一种比较好的单例实现模式,能够保证在多线程的情况下线程安全也不会有性能问题。
还有,这里的private static volatile Singleton singleton = null;中的volatile也必不可少,volatile关键字可以防止jvm指令重排优化,
在java内存模型中,volatile 关键字作用可以是保证可见性或者禁止指令重排。这里是因为 singleton = new Singleton() ,它并非是一个原子操作,事实上,在 JVM 中上述语句至少做了以下这 3 件事:
-
第一步是给 singleton 分配内存空间;
-
第二步开始调用 Singleton 的构造函数等,来初始化 singleton;
-
第三步,将 singleton 对象指向分配的内存空间(执行完这步 singleton 就不是 null 了)
这里需要留意一下 1-2-3 的顺序,因为存在指令重排序的优化,也就是说第 2 步和第 3 步的顺序是不能保证的,最终的执行顺序,可能是 1-2-3,也有可能是 1-3-2。
如果是 1-3-2,那么在第 3 步执行完以后,singleton 就不是 null 了,可是这时第 2 步并没有执行,singleton 对象未完成初始化,它的属性的值可能不是我们所预期的值。假设此时线程 2 进入 getInstance 方法,由于 singleton 已经不是 null 了,所以会通过第一重检查并直接返回,但其实这时的 singleton 并没有完成初始化,所以使用这个实例的时候会报错,详细流程如下图所示:
这里还说一下volatile关键字的第二个作用,保证变量在多线程运行时的可见性:
public class Testv04 {
public static void main(String[] args) throws InterruptedException {
T t = new T();
t.start();
Thread.sleep(2000);
System.out.println("主线程设置t线程参数来止损失");
t.setFlag(false);
}
}
class T extends Thread{
//定义flag默认为true
//volatile避免死循环
private volatile boolean flag=true;
public void setFlag(boolean flag){
this.flag=flag;
}
@Override
public void run() {
System.out.println("进入run方法");
while (flag){
}
}
}
在 JDK1.2 之前,Java的内存模型实现总是从主存(即共享内存)读取变量,是不需要进行特别的注意的。而在当前 的 Java 内存模型下,线程可以把变量保存本地内存(比如机器的寄存器)中,而不是直接在主存中进行读写。这就 可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数 据的不一致。 要解决这个问题,就需要把变量声明为 volatile,这就指示 JVM,这个变量是不稳定的,每次使用它都到主存中进行 读取。
3.3 单例模式优点和缺点
优点:单例类只有一个实例,节省了内存资源,对于一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能;单例模式可以在系统设置全局的访问点,优化和共享数据,例如前面说的Web应用的页面计数器就可以用单例模式实现计数值的保存。
缺点:单例模式一般没有接口,扩展的话除了修改代码基本上没有其他途径。
3.4 懒汉模式和饿汉模式区别 面试题
懒汉模式的优点便是在代码中没有使用的情况下,不会去加载单例类的资源不会造成资源的浪费。缺点也很明显,加锁同步会带来程序运行效率的损失。
饿汉模式的优缺点恰好与懒汉模式相反,如果明确知道单例对象在程序代码中用的很频繁,就可以考虑使用饿汉模式了。
4.工厂模式
工厂模式将目的将创建对象的具体过程屏蔽隔离起来,从而达到更高的灵活性,工厂模式可以分为三类:
-
简单工厂模式(Simple Factory)
-
工厂方法模式(Factory Method)
-
抽象工厂模式(Abstract Factory)
4.1 简单工厂模式(Simple Factory)
简单工厂模式的核心是定义一个创建对象的接口,将对象的创建和本身的业务逻辑分离,降低系统的耦合度,使得两个修改起来相对容易些,当以后实现改变时,只需要修改工厂类即可
如果不使用工厂,用户将自己创建宝马车,具体UML图和代码如下:
class Car {
private void Car() {}
}
public class Car1 extends Car{
public Car1() {
System.out.println("造一辆一号车");
}
}
public class Car2 extends Car{
public Car2() {
System.out.println("造一辆二号车");
}
}
public class Test01 {
public static void main(String[] args) {
Car1 car1 = new Car1();
Car2 car2 = new Car2();
}
}
用户需要知道怎么创建一款车,这样子客户和车就紧密耦合在一起了,为了降低耦合,就出现了简单工厂模式,把创建宝马的操作细节都放到了工厂里,而客户直接使用工厂的创建方法,传入想要的宝马车型号就行了,而不必去知道创建的细节。
-
工厂类角色: 该模式的核心,用来创建产品,含有一定的商业逻辑和判断逻辑
-
抽象产品角色:它一般是具体产品继承的父类或者实现的接口。
-
具体产品角色:工厂类所创建的对象就是此角色的实例。在java中由一个具体类实现。
//产品类
class Car {
private void Car() {}
}
public class Car1 extends Car{
public Car1() {
System.out.println("造一辆一号车");
}
}
public class Car2 extends Car{
public Car2() {
System.out.println("造一辆二号车");
}
}
//工厂类
class easyFactor {
public Car factor(String carName) {
if (carName.equals("car1")) {
return new Car1();
}
if (carName.equals("car2")){
return new Car2();
}
return null;
}
}
//用户类
public class Test02 {
//简单工厂
public static void main(String[] args) {
easyFactor easyFactor = new easyFactor();
Car car1 = easyFactor.factor("car1");
}
}
简单工厂模式的优缺点:
简单工厂模式提供专门的工厂类用于创建对象,实现了对象创建和使用的职责分离,客户端不需知道所创建的具体产品类的类名以及创建过程,只需知道具体产品类所对应的参数即可,通过引入配置文件,可以在不修改任何客户端代码的情况下更换和增加新的具体产品类,在一定程度上提高了系统的灵活性。
但缺点在于不符合“开闭原则”,每次添加新产品就需要修改工厂类。在产品类型较多时,有可能造成工厂逻辑过于复杂,不利于系统的扩展维护,并且工厂类集中了所有产品创建逻辑,一旦不能正常工作,整个系统都要受到影响。
为了解决简单工厂模式的问题,出现了工厂方法模式。
4.2 工厂方法模式:
==工厂方法模式将工厂抽象化,并定义一个创建对象的接口。每增加新产品,只需增加该产品以及对应的具体实现工厂类,由具体工厂类决定要实例化的产品是哪个,将对象的创建与实例化延迟到子类,这样工厂的设计就符合“开闭原则”了,扩展时不必去修改原来的代码==。在使用时,用于只需知道产品对应的具体工厂,关注具体的创建过程,甚至不需要知道具体产品类的类名,当我们选择哪个具体工厂时,就已经决定了实际创建的产品是哪个了
但缺点在于,每增加一个产品都需要增加一个具体产品类和实现工厂类,使得系统中类的个数成倍增加,在一定程度上增加了系统的复杂度,同时也增加了系统具体类的依赖。
工厂方法的 UML 结构图如下:
-
抽象工厂 AbstractFactory: 工厂方法模式的核心,是具体工厂角色必须实现的接口或者必须继承的父类,在 Java 中它由抽象类或者接口来实现。
-
具体工厂 Factory:被应用程序调用以创建具体产品的对象,含有和具体业务逻辑有关的代码 抽象产品 AbstractProduct:是具体产品继承的父类或实现的接口,在 Java 中一般有抽象类或者接口来实现。
-
具体产品 Product:具体工厂角色所创建的对象就是此角色的实例。
代码实现
产品类
//产品类
class Car {
private void Car() {}
}
public class Car1 extends Car{
public Car1() {
System.out.println("造一辆一号车");
}
}
工厂类:
public interface Ffactor {
public Car create();
}
public class Carfactor1 implements Ffactor{
@Override
public Car create() {
return new Car1();
}
}
public class Carfactor2 implements Ffactor{
@Override
public Car create() {
return new Car2();
}
}
用户类:
public class Teat03 {
public static void main(String[] args) {
//具体工厂
Carfactor1 carfactor1 = new Carfactor1();
Car car1 = carfactor1.create();
Carfactor2 carfactor2 = new Carfactor2();
Car car2 = carfactor2.create();
}
}
4.3.抽象工厂模式:了解
在工厂方法模式中,我们使用一个工厂创建一个产品,一个具体工厂对应一个具体产品,但有时候我们需要一个工厂能够提供多个产品对象,而不是单一的对象,这个时候我们就需要使用抽象工厂模式
在介绍抽象工厂模式前,我们先理清两个概念:
-
产品等级结构:产品等级结构指的是产品的继承结构,例如一个空调抽象类,它有海尔空调、格力空调、美的空调等一系列的子类,那么这个空调抽象类和他的子类就构成了一个产品等级结构。
-
产品族:产品族是指由同一个工厂生产的,位于不同产品等级结构中的一组产品。比如,海尔工厂生产海尔空调、海尔冰箱,那么海尔空调则位于海尔族中。
产品等级结构和产品族结构示意图如下
1、什么是抽象工厂模式:
抽象工厂模式主要用于创建相关对象的家族。当一个产品族中需要被设计在一起工作时,通过抽象工厂模式,能够保证客户端始终只使用同一个产品族中的对象;并且通过隔离具体类的生成,使得客户端不需要明确指定具体生成类;所有的具体工厂都实现了抽象工厂中定义的公共接口,因此只需要改变具体工厂的实例,就可以在某种程度上改变整个软件系统的行为。
但该模式的缺点在于添加新的行为时比较麻烦,如果需要添加一个新产品族对象时,需要更改接口及其下所有子类,这必然会带来很大的麻烦。
UML结构图
抽象工厂 AbstractFactory:定义了一个接口,这个接口包含了一组方法用来生产产品,所有的具体工厂都必须实现此接口。
具体工厂 ConcreteFactory:用于生产不同产品族,要创建一个产品,用户只需使用其中一个工厂进行获取,完全不需要实例化任何产品对象。
抽象产品 AbstractProduct:这是一个产品家族,每一个具体工厂都能够生产一整组产品。
具体产品 Product
代码实现
通过抽象工厂模式,我们可以实现以下的效果:比如宝马320系列使用空调型号A和发动机型号A,而宝马230系列使用空调型号B和发动机型号B,在为320系列生产相关配件时,就无需制定配件的型号,它会自动根据车型生产对应的配件型号A。
也就是说,当每个抽象产品都有多于一个的具体子类的时候(空调有型号A和B两种,发动机也有型号A和B两种),工厂角色怎么知道实例化哪一个子类呢?抽象工厂模式提供两个具体工厂角色(宝马320系列工厂和宝马230系列工厂),分别对应于这两个具体产品角色,每一个具体工厂角色只负责某一个产品角色的实例化,每一个具体工厂类只负责创建抽象产品的某一个具体子类的实例。
产品类:
//发动机以及型号
public interface Engine {}
public class EngineA implements Engine{
public EngineA(){
System.out.println("制造-->EngineA");
}
}
public class EngineB implements Engine{
public EngineB(){
System.out.println("制造-->EngineB");
}
}
//空调以及型号
public interface Aircondition {}
public class AirconditionA implements Aircondition{
public AirconditionA(){
System.out.println("制造-->AirconditionA");
}
}
public class AirconditionB implements Aircondition{
public AirconditionB(){
System.out.println("制造-->AirconditionB");
}
}
工厂类:
//创建工厂的接口
public interface AbstractFactory {
//制造发动机
public Engine createEngine();
//制造空调
public Aircondition createAircondition();
}
//为宝马320系列生产配件
public class FactoryBMW320 implements AbstractFactory{
@Override
public Engine createEngine() {
return new EngineA();
}
@Override
public Aircondition createAircondition() {
return new AirconditionA();
}
}
//宝马523系列
public class FactoryBMW523 implements AbstractFactory {
@Override
public Engine createEngine() {
return new EngineB();
}
@Override
public Aircondition createAircondition() {
return new AirconditionB();
}
}
客户类:
//创建工厂的接口
public interface AbstractFactory {
//制造发动机
public Engine createEngine();
//制造空调
public Aircondition createAircondition();
}
//为宝马320系列生产配件
public class FactoryBMW320 implements AbstractFactory{
@Override
public Engine createEngine() {
return new EngineA();
}
@Override
public Aircondition createAircondition() {
return new AirconditionA();
}
}
//宝马523系列
public class FactoryBMW523 implements AbstractFactory {
@Override
public Engine createEngine() {
return new EngineB();
}
@Override
public Aircondition createAircondition() {
return new AirconditionB();
}
}
工厂模式小结:
1、工厂方法模式与抽象工厂模式的区别在于:
(1)工厂方法只有一个抽象产品类和一个抽象工厂类,但可以派生出多个具体产品类和具体工厂类,每个具体工厂类只能创建一个具体产品类的实例。
(2)抽象工厂模式拥有多个抽象产品类(产品族)和一个抽象工厂类,每个抽象产品类可以派生出多个具体产品类;抽象工厂类也可以派生出多个具体工厂类,同时每个具体工厂类可以创建多个具体产品类的实例
5.适配器模式
-
定义:适配器模式把一个类的接口变换成客户端所期待的另一种接口,从而使原本因接口不匹配而无法在一起工作的两个类能够在一起工作。
二种适配器:类的适配器模式、对象的适配器模式
5.1 适配器模式之类适配器模式
实现方式:让Adapter继承Adaptee类,然后再实现Target接口,来实现适配器功能。
- Adaptee:适配者类,它是需要被访问的、需要被适配的组件. AC220V
- Target:目标接口,当前系统业务所使用的接口,可以是抽象类或接口. DC5V
- Adapter:适配器类,通过继承和实现目标接口,让客户端按照目标接口的方法访问适配者
- Client:客户端,适配器的使用者
实例:手机充电需要将220V的交流电转化为手机锂电池需要的5V直流电。使用电源适配器,将 AC220v转换为 DC5V。
public class AC220v {
//输出220V电流
public int output(){
int output=220;
return output;
}
}
public interface DC5 {
//期待得到5V
int out5V();
}
//适配器
public class PowerAdapte extends AC220v implements DC5 {
//输出5V直流电
@Override
public int out5V() {
int output = output();
return (output / 44);
}
@Override
public String toString() {
return "" + out5V();
}
}
//测试类适配器
public class Test01 {
public static void main(String[] args) {
//类适配器模式
DC5 dc5 = new PowerAdapte();
System.out.println("输出电流"+dc5+"V");
}
}
优点:由于Adapter继承了Adaptee类,所以它可以根据需求重写Adaptee类的方法,使得Adapter的灵活性增强了。
缺点:因为java单继承的缘故,Target类==必须是接口==,以便于Adapter去继承Adaptee并实现Target,完成适配的功能,但这样就导致了Adapter里暴露了Adaptee类的方法,使用起来的成本就增加了
1.2 适配器模式之对象适配器模式
-
实现方式:让Adapter持有Adaptee类的实例,然后再实现Target接口,以这种持有对象的方式来实现适配器功能。
- `Adaptee`:适配者类,它是需要被访问的、需要被适配的组件
- `Target`:目标接口,当前系统业务所使用的接口,可以是抽象类或接口
- `Adapter`:适配器类,通过聚合和实现目标接口,让客户端按照目标接口的方法访问适配者
- `Client`:客户端,适配器的使用者
public class AC220v {
//输出220V电流
public int output(){
int output=220;
return output;
}
}
public interface DC5 {
//期待得到5V
int out5V();
}
public class PowerAdapte02 implements DC5{
//类适配器
private AC220v ac220v;
public PowerAdapte02(AC220v ac220v){
this.ac220v=ac220v;
}
@Override
public int out5V() {
int output=ac220v.output();
return (output/44);
}
}
public class Test01 {
public static void main(String[] args) {
AC220v ac220 = new AC220v();
//类适配器模式
PowerAdapte02 powerAdapte02 = new PowerAdapte02(ac220);
System.out.println(powerAdapte02.out5V());
}
}
-
优点:根据合成复用原则,使用组合替代继承, 所以它解决了类适配器必须继承Adaptee的局限性问题,也不再要求Target必须是接口。使用成本更低,更灵活。
6. 代理模式
6.1.代理模式是什么
代理模式的定义:对其他对象提供一种代理以控制对这个对象的访问。
代理的作用
代理模式的主要作用是为其他对象提供一种代理以控制对这个对象的访问。在某些情况下,一个对象不想或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用。
代理模式的思想是为了提供额外的处理或者不同的操作而在实际对象与调用者之间插入一个代理对象。这些额外的操作通常需要与实际对象进行通信。
代理模式是一种设计模式,简单说即是在不改变源码的情况下,实现对目标对象的功能扩展。
抽象角色(Subject):通过接口或抽象类声明真实角色实现的业务方法。
角色具有的行为放入该接口或抽象类总
代理角色(Proxy):实现抽象角色,是真实角色的代理,通过真实角色的业务逻辑方法来实现抽象方法,并可以附加自己的操作。
真实角色(RealSubject):实现抽象角色,定义真实角色所要实现的业务逻辑,供代理角色调用
例子:
比如有个歌手对象叫Singer,这个对象有一个唱歌方法叫sing()。
public class Singer{
public void sing(){
System.out.println("唱一首歌");
}
}
假如你希望,通过你的某种方式生产出来的歌手对象,在唱歌前后还要想观众问好和答谢,也即对目标对象
Singer的sing方法进行功能扩展
1 public void sing(){
2 System.out.println("向观众问好");
3 System.out.println("唱一首歌");
4 System.out.println("谢谢大家");
5 }
但是往往你又不能直接对源代码进行修改
,可能是你希望原来的对象还保持原来的样子,又或许你提供的只是一个可插拔的插件,甚至你有可能都不知道你要对哪个目标对象进行扩展。这时就需要用到java的代理模式了。网上好多用生活中的经理人的例子来解释“代理”,看似通俗易懂,但我觉得不适合程序员去理解。程序员应该从代码的本质入手
优点:代理模式可以屏蔽用户真正请求的对象,是用户程序和正在的对象解耦。使用代理来担当那些创建耗时的对象的替身。 一个用户不想或者不能够直接引用一个对象(或者设计者不希望用户直接访问该画图对象),而代理对象可以在客户端和目标对象之间起到中介的作用。而且这个代理对象中,我们可以做更多的操作。
代理分类:我们有多种不同的方式来实现代理。如果按照代理创建的时期来进行分类的话, 可以分为两种:静态代理、动态代理。静态代理是由程序员创建或特定工具自动生成源代码,在对其编译。动态代理是在程序运行时通过反射机制动态创建的。
6.2.静态代理
概述:静态代理就是写死了在代理对象中执行这个方法前后执行添加功能的形式,每次要在接口中添加一个新方法,则需要在目标对象中实现这个方法,并且在代理对象中实现相应的代理方法。
1 public interface ISinger {
2 void sing();
3 }
4
5 /**
6 * 目标对象实现了某一接口
7 */
8 public class Singer implements ISinger{
9 public void sing(){
10 System.out.println("唱一首歌");
11 }
12 }
13
14 /**
15 * 代理对象和目标对象实现相同的接口
16 */
17 public class SingerProxy implements ISinger{
18 // 接收目标对象,以便调用sing方法
19 private ISinger target;
20 public UserDaoProxy(ISinger target){
21 this.target=target;
22 }
23 // 对目标对象的sing方法进行功能扩展
24 public void sing() {
25 System.out.println("向观众问好");
26 target.sing();
27 System.out.println("谢谢大家");
28 }
29 }
1 /**
2 * 测试类
3 */
4 public class Test {
5 public static void main(String[] args) {
6 //目标对象
7 ISinger target = new Singer();
8 //代理对象
9 ISinger proxy = new SingerProxy(target);
10 //执行的是代理的方法
11 proxy.sing();
12 }
13 }
总结:其实这里做的事情无非就是,创建一个代理类SingerProxy,继承了ISinger接口并实现了其中的方法。只不过这种实现特意包含了目标对象的方法,正是这种特征使得看起来像是“扩展”了目标对象的方法。假使代理对象中只是简单地对sing方法做了另一种实现而没有包含目标对象的方法,也就不能算作代理模式了。所以这里的包含是关键。
缺点:这种实现方式很直观也很简单,但其缺点是代理对象必须提前写出,如果==接口层发生了变化==,代理对象的代码也要进行维护。如果能在运行时动态地写出代理对象,不但减少了一大批代理类的代码,也少了不断维护的烦恼,不过运行时的效率必定受到影响。这种方式就是接下来的动态代理。
6.3.动态代理(也叫JDK代理)
概述:动态代理是在程序运行时通过==反射机制动态创建==的
Proxy类:创建代理角色的方法。
InvocationHandler接口:提供的执行被代理角色方法的方法。
1 public interface ISinger {
2 void sing();
3 }
4
5 /**
6 * 目标对象实现了某一接口
7 */
8 public class Singer implements ISinger{
9 public void sing(){
10 System.out.println("唱一首歌");
11 }
12 }
这回直接上测试,由于java底层封装了实现细节,所以代码非常简单,格式也基本上固定。
调用Proxy类的静态方法newProxyInstance即可,该方法会返回代理类对象
static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces,InvocationHandler h ) 接收的三个参数依次为:
-
ClassLoader loader:指定当前目标对象使用类加载器,写法固定
-
Class<?>[] interfaces:目标对象实现的接口的类型,写法固定
-
InvocationHandler h:提供的执行被代理角色方法的方法。
1 public class Test{
2 public static void main(String[] args) {
3 Singer target = new Singer();
4 ISinger proxy = (ISinger) Proxy.newProxyInstance(
5 target.getClass().getClassLoader(),
6 target.getClass().getInterfaces(),
7 new InvocationHandler() {
8 @Override
9 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
10 System.out.println("向观众问好");
11 //执行目标对象方法
12 Object returnValue = method.invoke(target, args);
13 System.out.println("谢谢大家");
14 return returnValue;
15 }
16 });
17 proxy.sing();
18 }
19 }
缺点:可以看出静态代理和JDK代理有一个共同的缺点,就是目标对象必须实现一个或多个接口,如果没有,则可以使用Cglib代理。
6.4.Cglib代理
前提条件:
需要引入cglib的jar文件,由于Spring的核心包中已经包括了Cglib功能,所以也可以直接引入spring-core-3.2.5.jar
目标类不能为==final==
目标对象的方法如果为final/static,那么就不会被拦截,即不会执行目标对象额外的业务方法
/**
* 目标对象,没有实现任何接口
*/
public class Singer{
public void sing() {
System.out.println("唱一首歌");
}
}
/**
* Cglib子类代理工厂
*/
public class ProxyFactory implements MethodInterceptor{
// 维护目标对象
private Object target;
public ProxyFactory(Object target) {
this.target = target;
}
// 给目标对象创建一个代理对象
public Object getProxyInstance(){,
//1.工具类
Enhancer en = new Enhancer();
//2.设置父类---cglib---
en.setSuperclass(target.getClass());
//3.设置回调函数
en.setCallback(this);
//4.创建子类(代理对象)
return en.create();
}
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("向观众问好");
//执行目标对象的方法
Object returnValue = method.invoke(target, args);
System.out.println("谢谢大家");
return returnValue;
}
/**
* 测试类
*/
public class Test{
public static void main(String[] args){
//目标对象
Singer target = new Singer();
//代理对象
Singer proxy = (Singer)new ProxyFactory(target).getProxyInstance();
//执行代理对象的方法
proxy.sing();
}
}
总结:三种代理模式各有优缺点和相应的适用范围,主要看目标对象是否实现了接口。以Spring框架所选择的代理模式举例
在Spring的AOP编程中:
如果加入容器的目标对象有实现接口,用JDK代理
如果目标对象没有实现接口,用Cglib代理
7.观察者模式
观察者(Observer)模式的定义:指多个对象间存在一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。这种模式有时又称作发布-订阅模式、模型-视图模式,它是对象行为型模式。
7.1 结构
观察者模式的主要角色如下。
-
抽象主题(Subject)角色:也叫抽象目标类,它提供了一个用于保存观察者对象的聚集类和增加、删除观察者对象的方法,以及通知所有观察者的抽象方法。
-
具体主题(Concrete Subject)角色:也叫具体目标类,它实现抽象目标中的通知方法,当具体主题的内部状态发生改变时,通知所有注册过的观察者对象。
-
抽象观察者(Observer)角色:它是一个抽象类或接口,它包含了一个更新自己的抽象方法,当接到具体主题的更改通知时被调用。
-
具体观察者(Concrete Observer)角色:实现抽象观察者中定义的抽象方法,以便在得到目标的更改通知时更新自身的状态。
7.2. 实现
观察者模式首先有一个被观察类,这个类中有一个 ArrayList 存放观察者们。除此以外还应有类状态和设置和获取状态的方法,状态改变时通知所有观察者,观察者类可以有个抽象类,所有的观察者类继承这个抽象类,观察者类有它要观察的对象。
7.4. 例子
需求:做一个案例,公众号推送消息,关注这个公众号的客户可以接收到消息。
1. 抽象主题---抽象公共号
1. 关注
2. 取消关注
3. 设置推送的消息
4. 群发
2. 具体主题--具体公众号类AAA
3. 抽象观察者---抽象用户
1. 接受消息
分析:
观察者模式分为四个角色:
1.抽象被观察者角色
抽象出一个公众号接口/抽象类
公共的方法:
关注方法
取关方法
设置推送消息方法
群发方法2.被观察者角色
有一个具体的公众号,实现/继承 抽象被观察者3.抽象观察者角色
抽象出一个客户 接口/抽象类
公共方法:
接收公众号推送的消息
4.观察者角色
具体用户 实现/继承 抽象观察者
抽象观察者角色:
public interface UserInterface {
/**
* 接受公众号发生的消息
* @param msg
*/
public void getMsg(String msg);
}
观察者角色:
public class User implements UserInterface {
private String name;
public User(String name) {
this.name = name;
}
public void getMsg(String msg) {
System.out.println(name+"接受到公众号发送的消息:"+msg);
}
}
抽象公众号:
public interface PublicHaoInterface {
public void add(UserInterface userInterface);
public void remove(UserInterface userInterface);
public void setMsg(String msg);
public void qunfa();
}
具体公共号:
public class AAAPublicHao implements PublicHaoInterce {
List<UserInterface> list=new ArrayList<UserInterface>();
//设置消息
private String msg;
public void add(UserInterface userInterface) {
list.add(userInterface);
}
public void remove(UserInterface userInterface) {
list.remove(userInterface);
}
public void setMsg(String msg) {
this.msg=msg;
}
public void qunfa() {
for(UserInterface userInterface:list){
userInterface.getMsg(msg);
}
}
}
7.5. 优缺点
观察者模式是一种对象行为型模式,其主要优点如下。
降低了目标与观察者之间的耦合关系,两者之间是抽象耦合关系。 目标与观察者之间建立了一套触发机制。
它的主要缺点如下。
目标与观察者之间的依赖关系并没有完全解除,而且有可能出现循环引用。 当观察者对象很多时,通知的发布会花费很多时间,影响程序的效率。
8.模板方法
8.1 概述
在模板模式(Template Pattern)中,一个抽象类公开定义了执行它的方法的方式/模板。它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行。这种类型的设计模式属于行为型模式。
8.2 意图
定义一个操作中的==算法的骨架==,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定功能。
8.3 炒菜案例:
分析:
1.取材
2.处理食材
3.起锅烧油
4.放入食材
5.翻炒
6.放入各种调料
7.翻炒均匀
8.出锅
8.4 代码
炒菜接口:
public abstract class ChaoCai {
//不能被子类重写
public final void chaocai(){
quShiCai();
handle();
qiGuoShaoYou();
putShiCai();
fanChao();
putTiaoLiao();
junYun();
outGuo();
}
//1.取食材
void quShiCai(){};
//2.处理食材
void handle(){};
//3.起锅烧油
void qiGuoShaoYou(){};
//4.放入食材
void putShiCai(){};
//5.翻炒
void fanChao(){};
//6.放入各种调料
void putTiaoLiao(){};
//7.翻炒均匀
void junYun(){};
//8.出锅
void outGuo(){};
}
番茄炒菜
public class FanQieChaoDan extends ChaoCai{
@Override
void quShiCai() {
System.out.println("取鸡蛋和番茄");
}
@Override
void handle() {
System.out.println("处理鸡蛋和番茄");
}
@Override
void qiGuoShaoYou() {
System.out.println("起锅烧油");
}
@Override
void putShiCai() {
System.out.println("翻入鸡蛋和番茄");
}
@Override
void fanChao() {
System.out.println("翻炒");
}
@Override
void putTiaoLiao() {
System.out.println("翻入调料");
}
@Override
void junYun() {
System.out.println("翻炒均匀");
}
@Override
void outGuo() {
System.out.println("出锅!");
}
}
测试
public class ChaoCaiTest {
public static void main(String[] args) {
FanQieChaoDan fanQieChaoDan = new FanQieChaoDan();
fanQieChaoDan.chaocai();
}
}
8.5优点:
1、封装不变部分,扩展可变部分。
2、提取公共代码,便于维护。
3、行为由父类控制,子类实现。
缺点:每一个不同的实现都需要一个子类来实现,导致类的个数增加,使得系统更加庞大。
使用场景:
1、有多个子类共有的方法,且逻辑相同。
2、重要的、复杂的方法,可以考虑作为模板方法。
注意事项:为防止恶意操作,一般模板方法都加上 final 关键词。
9. 策略模式
9.1背景
在开发中经常遇到这种情况,实现某个功能有多种算法策略,我们可以根据不同环境或者条件选择不同的算法策略来完成该功能,比如查找、排序等,一种常用方式是硬编码在一个类中,如需要提供多种查找算法,可以将这些算法写到一个类中,在该类中提供多个方法,每一个方法对应一个具体的查找算法;当然也可以将这些查找算法封装在一个统一的方法中,通过 if-else 或者 case 等条件判断语句来进行选择。但是如果需要增加新的算法策略,就需要修改封装算法类的源代码;更换查找算法,也需要修改客户端的调用代码。并且在这个类中封装了大量算法,也会使得该类代码较复杂,维护较为困难。如果我们将这些策略包含在客户端,这种做法更不可取,将导致客户端程序庞大而且难以维护,如果存在大量可供选择的算法时问题将变得更加严重。
如何让算法和对象分开来,使得算法可以独立于使用它的客户而变化?解决方法就是使用策略模式。
9.2 什么是策略模式:
将类中经常改变或者可能改变的部分提取为作为一个抽象策略接口类,然后在类中包含这个对象的实例,这样类实例在运行时就可以随意调用实现了这个接口的类的行为。
比如定义一系列的算法,把每一个算法封装起来,并且使它们可相互替换,使得算法可独立于使用它的客户而变化,这就是策略模式。
(1)环境类(Context):通过 ConcreteStrategy 具体策略类来配置,持有 Strategy 对象并维护对Strategy 对象的引用。可定义一个接口来让 Strategy 访问它的数据。
(2)抽象策略类(Strategy):定义所有支持的算法的公共接口。 Context使用这个接口来调用某ConcreteStrategy 定义的算法。
(3)具体策略类(ConcreteStrategy): Strategy 接口的具体算法。
9.3 代码
促销活动。
public class CuXiaoContext {
private CuXiao cuXiao;
public CuXiaoContext(CuXiao cuXiao) {
this.cuXiao = cuXiao;
}
//定义一个使用算法的方法
public void use(){
cuXiao.cuxiao();
}
}
public interface CuXiao {
void cuxiao();
}
public class SuanFa618 implements CuXiao {
@Override
public void cuxiao() {
System.out.println("使用6.18的促销算法");
}
}
public class SuanFa1111 implements CuXiao{
@Override
public void cuxiao() {
System.out.println("使用11.11的促销算法");
}
}
public class SuanFa1212 implements CuXiao{
@Override
public void cuxiao() {
System.out.println("使用12.12的促销算法");
}
}
public class CuXiaoTest {
public static void main(String[] args) {
//SuanFa618 suanFa618 = new SuanFa618();
SuanFa1111 suanFa1111 = new SuanFa1111();
CuXiaoContext context = new CuXiaoContext(suanFa1111);
context.use();
}
}
策略模式优缺点:
优点:
1、算法可以自由切换(策略类自由切换)。
2、避免使用多重条件判断。
3、扩展性良好(符合开闭原则)。
缺点:
1、策略类会增多。
2、所有策略类都需要对外暴露。
3、客户端必须知道所有的策略类,才能确定要调用的策略类。
策略模式使用场景:
如果在一个系统里面有许多类,它们之间的区别仅在于它们的行为,那么使用策略模式可以动态地让一个对象在许多行为中选择一种行为。
一个系统需要动态地在几种算法中选择一种。
如果一个对象有很多的行为,如果不用恰当的模式,这些行为就只好使用多重的条件选择语句来实现。