在上一篇文章中(传送门:区块链研究实验室 | 智能合约如何创建和继承),我们讨论了如何从另一个智能合约中创建一个智能合约。今天,我们将研究这种情况下的典型用例。
什么是工厂模式?
工厂模式的想法是拥有一个合同(工厂),该合同将承担创建其他合同的任务。在基于类的编程中,此模式的主要动机来自于单一职责原则(一个类不需要知道如何创建其他类的实例),并且该模式为构造函数提供了一种抽象。
为什么要牢固使用工厂模式?
在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: MIT
pragma 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);
});
});
});
结论
至此,我们这个系列已经完结
作者:链三丰,来源:区块链研究实验室
郑重声明:本文版权归原作者所有,转载文章仅为传播更多信息之目的,如作者信息标记有误,请第一时间联系我们修改或删除,多谢。
郑重声明:本文版权归原作者所有,转载文章仅为传播更多信息之目的,如作者信息标记有误,请第一时间联系我们修改或删除,多谢。