設計模式是許多開發場景中的首選解決方案,本文將介紹五種經典的智能合約設計模式並給出 以太坊solidity實現代碼:自毀合約、工廠合約、名稱註冊表、映射表迭代器和提款模式。數據庫
若是你但願立刻開始學習以太坊DApp開發,能夠訪問匯智網提供的出色的在線互動教程:設計模式
合約自毀模式用於終止一個合約,這意味着將從區塊鏈上永久刪除這個合約。 一旦被銷燬,就不可能 調用合約的功能,也不會在帳本中記錄交易。數組
如今的問題是:「爲何我要銷燬合約?」。安全
有不少緣由,好比某些定時合約,或者那些一旦達到里程碑就必須終止的合約。 一個典型的案例 是貸款合約,它應當在貸款還清後自動銷燬;另外一個案例是基於時間的拍賣合約,它應當在拍賣結束後 終止 —— 假設咱們不須要在鏈上保存拍賣的歷史記錄。app
在處理一個被銷燬的合約時,有一些須要注意的問題:ide
爲避免資金損失,應當在發送資金前確保目標合約仍然存在,移除全部對已銷燬合約的引用。 如今咱們來看看代碼:函數
contract SelfDesctructionContract { public address owner; public string someValue; modifier ownerRestricted { require(owner == msg.sender); _; } // constructor function SelfDesctructionContract() { owner = msg.sender; } // a simple setter function function setSomeValue(string value){ someValue = value; } // you can call it anything you want function destroyContract() ownerRestricted { suicide(owner); } }
正如你所看到的, destroyContract()
方法負責銷燬合約。學習
請注意,咱們使用自定義的ownerRestricted
修飾符來顯示該方法的調用者,即僅容許合約的擁有者 銷燬合約。區塊鏈
工廠合約用於建立和部署「子」合約。 這些子合約能夠被稱爲「資產」,能夠表示現實生活中的房子或汽車。ui
工廠用於存儲子合約的地址,以便在必要時提取使用。 你可能會問,爲何不把它們存在Web應用數據庫裏? 這是由於將這些地址數據存在工廠合約裏,就意味着是存在區塊鏈上,所以更加安全,而數據庫的損壞 可能會形成資產地址的丟失,從而致使丟失對這些資產合約的引用。 除此以外,你還須要跟蹤全部新 建立的子合約以便同步更新數據庫。
工廠合約的一個常見用例是銷售資產並跟蹤這些資產(例如,誰是資產的全部者)。 須要向負責部署資產的 函數添加payable修飾符以便銷售資產。 代碼以下:
contract CarShop { address[] carAssets; function createChildContract(string brand, string model) public payable { // insert check if the sent ether is enough to cover the car asset ... address newCarAsset = new CarAsset(brand, model, msg.sender); carAssets.push(newCarAsset); } function getDeployedChildContracts() public view returns (address[]) { return carAssets; } } contract CarAsset { string public brand; string public model; address public owner; function CarAsset(string _brand, string _model, address _owner) public { brand = _brand; model = _model; owner = _owner; } }
代碼address newCarAsset = new CarAsset(...)
將觸發一個交易來部署子合約並返回該合約的地址。 因爲工廠合約和資產合約之間惟一的聯繫是變量address[] carAssets
,因此必定要正確保存子合約的地址。
假設你正在構建一個依賴與多個合約的DApp,例如一個基於區塊鏈的在線商城,這個DApp使用了 ClothesFactoryContract、GamesFactoryContract、BooksFactoryContract等多個合約。
如今想象一下,將全部這些合約的地址寫在你的應用代碼中。 若是這些合約的地址隨着時間的推移而變化,那該怎麼辦?
這就是名稱註冊表的做用,這個模式容許你只在代碼中固定一個合約的地址,而不是數10、數百甚至數千個 地址。它的原理是使用一個合約名稱 => 合約地址的映射表,所以能夠經過調用getAddress("ClothesFactory")
從DApp內查找每一個合約的地址。 使用名稱註冊表的好處是,即便更新那些合約,DApp也不會受到任何影響,由於 咱們只須要修改映射表中合約的地址。
代碼以下:
contract NameRegistry { struct ContractDetails { address owner; address contractAddress; uint16 version; } mapping(string => ContractDetails) registry; function registerName(string name, address addr, uint16 ver) returns (bool) { // versions should start from 1 require(ver >= 1); ContractDetails memory info = registry[name]; require(info.owner == msg.sender); // create info if it doesn't exist in the registry if (info.contractAddress == address(0)) { info = ContractDetails({ owner: msg.sender, contractAddress: addr, version: ver }); } else { info.version = ver; info.contractAddress = addr; } // update record in the registry registry[name] = info; return true; } function getContractDetails(string name) constant returns(address, uint16) { return (registry[name].contractAddress, registry[name].version); } }
你的DApp將使用getContractDetails(name)
來獲取指定合約的地址和版本。
不少時候咱們須要對一個映射表進行迭代操做 ,但因爲Solidity中的映射表只能存儲值, 並不支持迭代,所以映射表迭代器模式很是有用。 須要指出的是,隨着成員數量的增長, 迭代操做的複雜性會增長,存儲成本也會增長,所以請儘量地避免迭代。
實現代碼以下:
contract MappingIterator { mapping(string => address) elements; string[] keys; function put(string key, address addr) returns (bool) { bool exists = elements[key] == address(0) if (!exists) { keys.push(key); } elements[key] = addr; return true; } function getKeyCount() constant returns (uint) { return keys.length; } function getElementAtIndex(uint index) returns (address) { return elements[keys[index]]; } function getElement(string name) returns (address) { return elements[name]; } }
實現put()
函數的一個常見錯誤,是經過遍從來檢查指定的鍵是否存在。正確的作法是 elements[key] == address(0)
。雖然遍歷檢查的作法不徹底是一個錯誤,但它並不可取, 由於隨着keys數組的增加,迭代成本愈來愈高,所以應該儘量避免迭代。
假設你銷售汽車輪胎,不幸的是賣出的全部輪胎出問題了,因而你決定向全部的買家退款。
假設你跟蹤記錄了合約中的全部買家,而且合約有一個refund()函數,該函數會遍歷全部買家 並將錢一一返還。
你能夠選擇 - 使用buyerAddress.transfer()或buyerAddress.send() 。 這兩個函數的區別在於, 在交易異常時,send()不會拋出異常,而只是返回布爾值false ,而transfer()則會拋出異常。
爲何這一點很重要?
假設大多數買家是外部帳戶(即我的),但一些買家是其餘合約(也許是商業)。 假設在 這些買方合約中,有一個合約,其開發者在其fallback函數中犯了一個錯誤,而且在被調用時拋出一個異常, fallback()函數是合約中的默認函數,若是將交易發送到合同但沒有指定任何方法,將調用合約 的fallback()函數。 如今,只要咱們在refund函數中調用contractWithError.transfer() ,就會拋出 異常並中止迭代遍歷。 所以,任何一個買家合約的fallback()異常都將致使整個退款交易被回滾, 致使沒有一個買家能夠獲得退款。
雖然在一次調用中退款全部買家可使用send()來實現,可是更好的方式是提供withdrawFunds()方法,它 將單獨按須要退款給調用者。 所以,錯誤的合約不會應用其餘買家拿到退款。
實現代碼以下:
contract WithdrawalContract { mapping(address => uint) buyers; function buy() payable { require(msg.value > 0); buyers[msg.sender] = msg.value; } function withdraw() { uint amount = buyers[msg.sender]; require(amount > 0); buyers[msg.sender] = 0; require(msg.sender.send(amount)); } }