Java设计模式(简易版)

第 1 章 单例模式

1.1 单例模式

1.1.1 什么是单例模式

所谓单例,指的就是单实例,有且仅有一个类的实例,该类提供了一个全局访问点供外部获取该实例,这个单例不应该由人来控制,而由该类负责创建自己的对象,同时确保只有单个对象被创建。

在创建实例时,检查系统是否已经存在该单例,如果存在则返回该实例,否则创建一个新实例。

总结:单例类只能有一个实例,单例类必须自己创建自己的唯一实例。

1.1.2 为什么要用单例模式

单例有其独有的使用场景,一般是对于那些业务逻辑上限定不能多例只能单例的情况:如,在多进程多线程环境下操作文件时,避免多个进程或线程同时操作一个文件,需要通过唯一实例进行处理。WEB 中的计数器,一般需要使用一个实例来进行记录,若多例计数则会不准确。

使用单例模式的好处是内存中只有一个实例,减少内存开销,避免频繁创建和销毁实例。

1.1.3 单例模式要素

单例模式包含以下几个主要角色:

(1)单例类:包含单例实例的类,通常将构造函数声明为私有。

(2)静态成员变量:用于存储单例实例的静态成员变量。

(3)获取实例方法:静态方法,用于获取单例实例。

(4)私有构造函数:防止外部直接实例化单例类。

(5)线程安全处理:确保在多线程环境下单例实例的创建是安全的。

1.1.4 单例模式的分类

单例模式主要有以下三种模式:

(1)懒汉式:这种方式是最基本的实现方式,这种实现最大的问题就是必须加锁 synchronized 才能保证单例,但加锁会影响效率。

(2)饿汉式:这种方式是基于 classloader 机制避免了多线程的同步问题,没有加锁,执行效率会提高,但没有达到 lazy loading 的效果,类加载时就初始化,浪费内存。

(3)静态内部类/登记式:这种方式同样利用了 classloader 机制来保证初始化 instance 时只有一个线程,它与饿汉式不同的是:饿汉式只要 Singleton 类被装载了,那么 instance 就会被实例化(没有达到 lazy loading 效果),而这种方式是 Singleton 类被装载了,instance 不一定被初始化。因为内部类没有被主动使用,只有通过显式调用 getInstance 方法时,才会显式装载内部类,从而实例化 instance。

1.2 单例模式的设计

1.2.1 懒汉模式设计

懒汉式必须加锁 synchronized 才能保证单例。

/**

  • 懒汉式单例

*/

public class LazySingleton {

    // 用于存储单例实例的静态成员变量

    private static LazySingleton instance;

    // 构造方法必须私有化

    private LazySingleton(){}

    // 静态方法,用于获取单例实例,保在多线程环境下单例实例的创建是安全的

    public static synchronized LazySingleton getInstance() {

        // 如果不存在单例对象,创建对象,否则就返回已有对象

        if (instance == null) {

            instance = new LazySingleton();

        }

        return instance;

    }

    /**

     * 测试

     */

    public static void main(String[] args) {

        // 在多线程下创建单例

        for(int i = 0;i < 10;i++){

            new Thread(()->{

                LazySingleton instance = LazySingleton.getInstance();

                System.out.println(instance);

            }).start();

        }

    }

1.2.2 饿汉模式设计

饿汉式基于 classloader 机制避免了多线程的同步问题,但没有达到 lazy loading 的效果,类加载时就初始化,浪费内存。

/**

  • 饿汉式单例

*/

public class HungrySingleton {

    // 用于存储单例实例的静态成员变量,很明显,一进来就加载对象,存在浪费空间

    private final static HungrySingleton instance = new HungrySingleton();

    // 构造方法必须私有化

    private HungrySingleton(){}

    // 静态方法,用于获取单例实例,保在多线程环境下单例实例的创建是安全的

    public static HungrySingleton getInstance() {

        return instance;

    }

    /**

     * 测试

     */

    public static void main(String[] args) {

        // 在多线程下创建单例

        for(int i = 0;i < 10;i++){

            new Thread(()->{

                HungrySingleton instance = HungrySingleton.getInstance();

                System.out.println(instance);

            }).start();

        }

    }

}

1.2.3 登记式/静态内部类模式设计

这种方式同样利用了 classloader 机制来保证初始化 instance 时只有一个线程,它跟饿汉式不同的是:饿汉式只要 Singleton 类被装载了,那么 instance 就会被实例化(没有达到 lazy loading 效果),而这种方式是 Singleton 类被装载了,instance 不一定被初始化。因为内部类没有被主动使用,只有通过显式调用 getInstance 方法时,才会显式装载内部类,从而实例化 instance。

/**

  • 静态内部类单例

*/

public class HolderSingleton {

    // 静态内部类用于存储单例实例的静态成员变量

    private static class holder{

        private static final HolderSingleton INSTANCE = new HolderSingleton();

    }

    // 构造方法必须私有化

    private HolderSingleton(){}

    // 静态方法,用于获取单例实例,保在多线程环境下单例实例的创建是安全的

    public static HolderSingleton getInstance() {

        return holder.INSTANCE;

    }

    /**

     * 测试

     */

    public static void main(String[] args) {

        // 在多线程下创建单例

        for(int i = 0;i < 10;i++){

            new Thread(()->{

                HolderSingleton instance = HolderSingleton.getInstance();

                System.out.println(instance);

            }).start();

        }

    }

}

总结:一般情况下,不建议使用懒汉方式,建议使用饿汉方式。只有在要明确实现 lazy loading 效果时,才会使用登记方式。

第 2 章 工厂模式

2.1 工厂模式

2.2.1 什么是工厂模式

工厂模式是一种创建对象的设计模式,它提供了一种方式来封装对象的创建逻辑, 并根据不同的需求,返回相应的对象实例。工厂模式使得客户端代码与具体对象的创建过程解耦(将代码中的关联性和依赖性降低,使得各个模块之间的耦合度减少),提高了代码灵活性和可维护性。

总结:工厂模式是通过封装对象创建的过程,提供了一种统一的接口来创建对象。 它隐藏了对象的具体实现细节(抽象),提供了灵活性、可扩展性,并提高了代码的可读性和可维护性。

2.2.2 为什么需要工厂模式

1、工厂模式的作用

工厂模式的主要目的是解决以下问题:

(1)将对象的创建逻辑与使用代码分离,使得使用代码不需要关心对象具体如何创建,即抽象。

(2)提供一种简单的扩展机制,使得新增一种产品时不需要修改现有的代码,只需扩展工厂类即可,即多态。

工厂模式的核心思想是引入一个抽象的工厂接口或抽象类,该接口或抽象类定义了创建对象的方法,具体的对象创建由实现该接口或抽象类的具体工厂类来完成。

2、工厂模式的优点

使用工厂模式,具有以下优点:

(1)封装对象的创建过程:工厂模式将对象的创建过程封装在工厂类中,使得客户端无需关注对象的具体创建细节,只需通过工厂类获取所需对象。

(2)解耦客户端与具体产品:户端只需要通过工厂接口来创建对象,而无需直接实例化具体产品类,从而降低了客户端与具体产品的耦合度。

(3)提供灵活性和可扩展性:通过新增具体工厂类和产品类,可以方便地扩展工厂模式,增加新的产品类型,而不需要修改已有代码。

(4)代码可读性和可维护性:工厂模式提供了清晰的代码结构,使得代码更易于理解、维护和测试。

3、工厂模式的缺点

工厂模式存在以下几点不足:

(1)增加了系统的复杂性:引入工厂类和抽象产品类会增加系统的类和对象数量,增加了系统的复杂性。

(2)增加了开发的初期成本:工厂模式需要定义抽象工厂类和具体工厂类,以及相应的产品类,这在开发的初期会增加一定的开发成本。

(3)难以支持新种类的产品:如果需要添加新种类的产品,除了新增具体产品类外,还需要修改抽象工厂接口和所有具体工厂类的实现,可能会导致较大的修改范围。

总结:工厂模式在提供灵活性、可扩展性和代码可读性方面具有优点,但也会增加系统的复杂性和开发成本。在设计时需要权衡使用工厂模式的利弊,并根据具体情况选择是否使用该模式。

2.2.3 工厂模式的分类

工厂模式包含很多,常见的有以下三种模式:

(1)简单工厂模式(Simple Factory Pattern):简单工厂模式通过一个工厂类来封装对象的创建逻辑,根据客户端传入的参数返回相应的产品对象。

(2)工厂方法模式(Factory Method Pattern):工厂方法模式将对象的创建延迟到子类中,每个具体子类都负责创建一个特定的产品对象,客户端通过调用工厂方法来获取所需的产品对象。

(3)抽象工厂模式(Abstract Factory Pattern):抽象工厂模式提供一个接口或抽象类,用于创建一系列相关或相互依赖的产品对象。具体的工厂类实现了该接口或继承了该抽象类,负责创建对应的产品系列。

2.2 工厂模式的设计

2.2.1 简单工厂模式设计

简单工厂模式通过一个工厂类来封装对象的创建逻辑,根据客户端传入的参数返回相应的产品对象。

1、设计简单工厂类

(1)创建接口Shape

/**

  • 图形接口

*/

public interface Shape {

    /**

     * 绘制图形

     */

    void draw();

}

(2)分别创建2个实现类Triangle,Square

绘制三角形实现类Triangle

/**

  • 绘制三角形

*/

public class Triangle implements Shape{

    @Override

    public void draw() {

        System.out.println("绘制三角形");

    }

}

绘制四边形实现类Square

/**

  • 绘制四边形

*/

public class Square implements Shape{

    @Override

    public void draw() {

        System.out.println("绘制四边形");

    }

}

(3)创建工厂类

/**

  • 图形工厂类

*/

public class ShapeFactory {

    /**

     * 创建对象

     * @param type 对象类型

     * @return

     */

    public static Shape createBean(String type){

        Shape shape;

        switch (type.toLowerCase()){

            case "triangle":

                shape = new Triangle();

                break;

            case "square":

                shape = new Square();

                break;

            default:

                shape = null;

        }

        return shape;

    }

}

(4)测试

创建测试类,并运行测试方法

public class ShapeFactoryTest {

   @Test

    public void test1(){

       //创建Triangle对象

       Shape triangle = ShapeFactory.createBean("triangle");

       triangle.draw();

       //创建Square对象

       Shape square = ShapeFactory.createBean("square");

       square.draw();

   }

}

2、添加新的产品需求

如果需要增加新的功能,如绘制圆形,只需要创建一个Circle实现类,不需要修改原来代码,但是工厂类的代码需要修改,需要装配一个新产品功能。

(1)创建圆形实现类Circle

/**

  • 绘制圆形

*/

public class Circle implements Shape{

    @Override

    public void draw() {

        System.out.println("绘制圆形");

    }

}

(2)修改工厂类的代码

      switch (type.toLowerCase()){

            case "triangle":

                shape = new Triangle();

                break;

            case "square":

                shape = new Square();

                break;

 			case "circle":

                shape = new Circle();

                break;

            default:

                shape = null;
        }

(3)测试

添加测试方法,并运行

@Test

public void test2(){

    //创建Triangle对象

    Shape triangle = ShapeFactory.createBean("triangle");

    triangle.draw();

    //创建circle对象

    Shape circle = ShapeFactory.createBean("circle");

    circle.draw();

}

2.2.2 工厂方法模式设计

工厂方法模式将对象的创建延迟到子类中,每个具体子类都负责创建一个特定的产品对象,客户端通过调用工厂方法来获取所需的产品对象。

接上例(已创建接口Shape,实现类Triangle、Square、Circle)

(1)创建工厂接口

/**

  • 工厂接口

*/

public interface IShapeFactory {

    /**

     * 创建对象方法

     * @return

     */

    Shape createBean();

}

(2)分别创建Triangle、Square的工厂类

/**

  • 三角形工厂类

*/


```java
public class TriangleFactory implements IShapeFactory{

    @Override

    public Shape createBean() {

        return new Triangle();

    }

}

/**

 * 四边形工厂类

 */

```java
public class SquareFactory implements IShapeFactory{

    @Override

    public Shape createBean() {

        return new Square();

    }

}

(3)测试

 @Test

    public void test3(){

        //创建Triangle对象

        IShapeFactory triangleFactory = new TriangleFactory();

        Shape triangle = triangleFactory.createBean();

        triangle.draw();

        //创建Square对象

        IShapeFactory squareFactory = new SquareFactory();

        Shape square = squareFactory.createBean();

        square.draw();

    }

(4)同理,如果添加绘制个一个圆形产品,只需要创建子类和子类的工厂类即可。

(代码略)

2.2.3 抽象工厂模式设计

抽象工厂模式提供一个接口或抽象类,用于创建一系列相关或相互依赖的产品对象。具体的工厂类实现了该接口或继承了该抽象类,负责创建对应的产品系列。

接上例(已创建接口Shape,实现类Triangle、Square、Circle)

(1)创建抽象工厂类(接口),用于封装创建各产品的方法

/**

  • 抽象工厂

*/

public interface IAbstractFactory {

    /**

     * 创建Triangle对象

     * @return

     */

    Shape createTriangleBean();

    Shape createSquareBean();

}

(2)创建抽象工厂类(接口)的实现类,具体创建子类对象

/**

  • 抽象工厂的实现类

*/

public class AbstractFactory implements IAbstractFactory{

    @Override

    public Shape createTriangleBean() {

        return new Triangle();

    }

    @Override

    public Shape createSquareBean() {

        return new Square();

    }

}

(3)测试

@Test

public void test4(){

   //创建抽象工厂

    IAbstractFactory beanFactory = new AbstractFactory();

    //创建Triangle对象

    Shape triangle = beanFactory.createTriangleBean();

    triangle.draw();

    //创建Square对象

    Shape square = beanFactory.createSquareBean();

    square.draw();

}

(4)思考,如果需要添加一个绘制圆形功能,如何实现?

总结:

(1)简单工厂模式

实现简单,适合创建单一类型的对象。将对象的实例化过程集中在工厂类中,客户端只需要与工厂进行交互,降低了客户端的复杂度。隐藏了对象的创建细节,提供了一种简单的方式来获取所需对象。

扩展性较差,对于新增产品类型需要修改工厂类的逻辑。违背了开闭原则,对于每个新增的产品类型都需要修改工厂类的代码,增加了耦合性。

(2)工厂方法模式

通过定义抽象工厂和具体工厂类,每个具体工厂负责创建特定的产品,可以灵活地扩展工厂和产品。符合开闭原则,新增产品类型只需添加相应的具体工厂即可,无需修改已有代码。

(3)抽象工厂模式

扩展新的产品族比较困难,需要修改抽象工厂接口和所有具体工厂的实现。但它提供了一种将相关产品组合成系列或簇的方式,可以在不修改已有代码的情况下引入新的产品系列。

第 3 章 代理模式

3.1 代理模式

3.1.1 什么是代理模式

代理模式通过引入一个代理对象来控制对原对象的访问。代理对象在客户端和目标对象之间充当中介,负责将客户端的请求转发给目标对象,同时可以在转发请求前后进行额外的处理(增加功能)。

在代理模式中,我们创建具有现有对象的对象,以便向外界提供功能接口。

3.1.2 为什么要用代理模式

代理模式可以控制对象的访问和权限,可以为对象提供额外的功能,例如缓存和延迟加载,可以降低系统的耦合度,将访问控制与业务逻辑分离,例如用户下单与用户登录。代理模式解决的是在直接访问某些对象时可能遇到的问题,或需要在访问一个对象时进行一些控制或额外处理。

3.1.3 代理模式要素

主要涉及到以下几个核心角色:

(1)抽象主题(Subject):定义了真实主题和代理主题的共同接口,这样在任何使用真实主题的地方都可以使用代理主题。

(2)真实主题(Real Subject):实现了抽象主题接口,是代理对象所代表的真实对象。客户端直接访问真实主题,但在某些情况下,可以通过代理主题来间接访问。

(3)代理(Proxy)主题:实现了抽象主题接口,并持有对真实主题的引用。代理主题通常在真实主题的基础上提供一些额外的功能,例如延迟加载、权限控制、日志记录等。

(4)客户端(Client):使用抽象主题接口来操作真实主题或代理主题,不需要知道具体是哪一个实现类。

3.1.4 代理模式的分类

代理模式有不同的形式,主要有三种:

(1)静态代理:静态代理是最基本的代理模式,它需要手动编写代理类。可以通过实现或继承相同的接口或父类,使得代理对象拥有与实际对象相同的方法和属性。代理对象在调用实际对象的方法时,可以在方法前或方法后添加一些额外的操作,以实现特定的功能。

(2)JDK动态代理:主要是基于反射,使用反射解析目标对象的属性、方法等,根据解析的内容生成代理类,就是在运行时生成一个代理对象,并将所有方法调用转发给我们指定的处理器。

(3)Cglib动态代理:基于目标对象创建子类的方式实现的,它可以在运行时动态修改目标对象的字节码,从而达到代理的目的,可以代理没有任何接口的类。

3.2 代理模式设计

3.2.1 静态代理模式设计

静态代理是最基本的代理模式,它需要手动编写代理类。可以通过实现或继承相同的接口或父类,使得代理对象拥有与实际对象相同的方法和属性。代理对象在调用实际对象的方法时,可以在方法前或方法后添加一些额外的操作,以实现特定的功能。

1、案例分析

(1)案例需求:需要保存用户信息,同时在保存用户信息之后记录保存日志。

(2)案例分析:为了降低程序的耦合性,可以将逻辑与访问控制分离。本案例主要功能是保存用户信息,记录日志操作成功与否不影响到用户信息的保存,可以将记录日志功能作为辅助功能(增强功能)。

(3)实现步骤如下图所示:

Java设计模式(简易版)_第1张图片

2、案例实现

(1)创建用户接口

/**

  • 用户接口

*/

public interface IUser {
    void save();
}

(2)创建日志类

public class Log {
    public static void doLog(){
        System.out.println("记录日志...");
    }
}

(3)创建用户类

public class UserImpl implements IUser{
    @Override
    public void save() {
        System.out.println("用户信息已存档");
    }
}

(4)创建用户代理类

public class UserProxy implements IUser{
    private UserImpl user = new UserImpl();
    @Override
    public void save() {
        user.save();// 保存用户信息
        Log.doLog();// 记录日志
    }
}

(5)测试,创建客户端类(用测试类代替)

public class ProxyTest {
    @Test
    public void test1(){
        IUser proxy= new UserProxy();
        proxy.save();
    }
}

从上例可以看出:如果需要增强功能,不需要改变目标对象类的代码,只是增加一个代理类即可。假如,用户类添加一个修改用户信息的功能,静态代理模式就会造成系统设计中类的数量增加,进而增加了系统的复杂度。

3.2.2 JDK动态代理

JDK动态代理主要是基于反射,使用反射解析目标对象的属性、方法等根据解析的内容生成代理类,就是在运行时生成一个代理对象,并将所有方法调用转发给我们指定的处理器。

①JDK动态代理的调用处理程序需要事先实现InvocationHandler接口,重写invoke方法,实现接口的invoke方法三个参数分别是:

  • proxy:代理对象,通过proxy可以强制转换为接口类型,通过该参数调用的方法是被代理后的方法。
  • method:要调用的方法。
  • args:方法中的参数。

通过反射,在invoke方法里面调用被代理对象的方法(method. invoke (target, args)),并在该行代码前后增加功能。

②调用Proxy的静态方法newProxyInstance创建代理对象。

newProxyInstance方法有三个参数:

  • classLoader:被代理对象的类加载器(确保代理类和目标对象属于同一个类加载器所加载的类)。
  • interfaces:被代理对象的实现接口,是一个数组(一个类实现了多个接口)。
  • invocationHandler:功能增强类,代理对象实现目标对象方法的过程。

(1)创建动态代理类

public class UserHandler implements InvocationHandler {
    private Object target; //代理接口
    public UserHandler(){}
    
    public UserHandler(Object target){
        this.target = target;

    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Object o = method.invoke(target,args);
        Log.doLog();
        return o;
    }

/**

 * 创建代理对象

 * @return

 */
    public Object getProxy(){
        return Proxy.newProxyInstance(target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),this);
    }
    
}

(2)测试,创建客户端

这里必须强转成接口类型,方便代理对象调用方法。

 @Test

    public void test2(){

        // 创建被代理对象
        IUser user = new UserImpl();

        // 创建增强类
        UserHandler userHandler = new UserHandler(user);

        // 创建代理对象
        IUser proxy = (IUser)  userHandler.getProxy();

        // 调用方法
        proxy.save();
    }

JDK动态代理也可以不实现InvocationHandler接口,使用内部类的方式实现InvocationHandler接口。

3.2.3 Cglib动态代理

Cglib动态代理是基于目标对象创建子类的方式实现的,CGLIB通过动态生成一个需要被代理类的子类 (即被代理类作为父类),该子类重写被代理类的所有不是 final修饰的方法,并在子类中采用方法拦截的技术拦截父类所有的方法调用。它可以在运行时动态修改目标对象的字节码,从而达到代理的目的,可以代理没有任何接口的类。

①Cglib动态代理的调用处理程序必须事先实现MethodInterceptor拦截器接口,重写intercept方法,实现接口的intercept方法四个参数分别是:

  • obj:代理对象。
  • method:要调用的方法。
  • objects:方法中的参数。
  • methodProxy:代理对象生成的代理方法。

②创建Enhancer ,设置要被代理的类和调用方法时触发的拦截器。Enhancer类是CGLIB中的一个类,既能够代理普通的class,也能够代理接口(JDK中的Proxy只能代理接口)。Enhancer类主要用到的方法:

  • setSuperclass():用来设置代理类的父类,即需要给哪个类创建代理类。
  • setCallback():传递的是MethodInterceptor接口类型的参数,这个MethodInterceptor接口的intercept方法会拦截代理对象所有的方法调用。
  • setCallbacks():顾名思义 传递的是MethodInterceptor接口类型的参数数组。
  • setCallbackFilter():设置回调选择器,我们通过CallbackFilter可以实现不同的方法使用不同的回调方法
  • create():方法获取代理对象。

(1)使用CGLIB动态代理,首先需要导入相关的Jar包

  

        cglib

        cglib

        3.3.0

    

(2)创建代理类

public class UserInterceptor  implements MethodInterceptor{

    private Object target;

    public UserInterceptor(Object target){

        this.target = target;

    }

    public Object intercept(Object object, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {

        Object o = methodProxy.invokeSuper(object, objects);

        Log.doLog();

        return o;

    }

    public Object getProxy(){

        // 创建增强类
        Enhancer enhancer = new Enhancer();

        // 创建被代理对象,加载字节码
        enhancer.setSuperclass(target.getClass());

        // 生成回调,将增强类引入到代理类中
        enhancer.setCallback(this);

        // 创建代理对象
        return enhancer.create();
    }
}

(3)创建客户端测试

    @Test

    public void test3(){

        // 创建被代理对象
        UserImpl user = new UserImpl();

        // 创建增强类
        UserInterceptor userInterceptor = new UserInterceptor(user);
        
        // 创建代理对象
        UserImpl proxy = (UserImpl)  userInterceptor.getProxy();
        
        proxy.save();    
}

总结:

(1)静态代理由于在程序运行前就已经确定代理类,因此它可以在编译时进行类型检查和安全管理。由于每个代理类只能代理一个接口,因此如果需要代理多个类,就需要为每个类写一个代理类,代码可能会非常冗余。

(2)JDK动态代理可以在运行时动态创建代理类,可以代理多个接口,从而减少代码冗余。JDK动态代理要求目标类实现一个接口,代理对象实现了与目标类相同的接口。在调用代理方法时,通过反射调用目标方法,因此会引入一定的性能开销。

(3)CGLIB动态代理在没有接口的情况可以使用(有接口也可以使用)。通过Enhancer类创建代理对象, 将目标对象设置为父类。CGLIB动态代理直接调用目标方法,省去了反射的开销,因此在性能上通常比JDK动态代理略快。但是对于final类、private方法和static方法等无法CGLIB无法生成代理类。

你可能感兴趣的:(java,设计模式,javascript,开发语言)