教程 | 面向条件的编程

张亚宁   |     |   993 次阅读


面向条件的编程(COP)是面向合约编程的一个子域,作为一种面向函数和命令式编程的混合模式。正确的使用它,作为你武器库中的工具,方便编写安全的合约。它有助于你的合约代码完全可审计-不严格的讲-非正式的的证明有更正确的运行时操作。

COP不特定于某语言;它更多的是一种不严格的方法论,而不是特定的语法。尽管如此,因为有modifier和events,它特别适用于Solidity语言。
简单的说,COP只有一个目的:
函数体不包含有条件分支。
或者换一种说法:
永远不要混合状态变化和条件。

这听起来是一个困难的目标对于命令式语言,因为条件分支是使你达到丰富的状态变化,它使程序运行更动态化。为了实现它,我们试着分割所有的条件和它们守护的状态变化。我们单独为它们命名,然后将它们结合起来形成真正的函数。
条件分支和状态变化逻辑混合在一起的问题是,它们增加了非线性的概念到状态语义。潜在的bug隐藏起来了,当程序员认为条件(因此状态展现出来如此)是这样,但是实际上它可能有微妙的差别。

一个单层的条件已经很不好了,但是当多层条件被引入时,复杂度(也就是说程序员必须考虑现实世界中所有的状态分支)迅速增长,然后它变得很难推断整个合约的状态变化,在没有正式工具普遍使用时。
COP解决了这个问题,通过需要程序员显示地枚举所有的条件。逻辑变得扁平,没有条件的状态变化。条件片段可以被正确的文档化,复用,可以根据需求和实现来推断。重要的是,COP在编程中把预先条件当作为一等公民。

使用方法

如果你已经在使用Solidity,你已经在不经意的情况下开始和COP打交道了。让我么来看一个简单的Token合约。

contract Token {
    // The balance of everyone
    mapping (address => uint) public balances;
    // Constructor - we're a millionaire!
    function Token() {
        balances[msg.sender] = 1000000;
    }
    // Transfer `_amount` tokens of ours to `_dest`.
    function transfer(uint _amount, address _dest) {
        balances[msg.sender] -= _amount;
        balances[_dest] += _amount;
    }
}

聪明的读者可能注意到这里有一个bug:transfer 函数不能保证 sender有足够的ether在他的账户里。一般的命令式编程语言解决这个问题的方式通常是引入一个条件判断到函数体:

function transfer(uint _amount, address _dest) {
    if (balances[msg.sender] >= _amount) {
        balances[msg.sender] -= _amount;
        balances[_dest] += _amount;
    }
}

或者:

function transfer(uint _amount, address _dest) {
    if (balances[msg.sender] < _amount)
        return;
    balances[msg.sender] -= _amount;
    balances[_dest] += _amount;
}

尽管如此,这两个解决方案都没有实现COP;我们的实现方式比较混乱(无论我们选哪种),意思是,这两个例子条件判断依然在函数主体中:执行账户 msg.sender,如果有至少 _amount 的余额。作为一个COP程序员,我们清楚地明白这个问题,因为两种解决方案都破坏了我们的基本原则:
函数主体不应该包含条件分支
所以在COP中,我么应该抽象条件(balances[msg.sender] >= _amount),创建一个modifier函数:

modifier only_with_at_least(uint x) {
    if (balances[msg.sender] >= x) _
}

这段代码本质上抽象了“执行账户必须至少有某个特定余额”的概念。当它在合适的位置时,我么不需要考虑条件方面的判断,更重要的是,我们不需要混合前置条件逻辑和状态变化的逻辑。这大大提高了人们对状态转移的可读性和可理解性。
以下是新的transfer函数:

function transfer(uint _amount, address _dest)
only_with_at_least(_amount) {
    balances[msg.sender] -= _amount;
    balances[_dest] += _amount;
}

抽象和复用

假设我们有另外一个函数,允许任何人超过1000 ether 余额就可以对某件事投票。我们假设现在投票只是,对一个以address为key的mapping进行赋值操作。
在我们这些规定下,我们会有像这样的函数:

function vote(uint _opinion) {
    if (balances[msg.sender] >= 1000) {
        votes[msg.sender] = _opinion;
    }
}

加到我们原来的代码中,我么现在有两个意思相近的条件。原则上,我们只需要一个这样的条件函数,方便审计和文档化,但是可以使用两次。使用COP:

function vote(uint _opinion) only_when_at_least(1000) {
    votes[msg.sender] = _opinion;
}

这使我们的投票函数有更强的可读性,而且允许我们复用守护逻辑,减少了一些潜在的表面攻击。

更复杂的交易

通过反对条件分支混在状态变化中,我们限制了状态变化的复杂度。这大大方便了审计,因为它允许我们对代码逻辑分而治之,单独的检查状态变化的逻辑,和守护这些逻辑的条件逻辑。但是有时候这些状态变化有自己的内部守护逻辑。
下面的投票例子,假设我们扩展了transfer 函数,那样我们可以确保,任何余额减少的账户没有投票权。
在传统的命令式编程语言中,我么可以简单的放置条件判断在投票减少的位置:

function transfer(uint _amount, address _dest) {
    if (balances[msg.sender] >= _amount) {
        balances[msg.sender] -= _amount;
        balances[_dest] += _amount;
        if (balances[msg.sender] < 1000) {
            votes[msg.sender] = 0;    // Clear their vote.
        }
    }
}

这完全违背了COP的理念。尽管如此,我们不能直接通过增加modifer 函数的方式来解决,因为没有明显的函数去修改;我们实际上是想在transfer的内部域增加一个条件守护。在这种情况下(至少Solidity),我们可以创建一个新的(inline)函数:

function clear_undeserved_vote(account _who)
only_with_under(1000)
only_when_voted {
    delete votes[_who];
}

注意inline在Solidity还不能使用;我么可以使用它,当它可以使用时。这个函数依赖两个modifier,它们容易编写(和审计):

modifier only_with_under(uint x) { if (balances[msg.sender] < x) _ }
modifier only_when_voted { if (votes[msg.sender] != 0) _ }

然后,我们可以在我们的transfer函数中使用这个函数:

function transfer(uint _amount, address _dest)
only_with_at_least(_amount) {
    balances[msg.sender] -= _amount;
    balances[_dest] += _amount;
    clear_undeserved_vote();
}

结论

我们的最终的合约代码从:

contract Token
{
    //...
    function transfer(uint _amount, address _dest) {
        if (balances[msg.sender] >= _amount) {
            balances[msg.sender] -= _amount;
            balances[_dest] += _amount;
            if (balances[msg.sender] < 1000) {
                votes[msg.sender] = 0;    // Clear their vote.
            }
        }
    }
    function vote(uint _opinion) {
        if (balances[msg.sender] >= 1000) {
            votes[msg.sender] = _opinion;
        }
    }
}

变成:

contract Token
{
    //...
    modifier only_with_at_least(uint x) {
        if (balances[msg.sender] >= x) _
    }
    modifier only_with_under(uint x) {
        if (balances[msg.sender] < x) _
    }
    modifier only_when_voted {
        if (votes[msg.sender] != 0) _
    }
    function clear_undeserved_vote(account _who)
    only_with_under(1000) only_when_voted {
        delete votes[_who];
    }
    function transfer(uint _amount, address _dest)
    only_with_at_least(_amount) {
        balances[msg.sender] -= _amount;
        balances[_dest] += _amount;
        clear_undeserved_vote();
    }
    function vote(uint _opinion)
    only_when_at_least(1000) {
        votes[msg.sender] = _opinion;
    }
}

现在代码有一点长,但是它强制程序员文档化内部实现,鼓励他们考虑分离条件和抽象重要的代码逻辑,确保没有隐患的copy/paste bugs。执行部分是扁平的,减少了概念打包。它容易被文档化和审计,可以一小块一小块的,全面而有条不紊的来进行。而且,即使没有被文档化,它也更易懂,根据条件的名字,而不是像原来那样将他们和状态变化逻辑混合在一起。

啰嗦的讲,COP肯定不是符合每个人的口味。在缺乏特定语言支持下,在巨大的合约中它可能变的有些笨重。但是,对于小和中等规模的合约,它提供给程序员和审计者清晰的方法可循,否则某些目标将比较难达到。

在下一篇文章中,在此系列中,我将会带来一个真实世界的合约,使用COP风格,来展示代码如何被分解,文档化,非正式地证明这种方式是正确的。

原文:https://medium.com/@gavofyork/condition-orientated-programming-969f6ba0161a
翻译:@rubyu2

 
3 人喜欢