6.1 面向对象概述
程序设计的本质是分析业务领域要解决的问题然后进行数据建模,最后再在数据模型基础上选择高效的算法进行数据处理并返回处理结果,所以大家常说程序=数据结构+算法。但随着要解决的业务领域问题越来越复杂,传统的根据业务问题直接建模并自上而下逐步分解和解决问题的方法暴露的弊端越来越明显,特别是涉及多人协作的时候代码之间的相互耦合使得系统升级和调试是成为一个噩梦。为了解决该问题诞生了面向对象程序设计范式,其和传统的面向过程程序设计最本质的区别是将要解决的业务问题按照领域进行对象划分和建模,对象本身可以包含特定的数据以及在这些数据基础上提供的服务,面向对象设计已经成为工程领域事实上的标准。
面向过程设计的核心是从机器的角度出发分解要解决的问题,而面向对象的核心是从人类思考问题的模式出发,将要解决的业务问题分解为一个个对象以及对象之间的交互和协作。之前的基础业务数据变为相关对象的属性,之前对数据的操作和处理变为对象对外提供的服务,并在权限许可的情况下提供对外访问。对具有共同属性和服务的一组对象进行抽象形成类,因此对象是类的一个实例。总结起来面向对象设计范式由四大核心组件组成:抽象、封装、继承和多态。
抽象是指将一类实体的共同特性和操作进行归纳总结,并以类的形式进行体现,这样当以后需要一个特定实例的对象时可以直接从这个抽象类进行实例化。封装是指将将数据以及对数据的操作放在一起,在权限许可的限制下以接口的形式对外提供服务。继承的主要功能是在一个已有的类的基础上叠加新的属性和方法,从而达到代码复用的目的以节约开发成本和资源。多态是指针对父类进行统一风格的接口编码,每个继承此父类的子类都可以无缝加入进来并完成个性化的处理,也就是编码针对父类但每个具体子类都表现其个性化的行为。
solidity作为区块链平台面向智能合约的专用编程语言,相比于传统编程语言,虽然进行了一定的功能简化但也提供了对面向对象程序设计范式的完整支持,包括对四大核心组件封装、抽象、继承和多态的支持。但由于区块链编程本身的一些特殊性在solidity中的进行面向对象编程时,与传统编程语言中的面向对象编程还是有一定区别的不能完全照搬,这在进行实际工程编码中需要特别注意。
6.2 抽象与封装
solidity本质上是纯粹的面向对象编程语言,其最小部署单元合约或者库本身就是一个对象只是不能进行实例化。对特定业务领域对象进行抽象时主要是通过结构体来实现的。但遗憾的是结构体本身只是对数据的抽象并没有封装对数据的相关操作,solidity中对数据操作的封装是通过合约函数来实现的。示意代码如下:
contract StructTest {
struct Person{
string name;
uint8 age;
}
Person[] persons;
function addPerson(string memory nname, uint8 aage) public {
persons.push(Person({
name : nname,
age : aage
}));
}
function calcAvarage() public view returns(uint) {
uint total;
for(uint idx = 0; idx < persons.length; idx++){
total += persons[idx].age;
}
return total / persons.length;
}
}
如代码所示对业务数据的封装除了简单的状态变量外,还可以利用结构体对数据进行更符合逻辑的抽象和封装,然后再根据需要以外部函数的形式对外提供服务。
对智能合约来说一个比较重要的函数就是构造函数,其以关键字constructor开头并可携带参数列表,当需要支持ETH接收时用payable关键修饰。该函数只在合约部署时执行一次后面不可再执行,因此一般把初始化信息放在构造函数里面完成,示意代码如下:
contract ConstructTest {
uint public data;
constructor() public payable {
data = 1;
}
// function ConstructTest(uint ddata) public payable {
// data = ddata;
// }//compile err, no override Construct
function ConstructTest1() public payable {
data = 123;
}
function getBalance() public view returns(uint) {
address me = address(this);
return me.balance;
}
}
如代码所示当合约正常部署后data的值为1,如果部署合约的时候存入了一定量的eth资金,通过余额查询函数能查询到当前合约的eth余额。构造函数在合约部署后对外不可见,另外solidity中不支持在函数中对参数指定默认值包括构造函数。另外一个常见的错误是构造函数名称没和合约名称严格一直,导致期望的合约状态变量初始化没正确执行从而引起未知错误。关于构造函数另外一个需要注意的地方是每个合约有且只能有一个构造函数,因此不支持常规意义上的构造函数重载否则会引起编译错误,这是和传统编程语言行为不一致的地方。
6.3 继承
继承是面向对象设计中重要的核心概念之一,是进行代码复用和功能扩展比较简单可行的方法,solidity中大名鼎鼎的工具框架zeppelin-solidity(官方主页:https://openzeppelin.org)其大部分功能都可以通过继承来无缝得到,从而简化我们日常的编码工作量并能得到框架提供的额外安全保障。笔者再次强调相对于传统编程类语言,在用solidity进行智能合约编码过程中,编码技巧的运用其重要性远远低于代码可读性以及安全性的保证,或者说大部分的编码技巧都是为这两方面服务的,因此尽量使用经过严格检验的第三方安全库而不是每次从零开始自己造轮子。可读写的提高是为了便于安全审计,因为智能合约应用场景大部分都涉及大量的金融相关属性。保持合约的简洁性和可读性,尽量通过继承来简化单个合约的复杂度。因为在实现继承时solidity编译器最终会将相关的父类代码拷贝到子类实现中并在部署时确保是一个独立的包含所有功能和状态变量的合约。
6.3.1 普通继承
solidity中继承是通过关键is来实现的,内部实现方式为拷贝父类合约代码实现中的功能代码到子类合约中来实现的,我们来看一个关于继承最简单的范例,代码如下:
contract Base {
uint public baseVal = 123;
function deposit() public payable {}
function getBalance() public view returns(uint) {
address me = address(this);
return me.balance;
}
}
contract Other is Base { }
基类合约Base提供运行时eth资金存款功能以及余额查询功能,子类合约other只是继承了父类合约然后什么都没有做。但当部署合约other后其也自动具备了存入eth资金的功能以及当前余额查询功能。继承不仅可以继承父类合约中的函数,父类中的非私有状态变量也可以通过继承得到,就像这些变量在子类合约中定义了一样。
前面介绍的事件、函数修改器以及枚举类型定义都可以通过继承的方式在子类合约中直接使用,示意代码如下:
contract Base {
uint public val;
enum EState { ES_NONE, ES_OPEN, ES_CLOSE }
address owner;
event LogVal(uint indexed pos, string info);
constructor() public {
owner = msg.sender;
}
modifier onlyOwner() {
require(msg.sender == owner);
_;
}
}
contract Other is Base {
EState locstate = EState.ES_CLOSE;
function setVal(uint nnew) public onlyOwner {
val = nnew;
emit LogVal(1, "set new info");
}
}
由此也诞生了个常用的设计范式就是把公共的类型定义、事件定义和函数修改器等定义独立到一个公共父类合约中,继承此合约的子类合约都自动拥有这些类型、事件和修改器定义,从而简化后续的代码编写和升级维护。这个就是继承的带来的好处,通过继承能有效的利用现有代码,特别是当基类由第三方框架提供时如zeppelin编码效率会大大提高。现在版本的solidity还处于其发展历史的早期今后会有越来越多的工具和框架可用,提前进行技术储备面对未来会有更多主动权。
网友评论