iT邦幫忙

2021 iThome 鐵人賽

DAY 9
0
Software Development

也該是時候學學 Design Pattern 了系列 第 9

Day 09: Creational patterns - Prototype

目的

實踐物件的 Deep Copy,避免 Shallow Copy 的問題以及省去重新製作物件。

說明

Prototype 源自於物件的複製問題,如果資料的型別是字串、數字、布林等,可以直接複製(call by value),物件與陣列這兩類只會複製記憶體的儲存位置(call by reference),這將導致複製不完全(Shallow CopyShallow Clone),新舊物件共用相同的物件與陣列,肯定會出事。

因此,為了完全複製(Deep CopyDeep Clone),可以採用 Prototype,作法是:

  1. 建立一個可以執行 Deep Copy 方法的物件,可以是 AbstractInterfaceClass
  2. 該方法實作上,針對物件與陣列處額外處理,確保複製成功。

UML 圖

Prototype Pattern UML Diagram

使用 Java 實作

Prototype:NewTasteBeverage

public class NewTasteBeverage implements Cloneable {
    private String baseBeverage;
    private String taste;
    private String size;
    private IdInfo idInfo;

    public NewTasteBeverage(String baseBeverage) {
        this.baseBeverage = baseBeverage;
        idInfo = new IdInfo();
    }

    /**
     * Deep Clone 需要注意的點
     */
    private NewTasteBeverage(IdInfo idInfo) throws CloneNotSupportedException {
        this.idInfo = (IdInfo) idInfo.clone();
    }

    public void setProduceInfo(String location, String factoryAddress) {
        idInfo.setLocation(location);
        idInfo.setFactoryAddress(factoryAddress);
    }

    public void setSeriesNumber(String seriesNumber) {
        idInfo.setSeriesNumber(seriesNumber);
    }

    public void setTaste(String taste) {
        this.taste = taste;
    }

    public void setSize(String size) {
        this.size = size;
    }

    @Override
    public Object clone() throws CloneNotSupportedException {
        NewTasteBeverage clonedObject = new NewTasteBeverage(this.idInfo);
        clonedObject.baseBeverage = this.baseBeverage;
        clonedObject.taste = this.taste;
        clonedObject.size = this.size;
        return clonedObject;
    }

    public void showInfo() {
        System.out.println("產品是:" + taste + " " + baseBeverage + " " + size);
        System.out.println("產地資訊:" + idInfo.getFactoryAddress() + " " + idInfo.getLocation());
        System.out.println("序號:" + idInfo.getSeriesNumber() + "\n");
    }
}

class IdInfo implements Cloneable {
    private String seriesNumber;
    private String location;
    private String factoryAddress;

    public String getSeriesNumber() {
        return seriesNumber;
    }

    public void setSeriesNumber(String seriesNumber) {
        this.seriesNumber = seriesNumber;
    }

    public String getLocation() {
        return location;
    }

    public void setLocation(String location) {
        this.location = location;
    }

    public String getFactoryAddress() {
        return factoryAddress;
    }

    public void setFactoryAddress(String factoryAddress) {
        this.factoryAddress = factoryAddress;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

進行複製

public class PrototypeSample {
    public static void main(String[] args) throws CloneNotSupportedException {
        System.out.println("進行第一項產品的嘗試");
        NewTasteBeverage beverage1 = new NewTasteBeverage("可樂");
        beverage1.setSize("350毫升");
        beverage1.setTaste("香草");
        beverage1.setProduceInfo("桃園", "臺灣");
        beverage1.setSeriesNumber("3617263");

        System.out.println("更換,進行第二項");
        NewTasteBeverage beverage2 = (NewTasteBeverage) beverage1.clone();
        beverage2.setTaste("檸檬");
        beverage2.setSeriesNumber("8911733");

        System.out.println("更換,進行最後一項");
        NewTasteBeverage beverage3 = (NewTasteBeverage) beverage1.clone();
        beverage3.setProduceInfo("臺中", "臺灣");
        beverage3.setSeriesNumber("1657209");

        beverage1.showInfo();
        beverage2.showInfo();
        beverage3.showInfo();
    }
}

使用 JavaScript 實作

Prototype:NewTasteBeverage

class NewTasteBeverage {
  constructor(baseBeverage) {
    this.baseBeverage = baseBeverage;
    this.taste = '';
    this.size = '';
    this.idInfo = new IdInfo();
  }

  setProduceInfo(location, factoryAddress) {
    this.idInfo.setLocation(location);
    this.idInfo.setFactoryAddress(factoryAddress);
  }

  setSeriesNumber(seriesNumber) {
    this.idInfo.setSeriesNumber(seriesNumber);
  }

  setTaste(taste) {
    this.taste = taste;
  }

  setSize(size) {
    this.size = size;
  }

  clone() {
    const clonedObject = Object.create(this);
    clonedObject.idInfo = this.idInfo.clone();
    clonedObject.baseBeverage = this.baseBeverage;
    clonedObject.taste = this.taste;
    clonedObject.size = this.size;
    return clonedObject;
  }

  showInfo() {
    console.log("產品是:" + this.taste + " " + this.baseBeverage + " " + this.size);
    console.log("產地資訊:" + this.idInfo.getFactoryAddress() + " " + this.idInfo.getLocation());
    console.log("序號:" + this.idInfo.getSeriesNumber() + "\n");
  }
}

class IdInfo {
  constructor() {
    this.seriesNumber = '';
    this.location = '';
    this.factoryAddress = '';
  }

  getSeriesNumber() {
    return this.seriesNumber;
  }

  setSeriesNumber(seriesNumber) {
    this.seriesNumber = seriesNumber;
  }

  getLocation() {
    return this.location;
  }

  setLocation(location) {
    this.location = location;
  }

  getFactoryAddress() {
    return this.factoryAddress;
  }

  setFactoryAddress(factoryAddress) {
    this.factoryAddress = factoryAddress;
  }

  clone() {
    const clonedObject = Object.create(this);
    clonedObject.seriesNumber = this.seriesNumber;
    clonedObject.location = this.location;
    clonedObject.factoryAddress = this.factoryAddress;
    return clonedObject;
  }
}

進行複製

const prototypeSample = () => {
  console.log("進行第一項產品的嘗試");
  const beverage1 = new NewTasteBeverage("可樂");
  beverage1.setSize("350毫升");
  beverage1.setTaste("香草");
  beverage1.setProduceInfo("桃園", "臺灣");
  beverage1.setSeriesNumber("3617263");

  console.log("更換,進行第二項");
  const beverage2 = beverage1.clone();
  beverage2.setTaste("檸檬");
  beverage2.setSeriesNumber("8911733");

  console.log("更換,進行最後一項");
  const beverage3 = beverage1.clone();
  beverage3.setProduceInfo("臺中", "臺灣");
  beverage3.setSeriesNumber("1657209");

  beverage1.showInfo();
  beverage2.showInfo();
  beverage3.showInfo();
}

prototypeSample();

總結

過往的開發 JavaScript 的經驗,處理物件的 Deep Clone 時,最常使用的是 JSON.parse(JSON.stringify(obj)),這個做法的前提物件本身的組成只有 JSON 允許的字串、數字、布林值、物件、陣列,其他像是 undefined、Symbol、Set、Map 會因為 JSON 不支援導致複製後得到的是 {}。除此之外,物件之間的繼承也會變重置,因為 JSON.parse() 會建立新物件,不可能會知道 JSON 前的繼承關係。

藉由這個模式,可以暸解實作 Deep Clone 是所有物件導向語言的都會遇到的問題,趁著這次的學習與了解,讓自己往後遇到擁有複雜繼承關係的物件時,多了一種處理方式。

明天將介紹最後一個 Creational patterns: Prototype 模式。


上一篇
Day 08: Creational patterns - Builder
下一篇
Day 10: Creational patterns - Singleton
系列文
也該是時候學學 Design Pattern 了31

1 則留言

0
TD
iT邦新手 4 級 ‧ 2021-09-25 09:24:17

明天將介紹最後一個 Creational patterns: Prototype 模式。

是不是都介紹完了 XD

我要留言

立即登入留言