设计模式学习笔记

Posted by     "Eric" on Wednesday, December 18, 2019

设计模式归根到底都是多态的变种

一、Singleton单例模式

只需要一个实例存在,比如PropertyMgr,即阻止别人随意new该对象

Key: 把构造方法设为私有的

做成单例之后,其他类使用该类可以直接拿来用,不用作为属性引入,进一步降低了耦合度

耦合度级别:继承、聚合(作为属性)、关联(出现在方法中)

/**
*饿汉式
*类加载到内存后,就实例化一个单利,JVM保证是线程安全的
*简单实用 推荐
*唯一缺点:不管你用不用,都会被加载 Class.forName() 因为INSTANCE是静态变量,所以会被执行
**/
public class Mgr01{
    // 虚拟机会保证一个类的<clinit>方法在多线程环境中能够被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程执行<clinit>()方法完毕。
    private static final Mgr01 INSTANCE =new Mgr01();
    private Mgr01(){};
    public static Mgr01 getInstance(){ return INSTANCE; }
}
/**
*懒汉式
*克服了饿汉式,但线程不安全
**/
public class Mgr03{
    private Mgr03(){};
    private static Mgr03 INSTANCE;
    public static Mgr03 getInstance(){
        if(INSTANCE==null)
            INSTANCE=new Mgr03();
        return INSTANCE;
    }
}
/**
*改进懒汉式
*效率降低
**/
public class Mgr04{
    private Mgr04(){};
    private static Mgr04 INSTANCE;
    public static synchronized Mgr04 getInstance(){
        if(INSTANCE==null)
            INSTANCE=new Mgr03();
        return INSTANCE;
    }
}
/**
*尽可能减小同步块,提高运行速度
*较完美的写法
**/
public class Mgr06{
    private Mgr04(){};
    private static Mgr04 INSTANCE;
    public static Mgr04 getInstance(){
        if(INSTANCE==null){ //双锁检查
            synchronized(Mgr04.class){
                if(INSTANCE==null)
                    INSTANCE=new Mgr06;
            }
        }
        return INSTANCE;
    }
}
/**
*静态内部类的写法
*最完美写法
*加载Mgr07时,并不会加载其内部类
**/
public class Mgr07{
    private Mgr07(){};
    private static class Mgr07Holder{
        private final static Mgr07 INSTANCE=new Mgr07();
    }
    public static Mgr07 getInstance(){
        return Mgr07Holder.INSTANCE;
    }
}
/**
*完美中的完美
*枚举单例
**/
public enum Mgr08{
    INSTANCE;
    private Mgr08(){};
}

二、Strategy策略模式

策略模式有两点主要思想,其一是将变化部分与不变部分分离,其二是面向接口编程而不是面向实现编程,以《Head First设计模式》中的例子,可以比较清晰的阐述清楚以上两点。

例如我们我们收到一个客户要求,需要生产各种鸭子(绿头鸭、唐老鸭、可达鸭…),我们自然而然的就想到了,首先设计一个鸭子的超类,其具有一些鸭子共有的属性以及行为,然后让其他的子类对其进行继承,但此时遇到一个问题,并非所有的鸭子都会飞,并且每一种鸭子的叫声也不一样。为了解决这个问题,我们只能不断的改写每一个鸭子子类中的方法,更大的问题是,当有新的鸭子产品继承自鸭子超类时,都要被迫检查可能需要覆盖的fly()和quark()。

3o4cBn.png

然后我们又想到,能否创建两个接口Flyable和Quackable,然后让鸭子的子类根据实际情况去继承这些接口们,然后各自实现接口的功能。

3o4fhT.png

显然,这也不是什么好主意,为了实现每一种接口,需要重复的代码更多,那如何从根本上解决这一问题呢?

鸭子类中会发生变化的只有fly()和quack()这两类行为,策略设计模式中有这样一条设计原则,**找到应用中可能需要变化之处,把它们独立出来,不要和哪些不需要变化的代码混在一起。**于是,我们将这两种行为从鸭子类中分离出来。

接下来又是新的问题,这两种被分离出来的行为要以何种的形式存在呢,以及鸭子类如何与这两类行为发生联系?此时就引出了第二条设计原则,**针对接口编程,而不是针对实现编程。**这句话更确切的说是“变量的声明类型应该是超类型,通常是一个抽象类或者是一个接口,如此,只要是具体实现此超类型的类所产生的对象,都可以指定给这个变量”

在Duck类中,我们利用接口代表每一种行为,而不是将行为的实现(即函数)绑定其中。既然在Duck类中,我们用接口代表每种行为,那么被分离出来的行为当然是以接口的形式存在,比如FlyBehavior和QuackBehavior。然后不同飞的方式(包括不能飞),不同叫的方式都可以作为这两种接口的实现类,然后让Duck中的行为接口指向这些实现类即可

3o453F.png

所以话说回来,到底什么是策略模式呢?以及在何时使用该模式呢?我的理解是当完成一件事情可以有多种策略(或者说成方法更好理解)时,就可以使用策略模式,其主要步骤是将该行为分离抽象为接口,完成该方法的不同策略实现这个接口,然后在拥有该行为的类中声明该接口的变量,指向实际的类。

举个例子,在学Java基础时,我们一定对Comparator以及Comparable这两个比较器接口不陌生,但是身边很多同学不理解这两个接口到底有什么区别。我个人的理解,Comparable就像是Flyable或者Quackable这类接口,我们需要为每一种需要比较的类写其实现,行为的实现还是和类牢牢的绑定在一起。而Comparator则是真真切切使用了策略模式的思想,关于比较这一行为,有不同的策略,拿人来说,可以以高矮进行比较,也可以拿胖瘦进行比较,我们完全可以通过实现Comparator这个接口,实现这些策略,然后传入到Sort类中进行比较,这大大提高了程序的弹性,这也就是策略模式的魅力所在

public interface Comparable<T> {
    int compareTo(T o);
}

public interface Comparator<T> {
    int compare(T o1, T o2);
}

三、工厂模式

任何可以产生对象的方法或类,都可以称之为工厂。单例也是一种工厂,为什么有了new之后,还要有工厂?是为了灵活控制生产过程,权限、修饰、日志等。

1.简单工厂

3o4LAx.png

简单工厂更像是一种编程习惯,由于在Pizza店中,pizza的种类是会随时改变的,而Pizza的制作流程(烘烤、切片、包装)是不变的,因此一种显而易见的方法就是将改变的部分分离封装起来。使其变成一种工厂。

2.工厂方法

定义了创建对象的接口,但由子类决定实例化哪个类,工厂方法把实例化操作推迟到子类。

下图中,Factory 有一个 doSomething() 方法,这个方法需要用到一个产品对象,这个产品对象由 factoryMethod() 方法创建。该方法是抽象的,需要由子类去实现。

3o4x3D.png

public abstract class Factory {
    public abstract Product factoryMethod();
    public void doSomething() {
        Product product = factoryMethod();
        // do something with the product (产品的使用)
    }
}
public class ConcreteFactory extends Factory{
    @Override
    public Product factoryMethod(){
        return new ConcreteProduct();
    }
}

可以看到,工厂方法帮助我们将产品的“实现”从“使用”中解耦,如果增加产品或者改变产品的实现,并不会影响到产品的使用。

观察下面这幅图,这幅图表明了这三个类之间的依赖关系。我们可以发现不管是高层组件,还是低层组件全都依赖于抽象类,这也就是依赖倒置的设计原则——“要依赖抽象,不要依赖具体类”。

在面向过程的编程中,上层调用下层,上层依赖于下层,当下层剧烈变动时上层也要跟着变动,这就会导致模块的复用性降低而且大大提高了开发的成本。

面向对象的开发很好的解决了这个问题,一般情况下抽象的变化概率很小,让用户程序依赖于抽象,实现的细节也依赖于抽象。即使实现细节不断变动,只要抽象不变,客户程序就不需要变化。这大大降低了客户程序与实现细节的耦合度。

3o5PHI.png

3.抽象工厂

抽象工厂提供一个接口,用于创建一类产品族。

3o5mvQ.png

public interface AbstractFactory{
    AbstractProductA createProductA();
    AbstractProductB createProductB();
}
public ConcreteFactory1 implements AbstractFactory{
    AbstractProductA createProductA(){ return new ProductA1(); }
    AbstractProductB createProductB(){ return new ProductB1(); }
}

抽象工厂的每个方法实际上看起来都是工厂方法。每个方法都声明为抽象的,而子类的方法覆盖这些方法来创建某些对象。

四、责任链

3o5KDs.png

创建一个责任链类,该类也继承自Handler接口,然后其中含有一个List集合,用来存储各种ConcreteHandler,这样对于一个请求,责任链中的每一个对象都可以依次检查请求,并进行处理,或者将它传给链中的下一个对象。

五、调停者

用调停者来集中相关对象之间复杂的沟通和控制方式,例如Bob打盹的时候会点击打盹按钮,他的闹钟开始告诉咖啡壶煮咖啡;等咖啡煮完再……,总之就是各个对象之间紧耦合,彼此之间有着错综复杂的关系

3o5Mbn.png

在这个系统中加入一个调停者,每个对象都会在自己的状态改变时,告诉调停者;每个对象也都会对调停者所发出的请求作出回应。中介者中包含着所有的控制逻辑。

六、装饰者

装饰者模式通常用在给某些类做“装饰”时,比如给坦克类加血条、加等级显示,给咖啡类加各种调料。以咖啡为例,咖啡中会有多种调料,比如糖、奶泡、摩卡…如果给添加了不同调料的咖啡创建类并继承自咖啡类,将会出现非常多的类:摩卡奶泡卡布奇诺,奶泡卡布奇诺,摩卡卡布奇诺…我们称之为类爆炸。为了避免这一情况,我们使用装饰者设计模式。

3o5J8U.png

装饰者模式的设计要点:

  1. 装饰者和被装饰者有相同的超类型
  2. 装饰者持有内层实例对象
  3. 重新方法,可以在内层对象的行为前/后加上自己的行为

在Java源码中,最典型的使用装饰者的例子是Java I/O

3o50V1.png

3o562D.png

另外举个例子,我们在设计project时,总是要自己定义一些异常类,这里我们常常会使用装饰者模式,实现代码和类关系如下所示

定义接口

public interface CommonError {
    public int getErrorCode();
    public String getErrorMsg();
    public CommonError setErrorMsg(String errMsg);
}

定义本体,本体是一个枚举类,对于枚举类的理解可以看这个博客https://www.cnblogs.com/panchanggui/p/10318368.html,其核心是,枚举类中可以定义一些成员对象。

public enum EmBusinessError implements CommonError{
    // 通用错误类型00001
    PARAMETER_VALIDATION_ERROR(10001,"参数不合法"),

    UNKNOWN_ERROR(10002,"未知错误"),

    // 10000开头为用户信息相关错误定义
    USER_NOT_EXIST(20001,"用户不存在"),

    STOCK_NOT_ENOUGH(30001,"库存不足"),

    USER_LOGIN_FAIL(20002,"用户手机号或密码不正确");

    private EmBusinessError(int errCode,String errMsg){
        this.errCode=errCode;
        this.errMsg=errMsg;
    }

    private int errCode;
    private String errMsg;
    
    @Override
    public int getErrorCode() {return errCode;}

    @Override
    public String getErrorMsg() {return errMsg;}

    @Override
    public CommonError setErrorMsg(String errMsg) {
        this.errMsg=errMsg;
        return this;
    }
}

定义装饰器,实现相同的接口,可以调用接口方法。

public class BusinessException extends Exception implements CommonError {

    private CommonError commonError;

    // 直接接收EmBusinessError的传参用于构造异常异常
    public BusinessException(CommonError commonError){
        super();
        this.commonError=commonError;
    }

    // 接收自定义errMsg的方式构造业务异常
    public BusinessException(CommonError commonError,String errMsg){
        super();
        this.commonError=commonError;
        this.commonError.setErrorMsg(errMsg);
    }

        @Override
    public int getErrorCode() {return this.commonError.getErrorCode();}

    @Override
    public String getErrorMsg() {return this.commonError.getErrorMsg();}

    @Override
    public CommonError setErrorMsg(String errMsg) {
        this.commonError.setErrorMsg(errMsg);
        return this;
    }
}

image-20200525000715147

七、代理模式

代理模式为另一个对象提供一个替身或占位符以控制对这个对象的访问,被代理的对象可以是远程的对象、创建开销大的对象或需要安全控制的对象。

以租房为例,我们能更好的理解代理模式。

企业微信截图_15880804648754

角色分析:

  • 抽象角色:一般会使用接口或者抽象类来解决
  • 真实角色:被代理的角色
  • 代理角色:代理真实角色,代理真实角色后,我们一般会做一些附属操作
  • 客户:访问代理对象的人

代理角色代表了真实角色,这两者拥有共同的动作——租房,而客户直接接触到的是代理对象中介,而不是房东;另外中介帮被代理者——房东做了许多事情,比如签合同、装修等等,相当于扩展了租房这个动作。

代理模式根据有无代理类分为静态代理和动态代理。所谓静态代理,就是自己写代理类,而动态代理的代理类则是在程序运行过程总动态生成的,动态代理的主流方法有jdk动态代理和cglib动态代理;而根据实现方式代理又可以分为继承和聚合。

方法1:继承

public class Host{
    public void rent(){
        ...
    }
}

public class Proxy extends Host{
    
    @Override
    public void rent(){
        //增强操作
        xxx;
        super.rent();
    }
}

方法2:聚合

public interface Rent{
    public void rent();
}

public class Host implements Rent{
    public void rent(){
        ...
    }
}

public class Proxy implements Rent{
    Rent rent;
    public void rent(){
        //增强操作
        xxx;
        rent.rent();
    }
}

聚合的方式更加灵活,因为我们可以灵活的注入被代理类。

Java.lang.reflect.Proxy类实现了一种代理模式,我们称之为动态代理,那什么是动态代理呢

  • 动态代理和静态代理角色一样
  • 动态代理的代理类是动态生成的,不是我们直接写好的
  • 动态代理分为两大类:基于接口的动态代理,基于类的动态代理

说的具体一点,我们知道一个类被加载首先需要有一个类的.java文件,然后经过编译写成.class,然后类加载器将其加载。在静态代理中,代理类的.java文件是要手动写的,而在动态代理中,我们不需要手动写代理类,而是调用了Proxy的一个静态方法newProxyInstance

Proxy提供了用于创建动态代理实例的静态方法。该类中使用一个静态方法newProxyInstance返回代理对象。该方法需要传入三个参数。

public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException
//loader为一个ClassLoader对象,定义了由哪个ClassLoader对象来对生成的代理对象进行加载
//interfaces:为一个Interface对象的数组,表示的是我将要给我需要代理的对象提供一组什么接口,如果我提供了一组接口给它,那么这个代理对象就宣称实现了该接口(多态),这样我就能调用这组接口中的方法了
//h为一个InvocationHandler对象,表示的是当我这个动态代理对象在调用方法的时候,会关联到哪一个InvocationHandler对象上

InvocationHandler的工作是响应代理的任何调用,可以把InvocationHandler想象成代理收到方法调用后,请求做实际工作的对象。

//InvocationHandler中只有一个方法
Object invoke(Object proxy, Method method, Object[] args);
//proxy为调用方法的代理对象
//method是调用方法的一个反射
//args则是调用方法的参数
//InvocationHandler将请求转发给RealSubject,因此实现InvocationHandler的类中应该持有被代理的对象

image-20200828103600569

设计动态代理类

public class ProxyInvocationHandler implements InvocationHandler {
    
    private Rent rent;
    public void setRent(Rent rent) {
        this.rent = rent;
    }

    public Object getProxy(){
        return Proxy.newProxyInstance(this.getClass().getClassLoader(),
                                      rent.getClass().getInterfaces(),this);
    }
    
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        seeHouse();
        fare();
        Object invoke = method.invoke(rent, args);
        return invoke;
    }

    public void seeHouse(){
        System.out.println("中介带看");
    }

    public void fare(){
        System.out.println("收中介费");
    }
}

Proxy是怎么做到动态代理的呢,其实是因为代理类的内容是非常固定,Proxy通过自己生成一个.java文件,然后编译,最后加载到内存,便可以返回一个代理对象,Proxy的实现原理大致如下所示

public class LubanProxy {

	public static Object getInstance(Object target) throws MalformedURLException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
//==========================生成代理类内容===============================
		Class clazz = target.getClass().getInterfaces()[0];
		String infName = clazz.getSimpleName();
		String content = "";
		String line = "\n";//换行
		String tab = "\t";//tab
		String packageContent = "package com.baidu;" + line;
		String importContent = "import " + clazz.getName() + ";" + line;
		String clazzFirstLineContent = "public class $ProxyLuban implements " + infName + "{" + line;
		String filedContent = tab + "private " + infName + " target;" + line;

		String constructorContent = tab + "public $ProxyLuban (" + infName + " target){" + line
				+ tab + tab + "this.target =target;"
				+ line + tab + "}" + line;


		String methodContent = "";
		Method[] methods = clazz.getDeclaredMethods();

		for (Method method : methods) {
			//String
			String returnTypeName = method.getReturnType().getSimpleName();
			//query
			String methodName = method.getName();
			// [String.class===class]
			Class args[] = method.getParameterTypes();
			String argsContent = "";
			String paramsContent = "";
			int flag = 0;
			for (Class arg : args) {
				//String
				String temp = arg.getSimpleName();
				//String
				//String p0
				argsContent += temp + " p" + flag + ",";
				//p0
				paramsContent += "p" + flag + ",";
				flag++;
			}
			if (argsContent.length() > 0) {
				argsContent = argsContent.substring(0, argsContent.lastIndexOf(",") - 1);
				paramsContent = paramsContent.substring(0, paramsContent.lastIndexOf(",") - 1);
			}
			//public String query(String p0){
			//		System.out.println("log");
			//		target.query(p0);
			//	}

			methodContent += tab + "public " + returnTypeName + " " + methodName + "(" + argsContent + ") {" + line
					+ tab + tab + "System.out.println(\"log\");" + line
					+ tab + tab + "target." + methodName + "(" + paramsContent + ");" + line
					+ tab + "}" + line;

		}

		content += packageContent + importContent + clazzFirstLineContent + filedContent + constructorContent + methodContent + "}";

//=========================写入到.java文件中===============================
		File file = new File("d:\\com\\baidu\\$ProxyLuban.java");

		try {
			if (!file.exists()) {
				file.createNewFile();
			}

			FileWriter fw = new FileWriter(file);
			fw.write(content);
			fw.flush();
			fw.close();
		} catch (Exception e) {

		}

//=========================动态编译===============================
		JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();

		StandardJavaFileManager fileMgr = compiler.getStandardFileManager(null, null, null);
		Iterable units = fileMgr.getJavaFileObjects(file);

		JavaCompiler.CompilationTask t = compiler.getTask(null, fileMgr, null, null, null, units);
		t.call();

		//Class.forName()
		//fileMgr.close();
//=========================使用类加载器加载到内存===============================
		URL[] urls = new URL[]{new URL("file:D:\\\\")};
		URLClassLoader urlClassLoader = new URLClassLoader(urls);
		Class cls = urlClassLoader.loadClass("com.baidu.$ProxyLuban");
		Constructor constructor = cls.getConstructor(clazz);
		Object o  = constructor.newInstance(target);

		return  o;

	}
}