使程序运行更高效——原型模式

214 阅读5分钟

《Android源码设计模式解析与实战》读书笔记(四) 《Android源码设计模式解析与实战》PDF资料下载

一、原型模式简介

原型模式是一个创建型的模式。原型二字表明了该模式应该有一个样板实例,用户从这个样板对象中复制出一个内部属性一致的对象,这就是“克隆”。被复制的实例就是我们所称的“原型”。

原型模式多用于创建复杂的或者构造耗时的实例,因为这种情况下,复制一个已经存在的实例可使程序运行更高效。

1.1、定义

用原型实例指定创建对象的种类,并通过拷贝这些原型创建新的对象。

1.2、使用场景

  1. 类初始化需要消化非常多的资源,这个资源包括数据、硬件资源等,通过原型拷贝避免这些消耗。
  2. 通过new产生一个对象需要非常繁琐的数据准备或访问权限,这时可以使用原型模式。
  3. 一个对象需要提供给其他对象访问,而且各个调用者可能都需要修改其值时,可以考虑使用原型模式拷贝多个对象供调用者使用,即保护性拷贝。

注意: 通过实现Cloneable接口的原型模式在调用clone函数构造实例时并不一定比通过new操作速度快,只有当通过new构造对象较为耗时或者说成本较高时,通过clone方法才能获得效率上的提升。

二、原型模式的简单实现

/**
 * 文档类型,扮演的是ConcretePrototype角色,而cloneable是代表prototype角色
 */
public class WordDocument implements Cloneable {
    //文本
    private String mText;
    //图片名列表
    private ArrayList<String> mImages = new ArrayList<>();

    public WordDocument() {
        System.out.println("----------------WordDocument构造函数-----------------");
    }

    @Override
    protected WordDocument clone() {
        try {
            WordDocument doc = (WordDocument) super.clone();
            doc.mText = this.mText;
            doc.mImages = this.mImages;
            return doc;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    public String getText() {
        return mText;
    }

    public void setText(String mText) {
        this.mText = mText;
    }

    public ArrayList<String> getImages() {
        return mImages;
    }

    public void addImages(String img) {
        this.mImages.add(img);
    }

    /**
     * 打印文档内容
     */
    public void showDocument() {
        System.out.println("------------Word Content Start-------------");
        System.out.println("Text:" + mText);
        System.out.println("Images List:");
        for (String imgName : mImages) {
            System.out.println("image name:"+imgName);
        }
        System.out.println("------------Word Content End---------------");
    }
}

Cloneable是一个标识接口,它表明这个类的对象是可拷贝的。如果没有实现Cloneable接口却调用了clone()函数将抛出异常。

调用代码如下:

		WordDocument originDoc = new WordDocument();
        //2.编辑文档,添加图片等
        originDoc.setText("这是一篇文档");
        originDoc.addImages("图片1");
        originDoc.addImages("图片2");
        originDoc.addImages("图片3");
        originDoc.showDocument();

        //以原始文档为原型,拷贝一份副本
        WordDocument doc2 = originDoc.clone();
        doc2.showDocument();
        //修改文档副本,不影响原始文档
        doc2.setText("这是修改过的Doc2文本");
        doc2.showDocument();

        originDoc.showDocument();

输出结果:

原型模式.png

doc2是originDoc的一份拷贝,它们的内容是一样的,而doc2修改了文本内容以后并不会影响originDoc的文本内容,这就保证了originDoc的安全性。还需要注意,通过clone拷贝对象时并不会执行构造函数

原型模式的核心问题就是对原始对象进行拷贝,在这个模式的使用过程中需要注意的一点就是:深、浅拷贝的问题

三、原型模式实战

这是一个简化版的客户端,在用户登陆之后,通过LoginSession保存用户的登录信息,这些用户信息可能在APP的其他模块被用来做登录校验、用户个人信息显示等。但是,这些信息在客户端程序是不允许修改的,而需要在其他模块被调用,因此,需要开放已登录用户信息的访问接口。

/**
 * 用户实体类
 */
public class User {
    public int age;
    public String name;
    public String phoneNum;
    public Address address;

    @Override
    public String toString() {
        return "User{" +
                "age=" + age +
                ", name='" + name + '\'' +
                ", phoneNum='" + phoneNum + '\'' +
                ", addrss=" + address +
                '}';
    }
}
/**
 * 用户地址类,存储地址的详细信息
 */
public class Address {
    //城市
    public String city;
    //区
    public String district;
    //街道
    public String street;

    public Address(String city, String district, String street) {
        this.city = city;
        this.district = district;
        this.street = street;
    }

    @Override
    public String toString() {
        return "Address{" +
                "city='" + city + '\'' +
                ", district='" + district + '\'' +
                ", street='" + street + '\'' +
                '}';
    }
}
//登陆接口
public interface Login {
    void login();
}
//登录实现
public class LoginImpl implements Login {

    @Override
    public void login() {
        // 登录到服务器,获取到用户信息
        User loginedUser = new User();
        //将服务器返回的完整信息设置给loginedUser对象
        loginedUser.age = 22;
        loginedUser.name = "Mr.Simple";
        loginedUser.address = new Address("北京市", "海淀区", "花园东路");
        //登录完之后将用户信息设置到Session中LoginSession.getLoginSession()里
        LoginSession.getLoginSession().setLoginedUser(loginedUser);
    }
}
//登录Session
public class LoginSession {
    static LoginSession sLoginSession = null;
    //已登录用户
    private User loginedUser;

    public LoginSession() {
    }

    public static LoginSession getLoginSession() {
        if (sLoginSession == null) {
            sLoginSession = new LoginSession();
        }
        return sLoginSession;
    }

    //设置已登录的用户信息,不对外开放
    void setLoginedUser(User user) {
        loginedUser = user;
    }

    public User getLoginedUser() {
        return loginedUser;
    }
}

LoginSession中的setLoginedUser函数是包级私有的,因此外部模块无法调用,这在一定程度上实现了外部客户端程序不能修改已登录的用户信息。

但是,也会存在类似如下代码:

//获取已登录的User对象
User newUser = LoginSession.getLoginSession().getLoginedUser();
newUser.address = new Address("北京市", "朝阳区", "大望路");

类似的代码也会更新用户的地址。因此,需要使用原型模式来进行保护性拷贝,也就是说在LoginSession的getLoginUser()函数中返回的是已登录用户的一个拷贝,当更新用户地址的网络请求完成时,在通过包级私有的LoginSession中的setLoginedUser更新用户信息。

于是在User类中覆写了clone方法:

/**
 * 用户实体类
 */
public class User implements Cloneable {
    public int age;
    public String name;
    public String phoneNum;
    public Address address;

    @Override
    protected Object clone() throws CloneNotSupportedException {
        User user = null;
        try {
            user = (User) super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return user;
    }

    @Override
    public String toString() {
        return "User{" +
                "age=" + age +
                ", name='" + name + '\'' +
                ", phoneNum='" + phoneNum + '\'' +
                ", addrss=" + address +
                '}';
    }
}

四、总结

原型模式本质上就是对象拷贝。使用原型模式可以解决构建复杂对象的资源消耗问题,能够在某些场景下提升创建对象的效率。还有一个重要的用途即使保护性拷贝

优点:

  • 原型模式是在内存中二进制流的拷贝,要比直接new一个对象性能好很多,特别是要在一个循环体内产生大量的对象时,原型模式可以更好地提现其优点。

缺点:

  • 直接在内存中拷贝,构造函数是不会执行的,在实际开发当中应该注意这个潜在的问题。

学海无涯苦作舟

我的微信公众号