設計模式---原型模式

簡述

  • 類型:創建型
  • 目標:通過拷貝快速創建相同或相似對象。

接下來我們看一個需要改進的案例。

優化案例

話不多說,先來看一個創建相同或相似對象的傳統寫法。

原版v0

public class Department {
    private String name;
    private String country;
    private String province;
    private String city;
    private List<Employee> employees;
    public String getName() {
        return name;
    }
    public String getCountry() {
        return country;
    }
    public String getProvince() {
        return province;
    }
    public String getCity() {
        return city;
    }
    public List<Employee> getEmployees() {
        return employees;
    }
    public Department(String name, String country, String province,
                      String city, List<Employee> employees) {
        this.name = name;
        this.country = country;
        this.province = province;
        this.city = city;
        this.employees = employees;
    }
}
class Employee {
    private String name;
    private String sex;
    private int age;
    private String country;
    private String province;
    private String city;
    private String post;
    public String getName() {
        return name;
    }
    public String getSex() {
        return sex;
    }
    public int getAge() {
        return age;
    }
    public String getCountry() {
        return country;
    }
    public String getProvince() {
        return province;
    }
    public String getCity() {
        return city;
    }
    public String getPost() {
        return post;
    }
    public Employee(String name, String sex, int age,
                    String country, String province,
                    String city, String post) {
        this.name = name;
        this.sex = sex;
        this.age = age;
        this.country = country;
        this.province = province;
        this.city = city;
        this.post = post;
    }
}

已知一個Department類型的對象,我們想構造一個相似的對象。

public static void main(String[] args) {
    Employee emp = new Employee("張三", "男", 15, "中國", "江西省", "南昌市", "124-1241-1353");
    Department department = new Department("開發部", "中國", "江西省", "南昌市", List.of(e)); // 已知對象
    Department department1 = new Department(department.getName(), department.getCountry(), department.getProvince(), department.getCity(), department.getPost()); // 拷貝對象
}

可以感受到,對象拷貝的樸素寫法非常的麻煩。而且想到每一處對象拷貝都需要這樣寫就感覺頭皮發麻。

為了解決這個問題,我們引入原型模式。請看以下樣例。

修改版v1(淺拷貝)

public class Department {
    private String name;
    private String country;
    private String province;
    private String city;
    private List<Employee> employees;
    public Department(String name, String country, String province,
                      String city, List<Employee> employees) {
        this.name = name;
        this.country = country;
        this.province = province;
        this.city = city;
        this.employees = employees;
    }
}
class Employee {
    private String name;
    private String sex;
    private int age;
    private String country;
    private String province;
    private String city;
    private String post;
    public Employee(String name, String sex, int age,
                    String country, String province,
                    String city, String post) {
        this.name = name;
        this.sex = sex;
        this.age = age;
        this.country = country;
        this.province = province;
        this.city = city;
        this.post = post;
    }
}

使用clone()方法拷貝目標對象。

public static void main(String[] args) throws CloneNotSupportedException {
    Employee e = new Employee("張三", "男", 15, "中國", "江西省", "南昌市", "124-1241-1353");
    Department department = new Department("開發部", "中國", "江西省", "南昌市", List.of(e));
    Department department1 = (Department)department.clone();
    System.out.println(department == department1); // false
    System.out.println(department.employees == department1.employees); // true
}

我們發現第8行輸出true,這說明兩個對象的employees的引用相同,這會導致修改其中一個employees的元素會影響到另一個,這並不好。

如何解決屬性相同引用的問題?看以下樣例。

修改版v2(深拷貝)

public class Department implements Cloneable {
    private String name;
    private String country;
    private String province;
    private String city;
    private List<Employee> employees;
    public Department(String name, String country, String province,
                      String city, List<Employee> employees) {
        this.name = name;
        this.country = country;
        this.province = province;
        this.city = city;
        this.employees = employees;
    }
    @Override
    public Object clone() throws CloneNotSupportedException {
        Department department = (Department)super.clone();
        List<Employee> emps = new ArrayList<>();
        for (int i = 0; i < department.employees.size(); i ++) {
            emps.add((Employee) employees.get(i).clone());
        }
        department.employees = emps;
        return department;
    }
}
class Employee implements Cloneable {
    private String name;
    private String sex;
    private int age;
    private String country;
    private String province;
    private String city;
    private String post;
    public Employee(String name, String sex, int age,
                    String country, String province,
                    String city, String post) {
        this.name = name;
        this.sex = sex;
        this.age = age;
        this.country = country;
        this.province = province;
        this.city = city;
        this.post = post;
    }
    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

使用clone() 拷貝對象,因為類以及類中的屬性也重寫了clone()。

public static void main(String[] args) throws CloneNotSupportedException {
    Employee e = new Employee("張三", "男", 15, "中國", "江西省", "南昌市", "124-1241-1353");
    Department department = new Department("開發部", "中國", "江西省", "南昌市", List.of(e));
    Department department1 = (Department)department.clone();
    System.out.println(department == department1); // false
    System.out.println(department.employees == department1.employees); // false
}

雖然這種方式可以深拷貝,但是這會讓程式碼量激增。

序列化與反序列化可以解決這個問題。

修改版v3(序列化與反序列化)(推薦使用)

public class Department {
    private String name;
    private String country;
    private String province;
    private String city;
    private List<Employee> employees;
    public Department(String name, String country, String province,
                      String city, List<Employee> employees) {
        this.name = name;
        this.country = country;
        this.province = province;
        this.city = city;
        this.employees = employees;
    }
}
class Employee {
    private String name;
    private String sex;
    private int age;
    private String country;
    private String province;
    private String city;
    private String post;
    public Employee(String name, String sex, int age,
                    String country, String province,
                    String city, String post) {
        this.name = name;
        this.sex = sex;
        this.age = age;
        this.country = country;
        this.province = province;
        this.city = city;
        this.post = post;
    }
}

序列化與反序列化的實現方式有很多種,本文使用Gson來實現。以下是樣例。

public static void main(String[] args) throws CloneNotSupportedException {
    Employee e = new Employee("張三", "男", 15, "中國", "江西省", "南昌市", "124-1241-1353");
    Department department = new Department("開發部", "中國", "江西省", "南昌市", List.of(e));
    Gson gson = new Gson();
    String s = gson.toJson(department);
    Department department1 = s.fromJson(s, Department.class);
    System.out.println(department == department1); // false
    System.out.println(department.employees == department1.employees); // false
}

基於序列化和反序列化實現的克隆不僅僅是深度克隆,更重要的是通過泛型限定,可以檢查出要克隆的對象是否支援序列化,這項檢查是編譯器完成的,不是在運行時拋出異常,這種是方案明顯優於使用Object類的clone方法克隆對象。讓問題在編譯的時候暴露出來總是優於把問題留到運行時。

總結

優點

  1. 由於是直接從記憶體中讀取對象進行克隆,所以性能卓越。
  2. 程式碼量不論是相較於傳統寫法要精簡很多,尤其是序列化與反序列化的方式。

缺點

  1. 程式碼的理解難度增加。尤其是深拷貝的理解較為複雜。

適用場景

  1. 適用於只有細微參數變動的對象創建。
  2. 適用於需要備份的場景。如,當業務執行過程中,某種情況下需要數據回滾的時候,提前備份可以使用。