5.原型模式
5.1.課程目標
1、掌握原型模式和建造者模式的應用場景
2、掌握原型模式的淺克隆和深克隆的寫法。
3、掌握建造者模式的基本寫法。
4、瞭解克隆是如何破壞單例的。
5、瞭解原型模式的優、缺點
6、掌握建造者模式和工廠模式的區別。
5.2.內容定位
1、已瞭解並掌握工廠模式的人群。
2、已瞭解並掌握單例模式。
3、聽說過原型模式,但不知道應用場景的人群。
5.3.定義
原型模式(PrototypePattern)是指原型實例指定創建對象的種類,並且通過拷貝這些原型創建新的對象,屬於創建型模式。
官方原文:Specify the kinds of objects to create using a prototypical instance,and create new objects by copying this prototype.
原型模式的核心在於拷貝原型對象。以系統中已存在的一個對象為原型,直接基於內存二進制流進行拷貝,無需再經歷耗時的對象初始化過程(不調用構造函數),性能提升許多。當對象的構建過程比較耗時時,可以利用當前系統中已存在的對象作為原型,對其進行克隆(一般是基於二進制流的複製),躲避初始化過程,使得新對象的創建時間大大減少。下面,我們來看看原型模式類結構圖:
從 UML 圖中,我們可以看到,原型模式 主要包含三個角色:
客戶(Client):客戶類提出創建對象的請求。
抽象原型(Prototype):規定拷貝接口。
具體原型(Concrete Prototype):被拷貝的對象。
注:對不通過 new 關鍵字,而是通過對象拷貝來實現創建對象的模式就稱作原型模式。
5.4.原型模式的應用場景
你一定遇到過大篇幅getter、setter賦值的場景。
代碼非常工整,命名非常規範,註釋也寫的很全面,其實這就是原型模式的需求場景。但是,大家覺 得這樣的代碼優雅嗎?我認為,這樣的代碼屬於純體力勞動。那原型模式,能幫助我們解決這樣的問題。
原型模式主要適用於以下場景:
1、類初始化消耗資源較多。
2、new產生的一個對象需要非常繁瑣的過程(數據準備、訪問權限等)
3、構造函數比較複雜。
4、循環體中生產大量對象時。
在 Spring 中,原型模式應用得非常廣泛。例如 scope=“prototype”,在我們經常用的 JSON.parseObject()也是一種原型模式。
5.5.原型模式的通用寫法(淺拷貝)
一個標準的原型模式代碼,應該是這樣設計的。先創建原型IPrototype接口:
<code> public interface IPrototype{ /<code>
T clone();
}
創建具體需要克隆的對象ConcretePrototype
<code> public class ConcretePrototype implements IPrototype {
private int age;
private String name;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public ConcretePrototype clone() {
ConcretePrototype concretePrototype = new ConcretePrototype();
concretePrototype.setAge(this.age);
concretePrototype.setName(this.name);
return concretePrototype;
}
@Override
public String toString() {
return "ConcretePrototype{" +
"age=" + age +
", name='" + name + '\\'' +
'}';
}
}/<code>
測試代碼:
<code> public class Client {
public static void main(String[] args) {
//創建原型對象
ConcretePrototype prototype = new ConcretePrototype();
prototype.setAge(18);
prototype.setName("Tom");
System.out.println(prototype);
//拷貝原型對象
ConcretePrototype cloneType = prototype.clone();
System.out.println(cloneType);
}
}/<code>
運行結果:
<code> ConcretePrototype{age=18, name='Tom'}
ConcretePrototype{age=18, name='Tom'}/<code>
這時候,有小夥伴就問了,原型模式就這麼簡單嗎?對,就是這麼簡單。在這個簡單的場景之下,看上 去操作好像變複雜了。但如果有幾百個屬性需要複製,那我們就可以一勞永逸。但是,上面的複製過程 是我們自己完成的,在實際編碼中,我們一般不會浪費這樣的體力勞動,JDK已經幫我們實現了一個現 成的API,我們只需要實現Cloneable接口即可。來改造一下代碼,修改ConcretePrototype類:
<code> @Data
public class ConcretePrototype implements Cloneable {
private int age;
private String name;
@Override
public ConcretePrototype clone() {
try {
return (ConcretePrototype)super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
return null;
}
}
}/<code>
重新運行,也會得到同樣的結果。
有了JDK的支持再多的屬性複製我們也能輕而易舉地搞定了。下面我 們再來做一個測試,給ConcretePrototype增加一個個人愛好的屬性hobbies:
<code> @Data
public class ConcretePrototype implements Cloneable {
private int age;
private String name;
private List<string> hobbies;
@Override
public ConcretePrototype clone() {
try {
return (ConcretePrototype)super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
return null;
}
}
}/<string>/<code>
修改客戶端測試代碼:
<code> public class Client {
public static void main(String[] args) {
//創建原型對象
ConcretePrototype prototype = new ConcretePrototype();
prototype.setAge(18);
prototype.setName("Tom");
List<string> hobbies = new ArrayList<string>();
hobbies.add("書法");
hobbies.add("美術");
prototype.setHobbies(hobbies);
//拷貝原型對象
ConcretePrototype cloneType = prototype.clone();
cloneType.getHobbies().add("技術控");
System.out.println("原型對象:" + prototype);
System.out.println("克隆對象:" + cloneType);
System.out.println(prototype == cloneType);
System.out.println("原型對象的愛好:" + prototype.getHobbies());
System.out.println("克隆對象的愛好:" + cloneType.getHobbies());
System.out.println(prototype.getHobbies() == cloneType.getHobbies());
}
}/<string>/<string>/<code>
運行結果:
<code> 原型對象:ConcretePrototype(age=18, name=Tom, hobbies=[書法, 美術, 技術控])
克隆對象:ConcretePrototype(age=18, name=Tom, hobbies=[書法, 美術, 技術控])
false
原型對象的愛好:[書法, 美術, 技術控]
克隆對象的愛好:[書法, 美術, 技術控]
true/<code>
我們給,複製後的克隆對象新增一項愛好,發現原型對象也發生了變化,這顯然不符合我們的預期。 因為我們希望克隆出來的對象應該和原型對象是兩個獨立的對象,不應該再有聯繫了。從測試結果分析 來看,應該是hobbies共用了一個內存地址,意味著複製的不是值,而是引用的地址。這樣的話,如果我們修改任意一個對象中的屬性值,prototype 和cloneType的hobbies值都會改變。這就是我們常 說的淺克隆。只是完整複製了值類型數據,沒有賦值引用對象。換言之,所有的引用對象仍然指向原來 的對象,顯然不是我們想要的結果。那如何解決這個問題呢?下面我們來看深度克隆繼續改造。
擴展知識:String對象在內存中是不可變的(final類型),雖然克隆後,兩個對象String的引用指向的是同一個內存地址,但是如果給克隆後的對象的String屬性改變值,那麼相當於是在內存中重新開闢了一塊內存來存儲這個改變的值,而此時的String屬性對象就指向了該內存值,所以這個時候克隆前和克隆後對象的String屬性是不一樣的)。
String 每次賦值,相當於new String()。
5.6.使用序列化實現深度克隆
在上面的基礎上我們繼續改造,來看代碼,增加一個deepClone()方法:
<code> @Data
public class ConcretePrototype implements Cloneable,Serializable {
private int age;
private String name;
private List<string> hobbies;
@Override
public ConcretePrototype clone() {
try {
return (ConcretePrototype)super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
return null;
}
}
public ConcretePrototype deepClone(){
try {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(this);
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
return (ConcretePrototype)ois.readObject();
}catch (Exception e){
e.printStackTrace();
return null;
}
}
}/<string>/<code>
來看客戶端調用代碼:
<code> public class Client {
public static void main(String[] args) {
//創建原型對象
ConcretePrototype prototype = new ConcretePrototype();
prototype.setAge(18);
prototype.setName("Tom");
List<string> hobbies = new ArrayList<string>();
hobbies.add("書法");
hobbies.add("美術");
prototype.setHobbies(hobbies);
//拷貝原型對象
ConcretePrototype cloneType = prototype.deepClone();
cloneType.getHobbies().add("技術控");
System.out.println("原型對象:" + prototype);
System.out.println("克隆對象:" + cloneType);
System.out.println(prototype == cloneType);
System.out.println("原型對象的愛好:" + prototype.getHobbies());
System.out.println("克隆對象的愛好:" + cloneType.getHobbies());
System.out.println(prototype.getHobbies() == cloneType.getHobbies());
}
}/<string>/<string>/<code>
運行程序,我們發現得到了我們期望的結果:
<code> 原型對象:ConcretePrototype(age=18, name=Tom, hobbies=[書法, 美術])
克隆對象:ConcretePrototype(age=18, name=Tom, hobbies=[書法, 美術, 技術控])
false
原型對象的愛好:[書法, 美術]
克隆對象的愛好:[書法, 美術, 技術控]
false/<code>
5.7.克隆破壞單例模式
如果我們克隆的目標的對象是單例對象,那意味著,深克隆就會破壞單例。實際上防止克隆破壞單 例解決思路非常簡單,禁止深克隆便可。要麼你我們的單例類不實現 Cloneable 接口;要麼我們重寫 clone()方法,在clone方法中返回單例對象即可,具體代碼如下:
<code> @Override
protected Object clone() throws CloneNotSupportedException {
return INSTANCE;
}/<code>
5.8.原型模式在源碼中的應用
先來JDK中Cloneable接口:
<code> public interface Cloneable {
}/<code>
接口定義還是很簡單的,我們找源碼其實只需要找到看哪些接口實現了 Cloneable 即可。來看 ArrayList類的實現。
Object方法
<code> protected native Object clone() throws CloneNotSupportedException;/<code>
ArrayList是實現的clone方法
<code> public class ArrayListextends AbstractList /<code>
implements List, RandomAccess, Cloneable, java.io.Serializable
{
public Object clone() {
try {
ArrayList> v = (ArrayList>) super.clone();
v.elementData = Arrays.copyOf(elementData, size);
v.modCount = 0;
return v;
} catch (CloneNotSupportedException e) {
// this shouldn't happen, since we are Cloneable
throw new InternalError(e);
}
}
}
我們發現方法中只是將List中的元素循環遍歷了一遍。這個時候我們再思考一下,是不是這種形式 就是深克隆呢?其實用代碼驗證一下就知道了,繼續修改 ConcretePrototype 類,增加一個 deepCloneHobbies()方法:
<code> @Data
public class ConcretePrototype implements Cloneable,Serializable {
...
public ConcretePrototype deepCloneHobbies(){
try {
ConcretePrototype result = (ConcretePrototype)super.clone();
result.hobbies = (List)((ArrayList)result.hobbies).clone();
return result;
} catch (CloneNotSupportedException e) {
e.printStackTrace();
return null;
}
}
...
}/<code>
修改客戶端代碼:
<code> public class Client {
public static void main(String[] args) {
...
//拷貝原型對象
ConcretePrototype cloneType = prototype.deepCloneHobbies();
...
}
}/<code>
運行也能得到期望的結果。但是這樣的代碼,其實是硬編碼,如果在對象中聲明瞭各種集合類型, 那每種情況都需要單獨處理。因此,深克隆的寫法,一般會直接用序列化來操作。
5.9.原型模式的優缺點
優點:
1、性能優良,Java自帶的 原型模式 是基於內存二進制流的拷貝,比直接new一個對象性能上提 升了許多。
2、可以使用深克隆方式保存對象的狀態,使用原型模式將對象複製一份並將其狀態保存起來,簡化 了創建對象的過程,以便在需要的時候使用(例如恢復到歷史某一狀態),可輔助實現撤銷操作。
缺點:
1、需要為每一個類配置一個克隆方法。
2、克隆方法位於類的內部,當對已有類進行改造的時候,需要修改代碼,違反了開閉原則。
3、在實現深克隆時需要編寫較為複雜的代碼,而且當對象之間存在多重嵌套引用時,為了實現深克 隆,每一層對象對應的類都必須支持深克隆,實現起來會比較麻煩。因此,深拷貝、淺拷貝需要運用得 當。
6.0總結
克隆方式:1.序列化 反序列化 2.jsonobject 3淺克隆加賦值
淺克隆:繼承Cloneable接口的都是淺克隆。
深克隆兩種方式:序列化,轉JSON。
6.建造者模式
6.1.定義
建造者模式(Builder Pattern)是將一個複雜對象的構建過程與它的表示分離,使得同樣的構建過 程可以創建不同的表示,屬於創建型模式。使用建造者模式對於用戶而言只需指定需要建造的類型就可 以獲得對象,建造過程及細節不需要了解。
官方原文:Separate the construction of a complex object from its representation so that the same construction process can create different representations.
建造者模式適用於創建對象需要很多步驟,但是步驟的順序不一定固定。如果一個對象有非常複雜 的內部結構(很多屬性),可以將複雜對象的創建和使用進行分離。先來看一下建造者模式的類圖:
建造者模式的設計中主要有四個角色:
1、產品(Product):要創建的產品類對象
2、建造者抽象(Builder):建造者的抽象類,規範產品對象的各個組成部分的建造,一般由子類 實現具體的建造過程。
3、建造者(ConcreteBuilder):具體的Builder類,根據不同的業務邏輯,具體化對象的各個組成 部分的創建。
4、調用者(Director):調用具體的建造者,來創建對象的各個部分,在指導者中不涉及具體產品 的信息,只負責保證對象各部分完整創建或按某種順序創建。
6.2.建造者模式的應用場景
建造者模式適用於一個具有較多的零件的複雜產品的創建過程,由於需求的變化,組成這個複雜產 品的各個零件經常猛烈變化,但是它們的組合方式卻相對穩定。
- 相同的方法,不同的執行順序,產生不同的結果時
- 多個部件或零件,都可以裝配到一個對象中,但是產生的結果又不相同。
- 產品類非常複雜,或者產品類中的調用順序不同產生不同的作用。
- 當初始化一個對象特別複雜,參數多,而且很多參數都具有默認值時。
建造者模式,只關注用戶需要什麼,將最少的關鍵字傳過來,生成你想要的結果。
實際順序是在build方法裡面。那是順序和條件都確定了。每個順序和條件都分別存儲下來了。判斷有沒有,有就添加到product後面。當然就是先判斷條件再判斷order順序了
6.3.建造者模式的基本寫法
我們還是以課程為例,一個完整的課程需要由PPT課件、回放視頻、課堂筆記、課後作業組成,但 是這些內容的設置順序可以隨意調整,我們用建造者模式來代入理解一下。首先我們創建一個需要構造 的產品類Course:
<code> @Data
public class Course {
private String name;
private String ppt;
private String video;
private String note;
private String homework;
}/<code>
然後創建建造者類CourseBuilder,將複雜的構造過程封裝起來,構造步驟由用戶決定:
<code> public class CourseBuilder{
private Course course = new Course();
public void addName(String name) {
course.setName(name);
}
public void addPPT(String ppt) {
course.setPpt(ppt);
}
public void addVideo(String video) {
course.setVideo(video);
}
public void addNote(String note) {
course.setNote(note);
}
public void addHomework(String homework) {
course.setHomework(homework);
}
public Course build() {
return course;
}
}/<code>
編寫測試類:
<code> public class Test {
public static void main(String[] args) {
CourseBuilder builder = new CourseBuilder();
builder.addName("設計模式");
builder.addPPT("【PPT課件】");
builder.addVideo("【回放視頻】");
builder.addNote("【課堂筆記】");
builder.addHomework("【課後作業】");
System.out.println(builder.build());
}
}/<code>
運行結果:
<code> Course(name=設計模式, ppt=【PPT課件】, video=【回放視頻】, note=【課堂筆記】, homework=【課後作業】)/<code>
來看一下類結構圖:
6.4.建造者模式的鏈式寫法
在平時的應用中,建造者模式通常是採用鏈式編程的方式構造對象,下面我們來一下演示代碼,修 改CourseBuilder類,將Course變為CourseBuilder的內部類。然後,將構造步驟添加進去,每完成一個步驟,都返回this:
<code> public class CourseBuilder {
private Course course = new Course();
public CourseBuilder addName(String name) {
course.setName(name);
return this;
}
public CourseBuilder addPPT(String ppt) {
course.setPpt(ppt);
return this;
}
public CourseBuilder addVideo(String video) {
course.setVideo(video);
return this;
}
public CourseBuilder addNote(String note) {
course.setNote(note);
return this;
}
public CourseBuilder addHomework(String homework) {
course.setHomework(homework);
return this;
}
public Course build() {
return this.course;
}
@Data
public class Course {
private String name;
private String ppt;
private String video;
private String note;
private String homework;
}
}/<code>
客戶端使用:
<code> public class Test {
public static void main(String[] args) {
CourseBuilder builder = new CourseBuilder()
.addName("設計模式")
.addPPT("【PPT課件】")
.addVideo("【回放視頻】")
.addNote("【課堂筆記】")
.addHomework("【課後作業】");
System.out.println(builder.build());
}
}/<code>
這樣寫法是不是很眼熟,好像在哪見過呢?後面我們分析建造者模式在源碼中的應用大家就會明白。 接下來,我們再來看一下類圖的變化:
6.5.建造者模式應用案例
下面我們再來看一個實戰案例,這個案例參考了開源框架JPA的SQL構造模式。是否記得我們在構 造SQL查詢條件的時候,需要根據不同的條件來拼接SQL字符串。如果查詢條件複雜的時候,我們SQL 拼接的過程也會變得非常複雜,從而給我們的代碼維護帶來非常大的困難。因此,我們用建造者類 QueryRuleSqlBuilder 將複雜的構造 SQL 過程進行封裝,用 QueryRule 對象專門保存 SQL 查詢時的 條件,最後根據查詢條件,自動生成SQL語句。來看代碼,先創建QueryRule類:
<code> /**
* QueryRule,主要功能用於構造查詢條件
*/
public final class QueryRule implements Serializable
{
...
/**
* 添加升序規則
* @param propertyName
* @return
*/
public QueryRule addAscOrder(String propertyName) {
this.ruleList.add(new Rule(ASC_ORDER, propertyName));
return this;
}
public QueryRule andEqual(String propertyName, Object value) {
this.ruleList.add(new Rule(EQ, propertyName, new Object[] { value }).setAndOr(AND));
return this;
}
public QueryRule andLike(String propertyName, Object value) {
this.ruleList.add(new Rule(LIKE, propertyName, new Object[] { value }).setAndOr(AND));
return this;
}
...
}/<code>
然後,創建QueryRuleSqlBuilder類:
<code> /**
* 根據QueryRule自動構建sql語句
*/
public class QueryRuleSqlBuilder {
...
/**
* 處理like
* @param rule
*/
private void processLike(QueryRule.Rule rule) {
if (ArrayUtils.isEmpty(rule.getValues())) {
return;
}
Object obj = rule.getValues()[0];
if (obj != null) {
String value = obj.toString();
if (!StringUtils.isEmpty(value)) {
value = value.replace('*', '%');
obj = value;
}
}
add(rule.getAndOr(),rule.getPropertyName(),"like","%"+rule.getValues()[0]+"%");
}
/**
* 處理 =
* @param rule
*/
private void processEqual(QueryRule.Rule rule) {
if (ArrayUtils.isEmpty(rule.getValues())) {
return;
}
add(rule.getAndOr(),rule.getPropertyName(),"=",rule.getValues()[0]);
}
/**
* 處理 order by
* @param rule 查詢規則
*/
private void processOrder(Rule rule) {
switch (rule.getType()) {
case QueryRule.ASC_ORDER:
// propertyName非空
if (!StringUtils.isEmpty(rule.getPropertyName())) {
orders.add(Order.asc(rule.getPropertyName()));
}
break;
case QueryRule.DESC_ORDER:
// propertyName非空
if (!StringUtils.isEmpty(rule.getPropertyName())) {
orders.add(Order.desc(rule.getPropertyName()));
}
break;
default:
break;
}
}
...
}/<code>
創建Order類:
<code> /**
* sql排序組件
*/
public class Order {
private boolean ascending; //升序還是降序
private String propertyName; //哪個字段升序,哪個字段降序
public String toString() {
return propertyName + ' ' + (ascending ? "asc" : "desc");
}
/**
* Constructor for Order.
*/
protected Order(String propertyName, boolean ascending) {
this.propertyName = propertyName;
this.ascending = ascending;
}
/**
* Ascending order
*
* @param propertyName
* @return Order
*/
public static Order asc(String propertyName) {
return new Order(propertyName, true);
}
/**
* Descending order
*
* @param propertyName
* @return Order
*/
public static Order desc(String propertyName) {
return new Order(propertyName, false);
}
}/<code>
編寫測試代碼:
<code> public class Test {
public static void main(String[] args) {
QueryRule queryRule = QueryRule.getInstance();
queryRule.addAscOrder("age");
queryRule.andEqual("addr","Changsha");
queryRule.andLike("name","Tom");
QueryRuleSqlBuilder builder = new QueryRuleSqlBuilder(queryRule);
System.out.println(builder.builder("t_member"));
System.out.println("Params: " + Arrays.toString(builder.getValues()));
}
}/<code>
這樣一來,我們的客戶端代碼就非常清朗,來看運行結果:
<code> select * from t_member where addr = ? and name like ? order by age asc
Params: [Changsha, %Tom%]/<code>
6.6.建造者模式在源碼中的體現
下面來看建造者模式在哪些源碼中有應用呢?首先來看JDK的StringBuilder,它提供append()方 法,給我們開放構造步驟,最後調用toString()方法就可以獲得一個構造好的完整字符串,源碼如下:
<code> public final class StringBuilder
extends AbstractStringBuilder
implements java.io.Serializable, CharSequence
{
public StringBuilder append(StringBuffer sb) {
super.append(sb);
return this;
}
}/<code>
在MyBatis中也有體現,比如CacheBuilder類。
同樣在 MyBatis 中,比如 SqlSessionFactoryBuilder 通過調用 build()方法獲得的是一個 SqlSessionFactory 類。
當然,在 Spring中自然也少不了,比如 BeanDefinitionBuilder 通過調用getBeanDefinition()方法獲得一個BeanDefinition對象。
6.7.建造者模式的優缺點
建造者模式的優點:
1、封裝性好,創建和使用分離;
2、擴展性好,建造類之間獨立、一定程度上解耦。
建造者模式的缺點:
1、產生多餘的Builder對象;
2、產品內部發生變化,建造者都要修改,成本較大。
6.8.建造者模式和工廠模式的區別
建造者模式和工廠模式的區別
1、建造者模式更加註重方法的調用順序,工廠模式注重於創建對象。
2、創建對象的力度不同,建造者模式創建複雜的對象,由各種複雜的部件組成,工廠模式創建出來 的都一樣。
3、關注重點不一樣,工廠模式模式只需要把對象創建出來就可以了,而建造者模式中不僅要創建出 這個對象,還要知道這個對象由哪些部件組成。
4、建造者模式根據建造過程中的順序不一樣,最終的對象部件組成也不一樣。
可以理解為工廠創建過程是靜態的,構建者模式創建過程經過外放而變成動態的。
6.9.總結
7.0.作業
1.用JSON方式實現一個原型模式的深克隆,並畫出UML圖。
一行代碼,比IO流簡單。
<code> public ConcretePrototype deepCloneByJSON(){
try {
return JSON.parseObject(JSON.toJSONString(this), ConcretePrototype.class);
}catch (Exception e){
e.printStackTrace();
return null;
}
}/<code>
2.請列舉1-3個需要用到建造者模式的業務場景。
建造者模式:適用於對象創建需要動態拼接複雜屬性值的業務場景。
例如:SQL拼接,鏈式編程,NIO,StringBuilder.append()方法。
閱讀更多 我是阿喵醬 的文章