引子:印簡歷如同印草紙#

小菜為明天的供需見面會印了一大疊履歷,連小學得獎都寫上去了,還寫「精通 C#、Java、SQL Server、Oracle」。大鳥吐槽:

  • 履歷印得像草紙,發履歷像發廣告
  • 招聘方收到太多反而不重視
  • 「精通」要寫得真誠,過度浮誇反讓人覺得不誠實

大鳥早年手寫履歷,公司也認真看;現在程式員寫履歷都知道複印——但寫程式時卻不一定懂得「複製」的真正用法

第一版:每份履歷都 new 一次#

class Resume
{
    private string name, sex, age, timeArea, company;
    public Resume(string name) { this.name = name; }
    public void SetPersonalInfo(string sex, string age) { ... }
    public void SetWorkExperience(string timeArea, string company) { ... }
    public void Display() { ... }
}

客戶端:

Resume a = new Resume("大鳥");
a.SetPersonalInfo("男", "29");
a.SetWorkExperience("1998-2000", "XX 公司");

Resume b = new Resume("大鳥");
b.SetPersonalInfo("男", "29");
b.SetWorkExperience("1998-2000", "XX 公司");

Resume c = new Resume("大鳥");
// ... 二十份履歷就要重複二十次

不僅冗長,而且若需把 1998 改成 1999,要改二十處——任一遺漏就出錯。

直接賦值行不行?#

Resume b = a;
Resume c = a;

這是傳引用,等於 b、c 兩張紙上都寫著「履歷在 a 處」,沒有實際內容。

原型模式#

原型模式(Prototype Pattern):用原型實例指定創建物件的種類,並且透過拷貝這些原型創建新的物件。[DP]

結構#

  • Prototype:原型類,宣告一個克隆自身的介面
  • ConcretePrototype:具體原型類,實作克隆自身的操作
classDiagram
    class Prototype {
        <<abstract>>
        -string id
        +Clone() Prototype
    }
    class ConcretePrototype1 {
        +Clone() Prototype
    }
    class ConcretePrototype2 {
        +Clone() Prototype
    }
    class Client
    Prototype <|-- ConcretePrototype1
    Prototype <|-- ConcretePrototype2
    Client ..> Prototype
abstract class Prototype
{
    private string id;
    public Prototype(string id) { this.id = id; }
    public string Id => id;
    public abstract Prototype Clone();
}

class ConcretePrototype1 : Prototype
{
    public ConcretePrototype1(string id) : base(id) { }
    public override Prototype Clone() => (Prototype)this.MemberwiseClone();
}

.NET 在 System 命名空間提供了 ICloneable 介面,唯一方法 Clone()。實作這個介面就能直接套用原型模式。

第二版:用 ICloneable 改寫履歷#

class Resume : ICloneable
{
    private string name, sex, age, timeArea, company;
    public Resume(string name) { this.name = name; }
    public void SetPersonalInfo(string sex, string age) { ... }
    public void SetWorkExperience(string timeArea, string company) { ... }
    public void Display() { ... }

    public object Clone() => (object)this.MemberwiseClone();
}

客戶端:

Resume a = new Resume("大鳥");
a.SetPersonalInfo("男", "29");
a.SetWorkExperience("1998-2000", "XX 公司");

Resume b = (Resume)a.Clone();
b.SetWorkExperience("1998-2006", "YY 企業");

Resume c = (Resume)a.Clone();
c.SetPersonalInfo("男", "24");

原型模式不只是讓客戶端清爽。每次 new 都要執行構造函式,若初始化耗時,多次 new 效率極低。

複製克隆是最好的辦法:既隱藏了物件創建細節,又動態獲得物件運行時狀態。

淺複製 vs. 深複製#

MemberwiseClone() 的行為:

  • 值類型:逐位複製
  • 引用類型:複製引用,但不複製引用的物件——原始物件與複本指向同一物件

範例:履歷引用「工作經歷」物件#

WorkExperience 提取為獨立類別後:

class WorkExperience
{
    public string WorkDate { get; set; }
    public string Company { get; set; }
}

class Resume : ICloneable
{
    private WorkExperience work;
    public Resume(string name)
    {
        this.name = name;
        work = new WorkExperience();
    }
    // ...
    public object Clone() => (object)this.MemberwiseClone();
}

a Clone 兩次後分別 SetWorkExperience三份履歷的工作經歷都變成最後一次設定的值——因為三個引用都指向同一個 WorkExperience 物件。

這就是淺複製(Shallow Copy):被複製物件的所有變數都含有相同值,但所有對其他物件的引用仍指向原來的物件。

深複製(Deep Copy)#

深複製:把要複製物件所引用的物件也複製一遍——指針指向新複製的物件,而非原物件。

實作方式:讓 WorkExperience 也實作 ICloneable,並在 Resume 中提供私有建構子處理深複製:

class WorkExperience : ICloneable
{
    public string WorkDate { get; set; }
    public string Company { get; set; }
    public object Clone() => (object)this.MemberwiseClone();
}

class Resume : ICloneable
{
    private WorkExperience work;

    public Resume(string name)
    {
        this.name = name;
        work = new WorkExperience();
    }

    private Resume(WorkExperience work)
    {
        this.work = (WorkExperience)work.Clone();
    }

    public object Clone()
    {
        Resume obj = new Resume(this.work);
        obj.name = this.name;
        obj.sex = this.sex;
        obj.age = this.age;
        return obj;
    }
}

若引用層層巢狀(履歷 → 工作經歷 → 公司 → 職位 → ……),深複製要深入到第幾層需事先決定。

還要當心循環引用問題,需要小心處理。

.NET 中的對應實作#

DataSet 物件提供了兩個方法:

  • Clone():複製結構,但不複製資料 → 對應淺複製
  • Copy():複製結構也複製資料 → 對應深複製

本章小結#

  • 原型模式從一個物件再創建另一個可訂製的物件,且不需知道任何創建細節
  • 隱藏了創建細節、提升效能(避免重複初始化)
  • 動態獲得物件運行時的狀態
  • 但要注意淺複製與深複製的差異——對引用類型的成員,淺複製只複製引用

大鳥的吐槽:寫程式時用原型模式簡化程式碼是好事,但找工作時與其複印履歷,不如針對職缺寫一封手寫求職信——稀缺的東西才會被重視。