原型設計模式

亦稱: 克隆、Clone、Prototype

意圖

原型模式

是一種建立型設計模式, 使你能夠複製已有物件, 而又無需使程式碼依賴它們所屬的類。

原型設計模式

問題

如果你有一個物件, 並希望生成與其完全相同的一個複製品, 你該如何實現呢? 首先, 你必須新建一個屬於相同類的物件。 然後, 你必須遍歷原始物件的所有成員變數, 並將成員變數值複製到新物件中。

不錯! 但有個小問題。 並非所有物件都能透過這種方式進行復制, 因為有些物件可能擁有私有成員變數, 它們在物件本身以外是不可見的。

原型設計模式

“從外部” 複製物件並非總是可行。

直接複製還有另外一個問題。 因為你必須知道物件所屬的類才能建立複製品, 所以程式碼必須依賴該類。 即使你可以接受額外的依賴性, 那還有另外一個問題: 有時你只知道物件所實現的介面, 而不知道其所屬的具體類, 比如可向方法的某個引數傳入實現了某個介面的任何物件。

解決方案

原型模式將克隆過程委派給被克隆的實際物件。 模式為所有支援克隆的物件聲明瞭一個通用介面, 該介面讓你能夠克隆物件, 同時又無需將程式碼和物件所屬類耦合。 通常情況下, 這樣的介面中僅包含一個

克隆

方法。

所有的類對

克隆

方法的實現都非常相似。 該方法會建立一個當前類的物件, 然後將原始物件所有的成員變數值複製到新建的類中。 你甚至可以複製私有成員變數, 因為絕大部分程式語言都允許物件訪問其同類物件的私有成員變數。

支援克隆的物件即為

原型

。 當你的物件有幾十個成員變數和幾百種類型時, 對其進行克隆甚至可以代替子類的構造。

原型設計模式

預生成原型可以代替子類的構造。

其運作方式如下: 建立一系列不同型別的物件並不同的方式對其進行配置。 如果所需物件與預先配置的物件相同, 那麼你只需克隆原型即可, 無需新建一個物件。

真實世界類比

現實生活中, 產品在得到大規模生產前會使用原型進行各種測試。 但在這種情況下, 原型只是一種被動的工具, 不參與任何真正的生產活動。

原型設計模式

一個細胞的分裂。

由於工業原型並不是真正意義上的自我複製, 因此細胞有絲分裂 (還記得生物學知識嗎?) 或許是更恰當的類比。 有絲分裂會產生一對完全相同的細胞。 原始細胞就是一個原型, 它在複製體的生成過程中起到了推動作用。

原型模式結構

基本實現

原型設計模式

原型

(Prototype) 介面將對克隆方法進行宣告。 在絕大多數情況下, 其中只會有一個名為

clone

克隆

的方法。

具體原型

(Concrete Prototype) 類將實現克隆方法。 除了將原始物件的資料複製到克隆體中之外, 該方法有時還需處理克隆過程中的極端情況, 例如克隆關聯物件和梳理遞迴依賴等等。

客戶端

(Client) 可以複製實現了原型介面的任何物件。

原型登錄檔實現

原型設計模式

原型登錄檔

(Prototype Registry) 提供了一種訪問常用原型的簡單方法, 其中儲存了一系列可供隨時複製的預生成物件。 最簡單的登錄檔原型是一個

名稱 → 原型

的雜湊表。 但如果需要使用名稱以外的條件進行搜尋, 你可以建立更加完善的登錄檔版本。

虛擬碼

在本例中,

原型

模式能讓你生成完全相同的幾何物件副本, 同時無需程式碼與物件所屬類耦合。

原型設計模式

克隆一系列位於同一類層次結構中的物件。

所有形狀類都遵循同一個提供克隆方法的介面。 在複製自身成員變數值到結果物件前, 子類可呼叫其父類的克隆方法。

// 基礎原型。abstract class Shape is field X: int field Y: int field color: string // 常規建構函式。 constructor Shape() is // 。。。 // 原型建構函式。使用已有物件的數值來初始化一個新物件。 constructor Shape(source: Shape) is this() this。X = source。X this。Y = source。Y this。color = source。color // clone(克隆)操作會返回一個形狀子類。 abstract method clone():Shape// 具體原型。克隆方法會建立一個新物件並將其傳遞給建構函式。直到建構函式運// 行完成前,它都擁有指向新克隆物件的引用。因此,任何人都無法訪問未完全生// 成的克隆物件。這可以保持克隆結果的一致。class Rectangle extends Shape is field width: int field height: int constructor Rectangle(source: Rectangle) is // 需要呼叫父建構函式來複制父類中定義的私有成員變數。 super(source) this。width = source。width this。height = source。height method clone():Shape is return new Rectangle(this)class Circle extends Shape is field radius: int constructor Circle(source: Circle) is super(source) this。radius = source。radius method clone():Shape is return new Circle(this)// 客戶端程式碼中的某個位置。class Application is field shapes: array of Shape constructor Application() is Circle circle = new Circle() circle。X = 10 circle。Y = 10 circle。radius = 20 shapes。add(circle) Circle anotherCircle = circle。clone() shapes。add(anotherCircle) // 變數 `anotherCircle(另一個圓)`與 `circle(圓)`物件的內 // 容完全一樣。 Rectangle rectangle = new Rectangle() rectangle。width = 10 rectangle。height = 20 shapes。add(rectangle) method businessLogic() is // 原型是很強大的東西,因為它能在不知曉物件型別的情況下生成一個與 // 其完全相同的複製品。 Array shapesCopy = new Array of Shapes。 // 例如,我們不知曉形狀陣列中元素的具體型別,只知道它們都是形狀。 // 但在多型機制的幫助下,當我們在某個形狀上呼叫 `clone(克隆)` // 方法時,程式會檢查其所屬的類並呼叫其中所定義的克隆方法。這樣, // 我們將獲得一個正確的複製品,而不是一組簡單的形狀物件。 foreach (s in shapes) do shapesCopy。add(s。clone()) // `shapesCopy(形狀副本)`陣列中包含 `shape(形狀)`陣列所有 // 子元素的複製品。

原型模式適合應用場景

如果你需要複製一些物件, 同時又希望程式碼獨立於這些物件所屬的具體類, 可以使用原型模式。

這一點考量通常出現在程式碼需要處理第三方程式碼透過介面傳遞過來的物件時。 即使不考慮程式碼耦合的情況, 你的程式碼也不能依賴這些物件所屬的具體類, 因為你不知道它們的具體資訊。

原型模式為客戶端程式碼提供一個通用介面, 客戶端程式碼可透過這一介面與所有實現了克隆的物件進行互動, 它也使得客戶端程式碼與其所克隆的物件具體類獨立開來。

如果子類的區別僅在於其物件的初始化方式, 那麼你可以使用該模式來減少子類的數量。 別人建立這些子類的目的可能是為了建立特定型別的物件。

在原型模式中, 你可以使用一系列預生成的、 各種型別的物件作為原型。

客戶端不必根據需求對子類進行例項化, 只需找到合適的原型並對其進行克隆即可。

實現方式

建立原型介面, 並在其中宣告

克隆

方法。 如果你已有類層次結構, 則只需在其所有類中新增該方法即可。

原型類必須另行定義一個以該類物件為引數的建構函式。 建構函式必須複製引數物件中的所有成員變數值到新建實體中。 如果你需要修改子類, 則必須呼叫父類建構函式, 讓父類複製其私有成員變數值。如果程式語言不支援方法過載, 那麼你可能需要定義一個特殊方法來複制物件資料。 在建構函式中進行此類處理比較方便, 因為它在呼叫

new

運算子後會馬上返回結果物件。

克隆方法通常只有一行程式碼: 使用

new

運算子呼叫原型版本的建構函式。 注意, 每個類都必須顯式重寫克隆方法並使用自身類名呼叫

new

運算子。 否則, 克隆方法可能會生成父類的物件。

你還可以建立一箇中心化原型登錄檔, 用於儲存常用原型。你可以新建一個工廠類來實現登錄檔, 或者在原型基類中新增一個獲取原型的靜態方法。 該方法必須能夠根據客戶端程式碼設定的條件進行搜尋。 搜尋條件可以是簡單的字串, 或者是一組複雜的搜尋引數。 找到合適的原型後, 登錄檔應對原型進行克隆, 並將複製生成的物件返回給客戶端。最後還要將對子類建構函式的直接呼叫替換為對原型登錄檔工廠方法的呼叫。

原型模式優缺點

你可以克隆物件, 而無需與它們所屬的具體類相耦合。

你可以克隆預生成原型, 避免反覆執行初始化程式碼。

你可以更方便地生成複雜物件。

你可以用繼承以外的方式來處理複雜物件的不同配置。

克隆包含迴圈引用的複雜物件可能會非常麻煩。

與其他模式的關係

在許多設計工作的初期都會使用工廠方法模式 (較為簡單, 而且可以更方便地透過子類進行定製), 隨後演化為使用抽象工廠模式、 原型模式或生成器模式 (更靈活但更加複雜)。

抽象工廠模式通常基於一組工廠方法, 但你也可以使用原型模式來生成這些類的方法。

原型可用於儲存命令模式的歷史記錄。

大量使用組合模式和裝飾模式的設計通常可從對於原型的使用中獲益。 你可以透過該模式來複制複雜結構, 而非從零開始重新構造。

原型並不基於繼承, 因此沒有繼承的缺點。 另一方面,

原型

需要對被複制物件進行復雜的初始化。 工廠方法基於繼承, 但是它不需要初始化步驟。

有時候原型可以作為備忘錄模式的一個簡化版本, 其條件是你需要在歷史記錄中儲存的物件的狀態比較簡單, 不需要連結其他外部資源, 或者連結可以方便地重建。

抽象工廠、 生成器和原型都可以用單例模式來實現。

原文:https://refactoringguru。cn/design-patterns/prototype