Android常用設(shè)計模式之原型模式詳解
前言
什么是原型模式?
它是指創(chuàng)建對象的種類,并通過拷貝這些原型創(chuàng)建新的對象。
它是用于創(chuàng)建重復(fù)的對象,同時又能保證性能。這種類型的設(shè)計模式屬于創(chuàng)建型模式,它提供了一種創(chuàng)建對象的最佳方式。
這種模式是實現(xiàn)了一個原型接口,該接口用于創(chuàng)建當(dāng)前對象的克隆。當(dāng)直接創(chuàng)建對象的代價比較大時,則采用這種模式。
原型模式的工作原理很簡單:將一個原型對象傳給那個要發(fā)動創(chuàng)建的對象,這個要發(fā)動創(chuàng)建的對象通過請求原型對象拷貝自己來實現(xiàn)創(chuàng)建過程。由于在軟件系統(tǒng)中我們經(jīng)常會遇到需要創(chuàng)建多個相同或者相似對象的情況,因此原型模式在真實開發(fā)中的使用頻率還是非常高的。
一、基本使用
固定的用法就是 實現(xiàn) Cloneable 接口 ,重寫 clone()方法。
public class UserProfile implements Cloneable {
public String userId;
public String name;
public String age;
public UserProfile() {
}
public UserProfile(String userId, String name, String age) {
this.userId = userId;
this.name = name;
this.age = age;
}
@NonNull
@Override
public UserProfile clone() throws CloneNotSupportedException {
return (UserProfile) super.clone();
}
}
使用:
val userProfile = UserProfile("1", "張三", "30")
YYLogUtils.w("userProfile:$userProfile name:" + userProfile.name + " age:" + userProfile.age + " skill:" + userProfile.skills)
try {
val newUser = userProfile.clone()
newUser.name = "李四"
YYLogUtils.w("userProfile:$newUser name:" + newUser.name + " age:" + newUser.age + " skill:" + newUser.skills)
} catch (e: Exception) {
e.printStackTrace()
}


二、對象與集合的使用
對集合與對象的的處理需要額外的注意。
public class UserProfile implements Cloneable {
public String userId;
public String name;
public String age;
public List<String> skills;
public UserProfile() {
}
public UserProfile(String userId, String name, String age) {
this.userId = userId;
this.name = name;
this.age = age;
}
@NonNull
@Override
public UserProfile clone() throws CloneNotSupportedException {
UserProfile profile = (UserProfile) super.clone();
profile.name = this.name;
profile.age = this.age;
profile.userId = this.userId;
profile.skills = (List<String>) this.skills.clone();
return profile;
}
}
例如我們自己處理數(shù)據(jù)的賦值,那么就會出現(xiàn)這樣的問題,List是無法clone的。
如果強制這么使用
val userProfile = UserProfile("1", "張三", "30")
val skills = listOf("籃球", "游泳", "長跑", "Java")
userProfile.skills = skills
YYLogUtils.w("userProfile:$userProfile name:" + userProfile.name + " age:" + userProfile.age + " skill:" + userProfile.skills)
try {
val newUser = userProfile.clone()
newUser.name = "李四"
newUser.skills.add("H5")
YYLogUtils.w("userProfile:$newUser name:" + newUser.name + " age:" + newUser.age + " skill:" + newUser.skills)
} catch (e: Exception) {
e.printStackTrace()
}
就會報錯:

我們需要改為ArrayList,因為只有它才顯示了Cloneable,也就是只有內(nèi)部成員屬性都實現(xiàn) Cloneable 接口才可以使用

那我們修改類的成員定義為ArrayList
public class UserProfile implements Cloneable {
public String userId;
public String name;
public String age;
public ArrayList<String> skills = new ArrayList<>();
public UserProfile() {
}
public UserProfile(String userId, String name, String age) {
this.userId = userId;
this.name = name;
this.age = age;
}
@NonNull
@Override
public UserProfile clone() throws CloneNotSupportedException {
//兩種方法都可以
// return (UserProfile) super.clone();
UserProfile profile = (UserProfile) super.clone();
profile.name = this.name;
profile.age = this.age;
profile.userId = this.userId;
profile.skills = (ArrayList<String>) this.skills.clone();
return profile;
}
}
我們就能打印正確的拷貝值:

集合是可以了,那么我們能不能添加自定義的對象呢?又會怎么樣?
我們再UserProfile類中添加一個Address的對象
public class UserProfile implements Cloneable {
public String userId;
public String name;
public String age;
public UserAddress address;
public ArrayList<String> skills = new ArrayList<>();
public UserProfile() {
}
public UserProfile(String userId, String name, String age) {
this.userId = userId;
this.name = name;
this.age = age;
}
@NonNull
@Override
public UserProfile clone() throws CloneNotSupportedException {
return (UserProfile) super.clone();
}
}
測試一下賦值與拷貝
fun prototypeTest() {
val userProfile = UserProfile("1", "張三", "30")
val skills = arrayListOf("籃球", "游泳", "長跑", "Java")
userProfile.address = UserAddress("武漢", "楚河漢街")
userProfile.skills = skills
YYLogUtils.w(
"userProfile:$userProfile name:" + userProfile.name + " age:" + userProfile.age +
" skill:" + userProfile.skills + "address:" + userProfile.address + " address-city:" + userProfile.address.city
)
try {
val newUser = userProfile.clone()
newUser.name = "李四"
newUser.skills.add("H5")
newUser.address.city = "長沙"
YYLogUtils.w(
"userProfile:$newUser name:" + newUser.name + " age:" + newUser.age +
" skill:" + newUser.skills + "address:" + newUser.address + " address-city:" + newUser.address.city
)
YYLogUtils.w(
"userProfile:$userProfile name:" + userProfile.name + " age:" + userProfile.age +
" skill:" + userProfile.skills + "address:" + userProfile.address + " address-city:" + userProfile.address.city
)
} catch (e: Exception) {
e.printStackTrace()
}
}
打印的結(jié)果如下:

可以看到 userProfile 與 newUser 中的 Address 是同一個對象,如果一個對象修改了 Address 的值,那么另一個對象的值就改了。
要修改起來其實很簡單,我們把 Address 對象重寫 Cloneable 并實現(xiàn) clone 方法。
public class UserAddress implements Cloneable {
public String city;
public String address;
public UserAddress() {
}
public UserAddress(String city, String address) {
this.city = city;
this.address = address;
}
@NonNull
@Override
public UserAddress clone() throws CloneNotSupportedException {
return (UserAddress) super.clone();
}
}
然后在UserProfile的clone方法中手動的對對象賦值
public class UserProfile implements Cloneable {
public String userId;
public String name;
public String age;
public UserAddress address;
public ArrayList<String> skills = new ArrayList<>();
public UserProfile() {
}
public UserProfile(String userId, String name, String age) {
this.userId = userId;
this.name = name;
this.age = age;
}
@NonNull
@Override
public UserProfile clone() throws CloneNotSupportedException {
UserProfile profile = (UserProfile) super.clone();
// 把地址也做一次克隆,達(dá)到深拷貝
profile.address = address.clone();
return profile;
}
}
我們再次運行就可以得到我們想要的結(jié)果了:

三、淺拷貝與深拷貝
淺拷貝只會復(fù)制值,深拷貝不僅會復(fù)制值也會復(fù)制引用
淺拷貝又叫影子拷貝,上面我們在拷貝文檔時并沒有把原文檔中的字段都重新構(gòu)造了一遍,而只是拷貝了引用,也就是Address字段引用了之前的Address字段,這樣的話修改新的Address中的內(nèi)容就會連原Address也改掉了,這就是淺拷貝。
深拷貝就是在淺拷貝的基礎(chǔ)上,對于引用類型的字段也要采用拷貝的形式,比如上面的Address,而像String、int這些基本數(shù)據(jù)類型則沒關(guān)系
平常的開發(fā)中更推薦大家使用深拷貝,就是對象實現(xiàn) Cloneable 并重寫 clone 方法。
例如這樣深拷貝:
@NonNull
@Override
public UserProfile clone() throws CloneNotSupportedException {
UserProfile profile = (UserProfile) super.clone();
// 把地址也做一次克隆,達(dá)到深拷貝
profile.address = address.clone();
return profile;
}
當(dāng)然還有另一種用法,既不是淺拷貝,也不是深拷貝,例如Intent的clone方法
@Override
public Object clone() {
return new Intent(this);
}
private Intent(Intent o, boolean all) {
this.mAction = o.mAction;
this.mData = o.mData;
this.mType = o.mType;
this.mPackage = o.mPackage;
this.mComponent = o.mComponent;
if (o.mCategories != null) {
this.mCategories = new ArraySet<String>(o.mCategories);
}
}
Intent 的 clone 方法實際上是通過new對象的方法來實現(xiàn)的,并沒有調(diào)用super.clone()。
四、Kotlin的應(yīng)用
那么在Kotlin中的使用又有什么不同呢?
其實在Kotlin中調(diào)用普通的類是和Java一樣的,但是如果是data class 的數(shù)據(jù)類,內(nèi)部有copy方法可以快速實現(xiàn)clone的對象。
那么它是淺拷貝還是深拷貝呢?
data class Company(
var name: String,
var year: String,
var address: UserAddress,
)
使用:
val company = Company("百度", "2001年", UserAddress("北京", "海淀區(qū)"))
YYLogUtils.w("company:$company")
val newCompany = company.copy()
newCompany.name = "網(wǎng)易"
newCompany.address.city = "杭州"
YYLogUtils.w("newCompany:$newCompany")
YYLogUtils.w("company:$company")
我們再 data class 中定義一個普通的對象 Address ,我們打印的值如下:

可以看到打印的結(jié)果,默認(rèn)的 data class copy 為淺拷貝。
那么如何實現(xiàn)深拷貝呢?我們仿造Java寫 Cloneable 和 clone() 試試。
data class Company(
var name: String,
var year: String,
var address: UserAddress,
) : Cloneable {
public override fun clone(): Company {
val company: Company = super.clone() as Company
company.address = address.clone()
return company
}
}
使用的時候我們不使用copy()而使用clone()
val company = Company("百度", "2001年", UserAddress("北京", "海淀區(qū)"))
YYLogUtils.w("company:$company addressCity:${company.address.city}")
val newCompany = company.clone()
newCompany.name = "網(wǎng)易"
newCompany.address.city = "杭州"
YYLogUtils.w("newCompany:$newCompany addressCity:${newCompany.address.city}")
YYLogUtils.w("company:$company addressCity:${company.address.city}")
那么打印的結(jié)果確實成了深拷貝,就是我們想要的。

另外一些其他的方法,例如一些第三庫 github.com/bennyhuo/Ko… 給對象 data class 加注解,讓其實現(xiàn)深拷貝的邏輯。
還有一些方法是通過Kotlin反射庫的一些方式實現(xiàn),擴(kuò)展方法如下
//data class 對象的深拷貝
fun <T : Any> T.deepCopy(): T {
//如果不是數(shù)據(jù)類,直接返回
if (!this::class.isData) {
return this
}
//拿到構(gòu)造函數(shù)
return this::class.primaryConstructor!!.let { primaryConstructor ->
primaryConstructor.parameters.map { parameter ->
//轉(zhuǎn)換類型
//最終value=第一個參數(shù)類型的對象
val value = (this::class as KClass<T>).memberProperties.first {
it.name == parameter.name
}.get(this)
//如果當(dāng)前類(這里的當(dāng)前類指的是參數(shù)對應(yīng)的類型,比如說這里如果非基本類型時)是數(shù)據(jù)類
if ((parameter.type.classifier as? KClass<*>)?.isData == true) {
parameter to value?.deepCopy()
} else {
parameter to value
}
//最終返回一個新的映射map,即返回一個屬性值重新組合的map,并調(diào)用callBy返回指定的對象
}.toMap().let(primaryConstructor::callBy)
}
}
如果報錯的話,可以看看是不是Kotlin反射庫沒有導(dǎo)入,比如我項目的Kotlin版本為1.5.31 , 那么我導(dǎo)入了一個反射庫依賴如下:
api 'org.jetbrains.kotlin:kotlin-reflect:1.5.31'
使用起來很簡單,直接使用擴(kuò)展方法即可:
val company = Company("百度", "2001年", Address("北京", "海淀區(qū)"))
YYLogUtils.w("company:$company addressCity:${company.address.city}")
val newCompany = company.deepCopy()
newCompany.name = "網(wǎng)易"
newCompany.address.city = "杭州"
YYLogUtils.w("newCompany:$newCompany addressCity:${newCompany.address.city}")
YYLogUtils.w("company:$company addressCity:${company.address.city}")
打印的結(jié)果符合我們的預(yù)期:

這種方法需要注意的是只支持 data class ,內(nèi)部的對象也需要時data class 如果是普通類或Java類的對象都是不行的。并且大家需要注意的是這么,深拷貝的性能是比淺拷貝要差一點的。特別是使用這樣擴(kuò)展方式的形式,他的性能比重寫 Cloneable 和 clone()這樣的方式性能要差。如果沒有必要還是推薦使用淺拷貝實現(xiàn)。
比如之前的文章 MVI框架的展示。我們就是使用淺拷貝,我們就是要改變內(nèi)存中List的值
private val _viewStates: MutableLiveData<Demo14ViewState> = MutableLiveData(Demo14ViewState())
//只需要暴露一個LiveData,包括頁面所有狀態(tài)
val viewStates: LiveData<Demo14ViewState> = _viewStates
//當(dāng)前頁面所需的數(shù)據(jù)與狀態(tài)
data class Demo14ViewState(
val industrys: List<Industry> = emptyList(),
val schools: List<SchoolBean> = emptyList(),
var isChanged: Boolean = false
) : BaseViewState()
//獲取學(xué)校數(shù)據(jù)
private fun requestSchool() {
viewModelScope.launch {
//開始Loading
loadStartLoading()
val result = mRepository.getSchool()
result.checkSuccess {
_viewStates.setState {
copy(schools = it ?: emptyList())
}
}
loadHideProgress()
}
}
例如上文中的copy用法,淺拷貝,當(dāng)我們調(diào)用接口獲取到學(xué)校的是時候就賦值學(xué)校數(shù)據(jù),淺拷貝一個對象,并對它賦值,再把它設(shè)置給LiveData,當(dāng)我們獲取到行業(yè)數(shù)據(jù)的時候,一樣的操作,又并不會對學(xué)校的數(shù)據(jù)做修改,又修改了原生LiveData中的Value值,因為兩個對象的引用是不同的,這樣就可以觸發(fā)LiveData的通知。也是一個比較典型的應(yīng)用。
如果是深拷貝,當(dāng)然也能實現(xiàn)同樣的邏輯,就是會麻煩一點,性能也沒有淺拷貝好,所以這個場景使用的是簡單的淺拷貝。
總結(jié)
原型模式本質(zhì)上就是對象的拷貝,容易出現(xiàn)的問題也都是深拷貝、淺拷貝。使用原型模式可以解決構(gòu)建復(fù)雜對象的資源消耗問題,能夠在某些場景下提升創(chuàng)建對象的效率。
優(yōu)點:
- 原型模式是在內(nèi)存中二進(jìn)制流的拷貝,要比直接new一個對象性能好很多,特別是要在一個循環(huán)體內(nèi)產(chǎn)生大量對象時,原型模式可能更好的體現(xiàn)其優(yōu)點。
- 還有一個重要的用途就是保護(hù)性拷貝,也就是對某個對象對外可能是只讀的,為了防止外部對這個只讀對象的修改,通??梢酝ㄟ^返回一個對象拷貝的形式實現(xiàn)只讀的限制。
缺點:
- 這既是它的優(yōu)點也是缺點,直接在內(nèi)存中拷貝,構(gòu)造函數(shù)是不會執(zhí)行的,在實際開發(fā)中應(yīng)該注意這個潛在問題。優(yōu)點是減少了約束,缺點也是減少了約束,需要大家在實際應(yīng)用時考慮。
- 通過實現(xiàn)Cloneable接口的原型模式在調(diào)用clone函數(shù)構(gòu)造實例時并不一定比通過new操作速度快,只有當(dāng)通過new構(gòu)造對象較為耗時或者說成本較高時,通過clone方法才能夠獲得效率上的提升。
平常的開發(fā)中,淺拷貝和深拷貝都有各自的應(yīng)用場景,如果 class 中都是基礎(chǔ)數(shù)據(jù),那也不需要關(guān)心是淺拷貝還是深拷貝,直接淺拷貝即可,如果包含對象,那么就要看自己的需求來選擇是淺拷貝還是深拷貝了。
以上就是Android常用設(shè)計模式之原型模式詳解的詳細(xì)內(nèi)容,更多關(guān)于Android 設(shè)計模式原型模式的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
源碼解析Android Jetpack組件之ViewModel的使用
Jetpack 是一個豐富的組件庫,它的組件庫按類別分為 4 類,分別是架構(gòu)(Architecture)、界面(UI)、 行為(behavior)和基礎(chǔ)(foundation)。本文將從源碼和大家講講Jetpack組件中ViewModel的使用2023-04-04
Android自定義View構(gòu)造函數(shù)詳解
這篇文章主要為大家詳細(xì)介紹了Android自定義View構(gòu)造函數(shù),具有一定的參考價值,感興趣的小伙伴們可以參考一下2016-10-10
android表格效果之ListView隔行變色實現(xiàn)代碼
首先繼承SimpleAdapter再使用重載的Adapter來達(dá)到效果,其實主要是需要重載SimpleAdapter,感興趣的朋友可以研究下,希望本文可以幫助到你2013-02-02
Android 快速使用正則表達(dá)式,校驗身份證號的實例
Android實現(xiàn)自定義ImageView的圓角矩形圖片效果

