设计模式-创建型设计模式篇

120 阅读10分钟

什么是设计模式?

定义:代码开发过程中,对于常见问题的通用可重用解决方案。

创建型设计模式

主要用于对象的创建问题,有以下5种设计模式:

  1. 单例模式
  2. 工厂模式
  3. 建造者Builder模式
  4. 原型模式
  5. 对象池模式

一、单例模式

保证一个类只能生成一个对象实例

要实现一个类只能生成一个对象实例,就必须要考虑以下几点:

  1. 类的无参构造方法必须私有化,访问类型设置为private,且不能有其他构造方法。否则就可以直接通过无参构造方法生成多个对象实例
  2. 提供一个获取类对象实例的方法,只能是static修饰的方法,不然无法访问该方法。因为无法实例化类,就只能通过类去访问获取对象实例的方法,那就只能是static方法了
  3. 类内部控制只能生成一个对象实例的逻辑

“懒汉式”单例模式

public class Person {
    private static Person person;
    private Person() {
    }
    public static Person getPerson() {
        if (person == null) {
            person = new Person();
        }
        return person;
    }
}

这个单例模式有问题么?有,在多线程情况下会有问题,当多个线程同时调用getPerson()方法时,同时判断person为null,这时候就会各自创建一个person对象实例,然后返回,这时就生成了多个Person类对象实例,违背了单例模式的原则。 一个简单的解决办法是加上synchronized关键字,对getPerson()方法进行加锁,保证只能有一个线程进入该方法,代码如下:

// 对于static静态方法,synchronized加锁的对象是类对象
public synchronized static Person getPerson() {
    if (person == null) {
        person = new Person();
    }
    return person;
}

或者

public static Person getPerson() {
    // 这里设置加锁的对象是类对象
    synchronized(Person.class){
        if (person == null) {
            person = new Person();
        }
    }
    return person;
}

再考虑下,这样有没有问题呢?逻辑上没有问题,但是有性能上的问题,因为每次获取该类的单例对象实例,都必须首先获取锁,遇到多线程的情况,就会造成锁竞争,那怎么优化呢? 考虑下,我们把getPerson()方法逻辑分成两块:

一、当person为null,初始化person;
二、如果不为null,则直接返回person对象实例

所以实际上要加锁的逻辑是第一步,第二步不用加锁,因为这时候对象实例已经创建完毕,直接返回即可,所以优化的地方就是,我们可以只对第一步初始化对象实例的部分加锁,减少加锁的范围

public static Person getPerson() {
    if (person == null) {
        synchronized(Person.class){
            person = new Person();
        }
    }
    return person;
}

调用getPerson()方法时,首先判断对象实例是否已创建,如果已创建,直接返回,如果没有创建,那么获取锁,获取锁成功后初始化对象,然后返回实例对象,这样就只在person为null的时候才需要获取锁,减小了锁范围,提高性能。

那这样写有没有问题呢?有,还是多线程情况下会有问题,当多个线程调用getPerson()方法,如果此时person为null,那么就都进入synchronized逻辑,这时你可以发现,不管怎样,多个线程只要同时进入synchronized逻辑,就一定都会执行:person = new Person();这样还是会创建多个对象。还是违背了单例模式的原则。

解决办法是在synchronized逻辑中增加一次:person == null判断,如下:

public static Person getPerson() {
    if (person == null) {
        synchronized(Person.class){
            if (person == null) {
                person = new Person();
            }
        }
    }
    return person;
}

这就是“双检锁”单例模式。

“饿汉式”单例模式

单例模式的最佳实现形式,是利用类加载机制中的类只会加载一次原则,在声明单例类对象时直接实例化静态成员的方式保证一个类只有一个实例,这种方式可以避免加锁和判断是否创建的额外检查逻辑。

public class Person {
    private static final Person person = new Person();
    private Person() {
    }
    public static Person getPerson() {
        return person;
    }
}

二、工厂模式

工厂模式是通过一个工厂类创建类对象实例,并通过公共的接口提供获取对象实例的服务,分为:简单工厂模式、工厂方法模式和抽象工厂模式。

简单工厂模式

public class VehicleFactory {
    public enum VehicleType {
        BIKE, CAR, TRUCK
    }
    public static Vehicle createVehicle(VehicleType type) {
        switch (type) {
            case BIKE -> new Bike();
            case CAR -> new Car();
            case TRUCK -> new Truck();
            default -> {
                return null;
            }
        }
        return null;
    }
}

这样的简单工厂模式,不符合开闭原则(对扩展开放,对修改关闭),因此当有一个新的类型,例如:Train时,就需要修改createVehicle()方法。

为了让简单工厂模式符合开闭原则,一个简单的扩展是让子类注册自己到工厂类中,工厂类通过Class的newInstance()方法来创建实例,这样就可以符合开闭原则,具体如下:

public class VehicleFactory {
    public enum VehicleType {
        BIKE, CAR, TRUCK
    }
    private static final Map<VehicleType, Class<? extends Vehicle>> vehicleMap = new ConcurrentHashMap<>();

    public static void registerVehicle(VehicleType vehicleType, Class<? extends Vehicle> clazz) {
        vehicleMap.put(vehicleType, clazz);
    }

    public static Vehicle createVehicle(VehicleType type) throws Exception {
        Class<? extends Vehicle> clazz = vehicleMap.get(type);
        return clazz.getDeclaredConstructor().newInstance();
    }

}
// 如何使用呢?
public class Main {
    public static void main(String[] args) throws Exception {
    // 往工厂类中注册自己
    VehicleFactory.registerVehicle(VehicleFactory.VehicleType.BIKE, Bike.class);
    VehicleFactory.registerVehicle(VehicleFactory.VehicleType.CAR, Car.class);
    VehicleFactory.registerVehicle(VehicleFactory.VehicleType.TRUCK, Truck.class);

    // 调用工厂方法创建对象实例
    Vehicle vehicle = VehicleFactory.createVehicle(VehicleFactory.VehicleType.TRUCK);
    }
}

工厂方法模式

为了解决这一问题,我们可以将工厂类抽象化,将生成实例的方法变成抽象方法,由子类实现。

// 工厂定义
public abstract class VehicleFactory {
    /**
     * 创建实例对象为抽象方法,由子类实现,这样在扩展时,就不用修改工厂方法
     * 这样就对扩展开发,对修改关闭,符合SOLID的开闭原则
     */
    protected abstract Vehicle createVehicle();
    
    public Vehicle orderVehicle(BigDecimal price) {
        Vehicle vehicle = createVehicle();
        vehicle.setPrice(price);
        return vehicle;
    }
}
//子类工厂定义
public class BikeFactory extends VehicleFactory{
    @Override
    public Vehicle createVehicle() {
        return new Bike();
    }
}
public class CarFactory extends VehicleFactory{
    @Override
    public Vehicle createVehicle() {
        return new Car();
    }
}
// 使用
public class Main {
    public static void main(String[] args) {
        // 指定子类工厂类,最后实例化的也是指定子类工厂类的对象
        VehicleFactory vehicleFactory = new CarFactory();
        Vehicle vehicle = vehicleFactory.orderVehicle(new BigDecimal("100000"));
    }
}

抽象工厂模式

抽象工厂模式是工厂方法模式的扩展,进一步进行抽象。

// 定义抽象产品类
@Data
public abstract class AbstractProduct {

    private String name;
    private BigDecimal price;
}

// 定义抽象工厂类
public abstract class AbstractFactory {

    protected abstract AbstractProduct createProduct();

    public AbstractProduct orderProduct(String name, BigDecimal price) {
        AbstractProduct product = createProduct();
        product.setName(name);
        product.setPrice(price);
        return product;
    }
}

三、建造者模式(Builder模式)

考虑一种场景:如果类有多个属性,为了满足创建对象实例时,设置不同属性,就要写多个构造方法。这样一方面会使得构造方法越来越多,不易管理;另一方面相同数量相同类型的不同属性的构造方法,还解决不了。这种情况下,对于类属性较多,实例化形式较多的场景,推荐使用建造者模式

建造者模式,建议使用【内部建造者类】+【方法链】来代替多个构造方法。

典型的使用场景:
1.lombok的@Builder注解

@Data
@Builder
public class Person {
    private String name;
    private int age;
}

看下编译后的字节码文件内容:
public class Person {
    private String name;
    private int age;

    // 1.生成一个被建造类的包含所有属性的构造方法,参数都设置为final
    Person(final String name, final int age) {
        this.name = name;
        this.age = age;
    }

    // 2.被建造类生成一个static类型的builder()方法,返回建造者实例对象
    public static PersonBuilder builder() {
        return new PersonBuilder();
    }

    public String getName() {
        return this.name;
    }
    public int getAge() {
        return this.age;
    }
    // set方法都把参数设置为final类型
    public void setName(final String name) {
        this.name = name;
    }
    // set方法都把参数设置为final类型
    public void setAge(final int age) {
        this.age = age;
    }

    // 建造者类
    // 3.拥有和被建造类相同的属性,方法链会把属性值设置给建造者类的属性字段
    // 4.build()方法会调用被建造类的包含全部参数的构造方法,并把属性值传入,从而返回被建造类的实例对象
    public static class PersonBuilder {
        private String name;
        private int age;

        PersonBuilder() {
        }

        public PersonBuilder name(final String name) {
            this.name = name;
            return this;
        }

        public PersonBuilder age(final int age) {
            this.age = age;
            return this;
        }

        public Person build() {
            return new Person(this.name, this.age);
        }
    }
}

再来看看如何使用
public class Main {
    public static void main(String[] args) {
        Person person = Person.builder()// builder()为被建造者类的static方法,返回建造者类实例对象
                .name("tom")//为建造者类属性设置值
                .age(20)//为建造者类属性设置值
                .build();//调用被建造者类的全部参数构造方法,返回被建造者类实例对象
}
总体看下来,建造者类copy了被建造者类的全部属性对象,先设置给自己,然后调用被建造者类的全部参数构造方法,有点类似于代理的概念。

2.自己动手实现

@Data
public class Person2 {
    private String name;
    private int age;

    public static class Person2Builder {
        private Person2 person2;// 建造者类持有被建造者类

        public Person2Builder() {//构造方法中生成被建造者类实例对象
            person2 = new Person2();
        }

        public Person2Builder name(String name) {
            person2.setName(name);
            return this;
        }

        public Person2Builder age(int age) {
            person2.setAge(age);
            return this;
        }

        public Person2 build() {//build()方法返回被建造者实例对象
            return person2;
        }
    }
}

如何使用呢?
public class Main {
    Person2 person2 = new Person2.Person2Builder()//初始化被建造者类实例对象
            .name("tom")//为被建造者实例对象设置值
            .age(20)//为被建造者实例对象设置值
            .build();//返回被建造者类实例对象
}
和lombok的区别在于
1.没有copy被建造者类的全部属性,而是直接持有被建造者类变量
2.设置值时也没有设置给自己,而是直接设置给被建造者类实例对象

四、原型模式(对象克隆模式、对象拷贝模式)

考虑以下场景:

  1. 创建对象非常耗时,例如需要依赖外部调用,或者依赖硬件密集型操作,此时在内存中复制该对象,从而创建一个新的对象反而相对较快
  2. 获取相同对象的相同状态拷贝,正常步骤时,先创建对象,然后重复走一遍流程,从而达到相同状态,这也是非常繁琐和耗时的,此时更简单的做法是直接拷贝对象,在拷贝结构的同时,也拷贝了对象的运行时状态数据
  3. 在不确定所属具体类时需要获取对象实例,比如给你一个Object类对象,让你获取生成一个类的对象实例,此时都不知道该Object类对象是哪个具体类的实例对象,所以就无法调用具体类的构造方法生成新的实例对象,此时就可以通过拷贝的方式生成新的实例对象。

方式:直接拷贝对象实例,生成新的对象实例。
目的:快速生成新的实例对象

从UML图可以看出:

  • ProtoType是声明了clone()方法的接口或者抽象类,clone()方法必须由实现类或者继承子类实现
  • ProtoTypeImpl是具体实现类或者继承子类,需要实现clone()方法,返回新的类对象实例, 未命名文件 (1).png

典型的使用场景:Object类的clone()方法,BeanUtils.copyProperties()。

五、对象池模式

考虑到对象的创建是非常的耗费性能和资源的,因此考虑是否可以预先生成一批对象的实例,作为共享的实例池,对外提供使用,从而在使用时无需重复创建,提供了性能,重复使用也节省了资源,这就是对象池模式的初衷。

方式:预先生成对象实例,由统一的对象池进行管理,统一对外提供使用
目的:重复和共享对象实例,提高系统性能,节省系统资源。

从UML图可以看出:

  • Resource是实例需要放到对象池中进行管理的资源类
  • ResourcePool是管理资源类对象的池类
  • Client是客户类,当需要使用Resource实例对象时,通过ResourcePool获取Resource类的实例对象 未命名文件.png

典型的使用场景:数据库连接池、线程池等。