目录

设计模式之原型模式

设计模式之原型模式

原型模式(Prototype Pattern)也三大分类中的创建型设计模式,其主要是用于为系统其他类提供一个本体的克隆体,也就是我们平时所说的克隆技术,相对于其他的设计模式而言,原型模式整体实现的思路是相对比较简单一些的,而且 在目前的Java语言中已经将原型模式中的克隆融为一体了,大家可以随手拿来使用即可。

1. 概念

原型模式(Prototype Pattern)是用于创建重复的对象,同时又能保证性能。其主要的实现就是从本体给外部提供一个克隆体进行使用。

https://i-blog.csdnimg.cn/direct/0d37d62ef0ce4bd58f33b3c69bee6cb8.png

2. 代码实现

这里呢我想通过一个简单的查询数据库并且将数据存放在缓存中间件的形式来实现这个设计模式(其实MyBatis的底层也使用了原型模式的设计理念):

这里我们先定义一个实体类User 并且使其能够实现Cloneable接口,代表此对象是可以克隆的:

/**
 * 当前对象是可克隆的
 */
public class User implements Cloneable {

    private String username;
    private Integer age;

    public User(){
        System.out.println("User对象创建");
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "User{" +
                "username='" + username + '\'' +
                ", age=" + age +
                '}';
    }


    /**
     * 再创建一个人,赋予我的所有属性
     * @return
     * @throws CloneNotSupportedException
     */
    @Override
    protected Object clone() throws CloneNotSupportedException {
        User user = new User();
        user.setUsername(username);
        user.setAge(age);
        return user;
    }
}

这里我们将这个类实现了Cloneable接口,那么我们就要实现该接口下的clone方法,以便后续其他类调用时我们可以通过clone方法快速返回一个克隆体对象供外部进行使用。

后续我们创建一个MyBatis类,以及中间使用HashMap来作为一个缓存层,主要业务逻辑也是简单的缓存机制:即先查询缓存中是否有数据,如果有直接返回;如果没有则从数据库里查询返回并放到缓存中:

public class Mybatis {

    //缓存user.序列化和反序列化-深克隆
    private Map<String,User> userCache = new HashMap<>();

    /**
     * 从数据库查数据
     * @return
     */
    public User getUser(String username) throws Exception {
        User user = null;
        //缓存中没有
        if(!userCache.containsKey(username)){
            //查询数据库
            user = getUserFromDb(username);
        }else {
            //从缓存中直接拿,脏缓存问题
            //原型已经拿到,但是不能直接给。(本人)
            user = userCache.get(username);
            System.out.println("从缓存中拿到的是:"+user);
            //从这个对象快速得到一个克隆体(克隆人)==原型模式
            user = (User) user.clone();
        }

        return user;
    }

    private User getUserFromDb(String username) throws Exception{
        System.out.println("从数据库查到:"+username);
        User user = new User();
        user.setUsername(username);
        user.setAge(18);
        //给缓存中放一个clone
        userCache.put(username, (User) user.clone());
        return user;
    }


}

这里我们发现,每一次从缓存中返回的数据并不是HashMap本身存储的数据,而是通过一个clone方法生成的克隆体来进行返回,我们反过来想想,为什么要设计成这样呢?这里我们思考一下,如果返回本体会有哪些问题,如下代码所示:

public class MainTest {

    public static void main(String[] args) throws Exception {
        Mybatis mybatis = new Mybatis();
        
        User zhangsan1 = mybatis.getUser("zhangsan");
        System.out.println("1==>"+zhangsan1);
        // 此时我们将原本的属性修改了
        zhangsan1.setUsername("李四");
        System.out.println("zhangsan1自己改了:"+zhangsan1);


        // 后续我们再进行取数据时,发现全是被修改后的脏数据,导致缓存数据全部变脏
        User zhangsan2 = mybatis.getUser("zhangsan");
        System.out.println("2-->"+zhangsan2);
    }
}

当我们对数据进行修改了,如果返回的本体对象,由于返回了该对象的引用,修改了属性后,JVM的堆空间中此对象的引用的属性值也被永久改变了,后续其他任何的调用时,都会发现属性被修改了,与数据库本身的数据互相违背,这样会在业务场景下造成很大的麻烦,并且排查起来相当困难,因为在我们程序员排查过程中,整体业务代码是走通的,发现该问题会有一定的难度。

3. 使用场景

  • 资源优化
  • 系统对于性能与安全有一定的要求
  • 一个对象多个修改者的场景
  • 一个对象需要提供给其他对象访问,而且各个调用者可能都需要修改其值时可以考虑使用原型模式拷贝多个对象供调用者使用。

原型模式其实无处不在,尤其是在Java语言本身已经和它融为一体,所以整体的学习难度以及理解程度相对于简单,但是如果我们作为程序开发者来讲,在特定的场景下如果没有使用原型模式很有可能会造成不必要的麻烦。