如何在智能合约中使用工厂模式
在上一篇文章中(传送门:区块链研究实验室 | 智能合约如何创建和继承),我们讨论了如何从另一个智能合约中创建一个智能合约。今天,我们将研究这种情况下的典型用例。
什么是工厂模式?
工厂模式的想法是拥有一个合同(工厂),该合同将承担创建其他合同的任务。在基于类的编程中,此模式的主要动机来自于单一职责原则(一个类不需要知道如何创建其他类的实例),并且该模式为构造函数提供了一种抽象。
为什么要牢固使用工厂模式?
在Solidity中,出于以下原因之一,您可能要使用工厂模式:如果要创建同一合同的多个实例,并且正在寻找一种跟踪它们并简化其管理的方法。
contract Factory { Child[] children; function createChild(uint data){ Child child = new Child(data); children.push(child); }}contract Child{ uint data; constructor(uint _data){ data = _data; }}
这能够节省部署成本,您部署工厂模式,以后再使用它来部署其他合同,并且可以提高合同安全性。
如何与已部署的智能合约进行交互
在深入探讨如何实现工厂模式的细节之前,我想澄清一下我们与已部署的智能合约进行交互的方式。工厂模式是关于创建子合同的,我们可能希望调用它们的某些功能以更好地管理这些合同。
当我们要调用已部署的智能合约时,需要做两件事:
合同的ABI(提供有关功能签名的信息)。如果合同在同一个项目中。您可以使用import关键字将其导入。
部署合同的地址。
让我们举个例子:
contract A { address bAddress; constructor(address b){ bAddress = b; } function callHello() external view returns(string memory){ B b = B(bAddress); // explicit conversion from address to contract type return b.sayHello(); }}contract B { string greeting = "hello world"; function sayHello() external view returns(string memory){ return greeting; }}
在Remix中,首先部署合约B,然后复制其地址,并在部署时将其提供给A的构造函数。现在,您可以调用该callHello()函数,您将获得sayHello()合约B的函数结果。
正常工厂模式
在这种模式下,我们创建具有处理子合同创建功能的工厂合同,并且可能还会添加其他功能来有效管理这些合同(例如,查找特定合同或禁用合同)。在create函数中,我们使用new关键字来部署子合同。
contract Factory{ Child[] public children; uint disabledCount; event ChildCreated(address childAddress, uint data); function createChild(uint data) external{ Child child = new Child(data, children.length); children.push(child); emit ChildCreated(address(child), data); } function getChildren() external view returns(Child[] memory _children){ _children = new Child[](children.length- disabledCount); uint count; for(uint i=0;i<children.length; i++){ if(children[i].isEnabled()){ _children[count] = children[i]; count++; } } } function disable(Child child) external { children[child.index()].disable(); disabledCount++; } }contract Child{ uint data; bool public isEnabled; uint public index; constructor(uint _data,uint _index){ data = _data; isEnabled = true; index = _index; } function disable() external{ isEnabled = false; }}
克隆工厂模式
先前模式的问题在于,由于所有子合同都具有相同的逻辑,并且每次我们几乎都重新部署了相同的合同-相同的代码但上下文不同,因此浪费了大量的精力。我们需要一种方法来仅部署一个具有所有功能的子合同,并使所有其他子合同充当代理,以将调用委派给我们创建的第一个子合同,并让功能在代理合同的上下文中执行。
幸运的是,有一个EIP-1167规范定义了如何廉价地实现代理合同。该代理将所有呼叫和100%的天然气转发给实施合同,然后将返回值中继回调用者。根据规范,代理合同的字节码为:
363d3d373d3d3d363d73bebebebebebebebebebebebebebebebebebebebe5af43d82803e903d91602b57fd5bf3。
索引10-29(含10-29)处的字节被替换为主功能合同(我们将委派调用的合同)的20字节地址。
使用可以完成代理合同的全部魔力delegatecall。通过阅读本文,您可以了解其工作原理。
现在让我们看看如何进行这项工作:
//SPDX-License-Identifier: MITpragma solidity ^0.7.0;import './CloneFactory.sol';contract Factory is CloneFactory { Child[] public children; address masterContract; constructor(address _masterContract){ masterContract = _masterContract; } function createChild(uint data) external{ Child child = Child(createClone(masterContract)); child.init(data); children.push(child); } function getChildren() external view returns(Child[] memory){ return children; }}contract Child{ uint public data; // use this function instead of the constructor // since creation will be done using createClone() function function init(uint _data) external { data = _data; }}
这次,我们使用了createCloneGitHub存储库中的函数来创建子合同,而不是new关键字。
您可以通过在Truffle中创建一个新的迁移文件来部署合同,如下所示:
const Child = artifacts.require("Child");const Factory = artifacts.require("Factory"); module.exports = function (_deployer) _deployer.deploy(Child).then(() => _deployer.deploy(Factory, Child.address)); };
为了测试代码是否有效,我创建了一个测试文件,您可以自行尝试确保所有文件均按预期工作:
contract("Factory", function (/* accounts */) { it("should assert true", async function () { await Factory.deployed(); return assert.isTrue(true); }); describe("#createChild()",async () => { let factory; beforeEach(async ()=>{ factory = await Factory.deployed(); }); it("should create a new child", async () => { await factory.createChild(1); await factory.createChild(2); await factory.createChild(3); const children = await factory.getChildren(); //console.log(children); const child1 = await Child.at(children[0]); const child2 = await Child.at(children[1]); const child3 = await Child.at(children[2]); const child1Data = await child1.data(); const child2Data = await child2.data(); const child3Data = await child3.data(); assert.equal(children.length, 3); assert.equal(child1Data, 1); assert.equal(child2Data, 2); assert.equal(child3Data, 3); }); });});
Scan QR code with WeChat