Spring 常用的设计模式

2.1 为什么要从设计模式开始

设计模式并不是可以帮你解决一个难题解决一个问题,而是让你以后的代码能够很好的维护,减少修改一处,多出bug,并且很优雅、复用率高,并且可能一处修改,多处适配的情况。

2.1.1 写出优雅的代码

不知道大家有没有写过这样的代码:

/**
 * @author WangYifei
 * @date 2020-12-03 13:54
 * @describe
 */
public class Test {
    public void test(String name, Object object) {
        if (name.equals("")) {
            // object.setAge = name
        }
        if (name.equals("")) {
            
        }
        if (name.equals("")) {
            
        }
        //..... 很多if
    }
}

为的是将object中的某个属性全部弄到一个新的对象中,因为换了一个实体类。

不知道有没有用过阿里的一款jar包,叫fastJson,通过json去转成json字符串,之后转成某个对象,而不需要你if了,然后转到指定的对象中去。大量的减少了 if 判断赋值的操作。

2.1.3 经典框架都在用设计模式解决问题

Spring 就是一个把设计模式用得淋漓尽致的经典框架,其实从类的命名就能看出来,下面一一列举,如下表所示。

img

2.2 工厂模式详解

2.2.1 工厂模式的由来

在现实生活中我们都知道,原始社会自给自足(没有工厂)、农耕社会有了小作坊(简单工厂,如民间酒坊)、工业革命后有了流水线(工厂方法,自产自销)、现代产业链中有代工厂(抽象工厂,如富士康)。

我们的项目代码同样也是由简到繁一步一步迭代而来的,但对于调用者来说却越来越简单化了。

2.2.2 简单工厂模式

简单工厂模式(Simple Factory Pattern)是指由一个工厂对象决定创建哪一种产品类的实例,但它不属于 GoF 的 23 种设计模式。简单工厂模式适用于工厂类负责创建的对象较少的场景,且客户端只需要传入工厂类的参数,对于如何创建对象不需要关心。

首先有一个人类,每个人都会说话,用于告诉大家自己是谁。

/**
 * @author WangYifei
 * @date 2020-12-03 14:04
 * @describe
 */
public interface Person {
    void say();
}

随后lyj是一个人

/**
 * @author WangYifei
 * @date 2020-12-03 14:03
 * @describe
 */
public class Lyj implements Person{
    @Override
    public void say() {
        System.out.println("我是lyj");
    }
}

wyf也是一个人

/**
 * @author WangYifei
 * @date 2020-12-03 14:03
 * @describe
 */
public class Wyf implements Person{
    @Override
    public void say() {
        System.out.println("我是wyf");
    }
}

接下来用到了简单工厂类

/**
 * @author WangYifei
 * @date 2020-12-03 14:03
 * @describe 简单工厂,把这个工厂当成女娲
 */
public class SimpleFactory {
    /**
     * 通过名称获取要哪一个对象
     * @param name 对象名称
     * @return
     */
    public static Person getPerson(String name) {
        if (name.equals("wyf")){
            return new Wyf();
        } else if (name.equals("lyj")) {
            return new Lyj();
        }
        return null;
    }


    public static void main(String[] args) {
        Person person = getPerson("lyj");
        person.say();
    }
}

一个getPerson(String name); 方法,通过name作为约束告诉女娲,我想要一个什么人。

图中,我要一个lyj,于是就new 一个Lyj() 出来,person.say() 一定是lyj想说的话。

那么就有一个问题了,我要new一个吴彦祖呢?new一个彭于晏呢?是不是要在getPerson() 修改代码,新增加一个else if,不符合开闭原则,需要对代码进行修改。

简单工厂模式也有它的缺点:工厂类的职责相对过重,不易于扩展过于复杂的产品结构。

2.2.3 工厂方法模式

工厂方法模式(Fatory Method Pattern)是指定义一个创建对象的接口,但让实现这个接口的类来决定实例化哪个类,工厂方法模式让类的实例化推迟到子类中进行。在工厂方法模式中用户只需要关心所需产品对应的工厂,无须关心创建细节,而且加入新的产品时符合开闭原则。

解释:这个意思就是,我tm管你什么条件,你要什么自己new什么,反正我这个工厂只做这一件事。

image-20201203142539063

/**
 * @author WangYifei
 * @date 2020-12-03 14:23
 * @describe
 */
public class WyfFactory implements PersonFactory{
    @Override
    public Person create() {
        return new Wyf();
    }
}
/**
 * @author WangYifei
 * @date 2020-12-03 14:22
 * @describe
 */
public class LyjFactory implements PersonFactory{

    @Override
    public Person create() {
        return new Lyj();
    }
}
/**
 * @author WangYifei
 * @date 2020-12-03 14:22
 * @describe
 */
public interface PersonFactory {
    Person create();
}

每一个工厂都能造东西,之前是放到一个工厂,由某个约束去选择创造哪一个,但是违反了开闭原则,于是弄成一个工厂只做一件事,互相不影响实现了开闭原则和单一职责原则。

/**
 * @author WangYifei
 * @date 2020-12-03 14:23
 * @describe
 */
public class Test {
    public static void main(String[] args) {
        PersonFactory personFactory = new LyjFactory();
        Person person = personFactory.create();
        person.say();
    }
}

我实例化lyj工厂,然后进行创建一个lyj,那么这个person肯定是lyj,不用怀疑。

想必也发现了问题,那我lyj,wyf都是person,又来吴彦祖了,又来彭于晏了。那我是不是还要一直创建一个对象??一个工厂,卧槽,太特码麻烦了吧??N个文件啊!

看下面的抽象工厂模式:

2.2.4 抽象工厂模式

抽象工厂模式(Abastract Factory Pattern)是指提供一个创建一系列相关或相互依赖对象的接口,无须指定它们的具体类。客户端(应用层)不依赖于产品类实例如何被创建、如何被实现等细节,强调的是一系列相关的产品对象(属于同一产品族)一起使用创建对象需要大量重复的代码。需要提供一个产品类的库,所有的产品以同样的接口出现,从而使客户端不依赖于具体实现。

把一系列东西设定为一个工厂,比如苹果工厂就创造,红苹果,白苹果,牛奶苹果等等...

梨子工厂就创造,红梨子,白梨子,牛奶梨子等等...

上代码:

/**
 * @author WangYifei
 * @date 2020-12-03 14:41
 * @describe
 */
public interface PersonFactory {
    Person createWyf();

    Person createLyj();
}
/**
 * @author WangYifei
 * @date 2020-12-03 14:36
 * @describe
 */
public class PersonFactoryImpl implements PersonFactory{
    @Override
    public Person createWyf(){
        return new Wyf();
    }

    @Override
    public Person createLyj() {
        return new Lyj();
    }
}

wyf和lyj都同属于person,所以可以放到person工厂中去创造,这样就减少了很多类的创建。

/**
 * @author WangYifei
 * @date 2020-12-03 14:39
 * @describe
 */
public class Test {
    public static void main(String[] args) {
        PersonFactory personFactory = new PersonFactoryImpl();
        Person wyf = personFactory.createWyf();
        wyf.say();
    }
}

但是这样又存在一个问题:不符合开闭原则啊!,我这个系列,多了一个彭于晏,我多家制造厂都多一个彭于晏,那怎么办???只能全部都重写一个制造彭于晏的方法!高扩展,但是违背开闭原则。

*:所以设计模式也并不是万能的,具体的选用还是根据情况而定。

2.3 单例模式详解

2.3.1 单例模式的应用场景

单例模式(Singleton Pattern)是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。单例模式是创建型模式。单例模式在现实生活中应用也非常广泛,例如,公司CEO、部门经理等。J2EE 标准中的 ServletContext、ServletContextConfig 等、Spring 框架应用中的ApplicationContext、数据库的连接池等也都是单例形式。

*:个人总结: 就是这个对象把构造器私有化,不可主动创建对象,通过一个访问点创建,并且这个对象内存地址最终都一致。

2.3.2 饿汉式单例模式

饿汉式单例模式在类加载的时候就立即初始化,并且创建单例对象。它绝对线程安全,在线程还没出现以前就实例化了,不可能存在访问安全问题。

*: 采用了Java类加载的特性,static 变量在类初始化的时候就会进行对象创建,并且不会创建第二次,这个时候只需要给一个访问点获取这个变量就可以了。

优点: 没有加任何锁、执行效率比较高,用户体验比懒汉式单例模式更好。

缺点: 每次类加载的时候都会进行初始化,直接进行内存占用,如果过多,并且不用,那么就会内存浪费。

Spring中IoC容器ApplicationContext本身就是典型的饿汉式单例模式。接下来看一段代码:

image-20201204093924347

还有另外一种写法,利用静态代码块的机制:

image-20201204093958906

这两种写法都非常简单,也非常好理解,饿汉式单例模式适用于单例对象较少的情况。下面我们来看性能更优的写法。

2.3.3 懒汉式单例模式

/**
 * @author WangYifei
 * @date 2020-12-04 9:41
 * @describe 懒汉式单例
 */
public class Test {
    private static Object object = null;

    private Test(){};

    public static Object getObject() {
        if (object == null) {
            object = new Object();
        }
        return object;
    }
}

这是懒汉式,在需要用到的时候进行对象的创建,在判断是否为空,如果为空,进行一个创建,否则直接返回,但是这样的话有一个问题?在多线程的情况下怎么办?两个线程同时进入if里面,就覆盖了,可以加锁,问题是怎么加?

/**
 * @author WangYifei
 * @date 2020-12-04 9:41
 * @describe
 */
public class Test {
    private static Object object = null;

    private Test(){};

    public static synchronized Object getObject() {
        if (object == null) {
            object = new Object();
        }
        return object;
    }
}

这样吗?那这样效率太低了吧?所有的线程进入这个方法都需要进行上锁同步。我们可以进行优化,加到方法里。

/**
 * @author WangYifei
 * @date 2020-12-04 9:41
 * @describe
 */
public class Test {
    private static Object object = null;

    private Test(){};

    public static Object getObject() {
        synchronized (Test.class) {
            if (object == null) {
                object = new Object();
            }
        }
        return object;
    }
}

这样?仔细想想这样跟外面其实区别不大每次进入方法,进行一个上锁判断,还可以进行一个优化。

/**
 * @author WangYifei
 * @date 2020-12-04 9:41
 * @describe
 */
public class Test {
    private static Object object = null;

    private Test(){};

    public static Object getObject() {
        if (object == null) {
            synchronized (Test.class) {
                if (object == null) {
                    object = new Object();
                }
            }
        }
        return object;
    }
}

代码到这有的人就容易晕了,为什么要两个if,有什么作用?仔细脑中模拟一下,如果在外面两个线程同时进入到了第一个if,一个争夺到了锁,另一个等待,这个时候如果没有里面if,那么就会进行一个覆盖,所以需要双重验证。

*: 个人觉得单例没什么好说的。。还有几种破坏单例的情况,比如序列化破坏单例、反射破坏单例,感兴趣的可以自己百度。

2.4 原型模式详解

2.4.1 原型模式的应用场景

你一定遇到过大篇幅使用get或set赋值的场景,例如下面这样的代码:

image-20201204095050379

上述代码非常工整,命名非常规范,注释也写得很全面,大家觉得这样的代码优雅吗?我认为,这样的代码属于“纯体力劳动”。原型模式就能帮助我们解决这样的问题。

原型模式(Prototype Pattern)是指原型实例指定创建对象的种类,并且通过复制这些原型创建新的对象。

原型模式主要适用于以下场景:

(1)类初始化消耗资源较多。

(2)使用new生成一个对象需要非常烦琐的过程(数据准备、访问权限等)。

(3)构造函数比较复杂。

(4)在循环体中产生大量对象。

在 Spring 中,原型模式应用得非常广泛。例如 scope="prototype",我们经常用的JSON.parseObject()也是一种原型模式。下面我们来看看原型模式的类结构图,如下图所示。

*:可以利用序列化的方式进行对象的克隆,并且克隆出来的是一个唯一的对象,非引用对象。剩下的自行体会= =,这个我会就不想说了= =

2.5 代理模式详解

2.5.1 代理模式的应用场景

生活中的租房中介、售票黄牛、婚介、经纪人、快递、事务代理、非侵入式日志监听等,都是代理模式的实际体现。代理模式(ProxyPattern)的定义也非常简单,是指为其他对象提供一种代理,以控制对这个对象的访问。代理对象在客户端和目标对象之间起到中介作用,代理模式属于结构型设计模式。使用代理模式主要有两个目的:一是保护目标对象,二是增强目标对象。下面我们来看一下代理模式的类结构图,如下图所示。

image-20201204095554164

Subject是顶层接口,RealSubject是真实对象(被代理对象),Proxy是代理对象,代理对象持有被代理对象的引用,客户端调用代理对象的方法,同时也调用被代理对象的方法,但是会在代理对象前后增加一些处理代码。在代码中,一般代理会被理解为代码增强,实际上就是在原代码逻辑前后增加一些代码逻辑,而使调用者无感知。代理模式属于结构型模式,分为静态代理和动态代理。

2.5.2 静态代理

看一下代码先,为了之后对接口开发扩展,我弄了两个接口类。

/**
 * @author WangYifei
 * @date 2020-12-04 9:57
 * @describe 客户,不同的客户可能要买不同的房子,但是都是交给一个中介去做
 */
public interface IUser {
    void buy();
}
/**
 * @author WangYifei
 * @date 2020-12-04 9:58
 * @describe 中介不同的客户不同的中介
 */
public interface Medium {
    void find();
}
/**
 * @author WangYifei
 * @date 2020-12-04 9:57
 * @describe
 */
public class Wyf implements IUser{

    @Override
    public void buy() {
        System.out.println("wyf要大别野");
    }
}
/**
 * @author WangYifei
 * @date 2020-12-04 9:59
 * @describe
 */
public class WyfMedium implements Medium{
    // 需要定义客户是谁
    IUser iUser = null;
    public WyfMedium(IUser iUser) {
        this.iUser = iUser;
    }
    @Override
    public void find() {
        System.out.println("帮客户找房子");
        // 客户说需求
        iUser.buy();
        System.out.println("找到了这个房子");
    }
}

然后是进行测试。

/**
 * @author WangYifei
 * @date 2020-12-04 10:00
 * @describe
 */
public class Test {
    public static void main(String[] args) {
        Medium medium = new WyfMedium(new Wyf());
        medium.find();
    }
}

自己看 WyfMedium.java 像不像一个普通方法加入了一个自己平时封装的函数,其实就是这个意思,只不过更加灵活一点,我可以选择放哪一个进去,也就是类似于对原有的方法进行一个增强。

这里“小伙伴们”可能会觉得还是不知道如何将代理模式应用到业务场景中,我们来看一个实际的业务场景。在分布式业务场景中,通常会对数据库进行分库分表,分库分表之后使用 Java操作时就可能需要配置多个数据源,我们通过设置数据源路由来动态切换数据源。

先创建Order订单类:

/**
 * @author WangYifei
 * @date 2020-12-04 10:10
 * @describe
 */
@Data
public class Order {
    private Object orderInfo;

    private Long createTime;

    private String id;
}

创建OrderDao持久层操作类:

/**
 * @author WangYifei
 * @date 2020-12-04 10:12
 * @describe
 */
public class OrderDao {
    public int insert(Order order) {
        System.out.println("OrderDao 创建 Order 成功!");
        return 1;
    }
}

创建OrderService接口:

/**
 * @author WangYifei
 * @date 2020-12-04 12:46
 * @describe
 */
public interface OrderService {
    int createOrder(Order order);
}

创建OrderServiceImpl实现类:

/**
 * @author WangYifei
 * @date 2020-12-04 12:47
 * @describe
 */
public class OrderServiceImpl implements OrderService{
    private OrderDao orderDao;

    public OrderServiceImpl() {
        orderDao = new OrderDao();
    }
    @Override
    public int createOrder(Order order) {
        System.out.println("OrderService 调用 orderDao 创建订单");
        return orderDao.insert(order);
    }
}

这个是正常的数据库的操作,我需要插入一条新的数据,比如按照某个订单的年进行分库分表,要去不同的数据源查询,为了符合开闭原则,不去修改原有的方法,从而使用代理,对原有进行增强。

/**
 * @author WangYifei
 * @date 2020-12-04 12:49
 * @describe 动态切换数据源
 */
public class DynamicDataSourceEntry {
    // 默认数据源
    public final static String DEFAULT_SOURCE = null;

    private final static ThreadLocal<String> local = new ThreadLocal<>();

    private DynamicDataSourceEntry(){}

    // 清空数据源
    public static void clear() {
        local.remove();
    }

    // 获取当前正在使用的数据源
    public static String get() {
        return local.get();
    }

    // 还原当前切换的数据源
    public static void restore() {
        local.set(DEFAULT_SOURCE);
    }

    // 设置已知名字的数据源
    public static void set(int year) {
        local.set("BD_" + year);
    }
}

数据源相关操作。

/**
 * @author WangYifei
 * @date 2020-12-04 12:53
 * @describe
 */
public class OrderServiceStaticProxy implements OrderService{
    private SimpleDateFormat sp = new SimpleDateFormat("yyyy");

    private OrderService orderService;

    public OrderServiceStaticProxy(OrderService orderService) {
        this.orderService = orderService;
    }

    @Override
    public int createOrder(Order order) {
        before();
        Long time = order.getCreateTime();
        Integer dbRouter = Integer.valueOf(sp.format(new Date(time)));
        System.out.println("静态代理类自动分配到【DB_" + dbRouter + "】数据源处理数据");
        DynamicDataSourceEntry.set(dbRouter);
        orderService.createOrder(order);
        after();
        return 0;
    }

    private void before() {
        System.out.println("Proxy before method");
    }

    private void after() {
        System.out.println("Proxy after method");
    }
}

代理类。

测试类:

/**
 * @author WangYifei
 * @date 2020-12-04 13:00
 * @describe
 */
public class Test {
    @SneakyThrows
    public static void main(String[] args) {
        Order order = new Order();
        SimpleDateFormat sp = new SimpleDateFormat("yyyy/MM/dd");
        Date date = sp.parse("2020/10/10");
        order.setCreateTime(date.getTime());

        OrderService orderService = new OrderServiceStaticProxy(new OrderServiceImpl());
        orderService.createOrder(order);
    }
}

按照以前的写法可能是这样的:

/**
 * @author WangYifei
 * @date 2020-12-04 13:00
 * @describe
 */
public class Test {
    public static void main(String[] args) {
        Order order = new Order();
        OrderService orderService = new OrderServiceImpl();
        orderService.createOrder(order);
    }
}

突然来了一个需求,我要根据订单的创建时间切换数据源,插入不同的数据库或者数据表。

新增一个数据源,进行负责切换数据源并且在没有改变原有方法的基础上进行了增加,这就是代理模式的初衷。

最后修改:2021 年 01 月 21 日 04 : 34 PM
如果觉得我的文章对你有用,请随意赞赏