智能合约的5种设计模式

设计模式是许多开发场景中的首选解决方案,本文将介绍五种经典的智能合约设计模式并给出 以太坊solidity实现代码:自毁合约、工厂合约、名称注册表、映射表迭代器和提款模式。数据库

一、自毁合约

合约自毁模式用于终止一个合约,这意味着将从区块链上永久删除这个合约。 一旦被销毁,就不可能 调用合约的功能,也不会在帐本中记录交易。设计模式

如今的问题是:“为何我要销毁合约?”。数组

有不少缘由,好比某些定时合约,或者那些一旦达到里程碑就必须终止的合约。 一个典型的案例 是贷款合约,它应当在贷款还清后自动销毁;另外一个案例是基于时间的拍卖合约,它应当在拍卖结束后 终止 —— 假设咱们不须要在链上保存拍卖的历史记录。安全

在处理一个被销毁的合约时,有一些须要注意的问题: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应用数据库里? 这是由于将这些地址数据存在工厂合约里,就意味着是存在区块链上,所以更加安全,而数据库的损坏 可能会形成资产地址的丢失,从而致使丢失对这些资产合约的引用。 除此以外,你还须要跟踪全部新 建立的子合约以便同步更新数据库。.net

工厂合约的一个常见用例是销售资产并跟踪这些资产(例如,谁是资产的全部者)。 须要向负责部署资产的 函数添加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));
   }
}
相关文章
相关标签/搜索