从这一篇开始我们将主要讨论函数。我们首先从一个问题开始,我们为什么需要函数?或是函数能给我们带来什么好处?下面的列表是我基于前人的经验所理解的函数的好处:
- 降低复杂度
- 引入易于理解的抽象层
- 封装变化
- 避免重复代码
- 简化复杂的条件表达式
- ....
降低复杂度
函数为什么能够降低复杂度呢?首先我们来看看我们是如何解决复杂问题的。我们在解决复杂问题的时候,常用的也是最有效的方法是:将复杂的问题分解成相对简单的多个子问题。有时我们甚至会不断的重复这个过程(即将子问题继续分解成更小更简单的问题),直至最小的问题能够被我们相对容易的理解和解决。最小子问题被解决之后,就可以被当作一个个的模块或黑盒子直接拿来用于解决相对复杂的父问题。
这让我想起了我们小时候学习数学的过程。老师首先会教我们一些最基本数学定律,这些定律很容易被理解和证明。随着年级的升高,老师会教我们如何用这些最基本的数学定律去证明相对复杂一点的定律,这样层层迭代就能证明很复杂的数学问题。同时很多时候在证明相对复杂定律的时候我们并不关心这些相对简单定律的证明过程。
那么在软件的世界里对于一个很复杂的逻辑我们怎么来降低复杂度呢?我想这个时候你一定会想到:子函数。将复杂的逻辑分解成多个相对简单的逻辑,然后用子函数封装这些相对简单逻辑。
引入易于理解的抽象层
子函数的作用其实是为我们引入了一层易于理解的抽象层。原来是一个很大的函数,下一层直接就是具体的复杂的代码。这就好像我们在学习复杂数学定律的时候,老师直接从证明简单定律开始,一直证明至复杂的数学定律。这个证明过程一定会冗长及晦涩。
子函数这时相当于是那些相对简单的数学定律。一是这些子函数相对容易被实现(因为所对应的逻辑相对简单);二是与基于简单定律直接证明相对复杂的定律类似,基于子函数再实现父函数就会相对简单。但这里有一个非常重要的前提条件:为子函数取一个表达其作用或意图的名字。不然我们只是引入了一个抽象层,但这个抽象层却不好理解。
封装变化
函数还能够为我们封装变化。封装什么样的变化呢?我们首先来看看函数到底有哪些部分组成。下图1是一个典型的函数,它有两个部分组成:函数签名、函数体。函数签名相当于是一个约定或是服务,而函数体是具体怎么来履行这个约定。
函数的调用者只能看到这个约定,他们无法看到函数是如何履行这个约定的。事实上函数的调用者也只关心这个约定。只要这个约定不被打破,那么无论被调用函数如何改变其履行约定的方法,都不会影响到函数的调用者,这就将履行约定的变化封装在约定的背后。
图1
下面的代码片段展示了封装变化的作用。在版本1中ValueHolder直接将其成员变量公布在外面,ValueCustomer直接可以给这个变量赋值。这时如果需求变了呢?如果Value有了取值范围呢?这时我们就需要到ValueCustomer的ChangeState函数中修改代码。如果有100多个地方对Value成员变量赋值了呢?那意味着我们需要去100个地方修改代码。这是不是让你想起了价值篇(TODO)中的图1?当一个变化来的时候,我们需要对系统多个地方进行修改。
我们再来看看版本2的实现。在版本2中Value成员变量变成私有变量,外部无法访问这个成员变量,外部甚至都不知道有这个成员变量的存在。同时ValueHolder提供一个用于修改Value成员变量的UpdateValue公共方法。这个公共方法封装了如何修改其内部成员变量的具体实现。对于ValueCustomer 类它可以通过调用这个方法,来让ValueHolder修改其内部的状态。当变化来的时候,我们只需要修改UpdateValue方法的实现。所有使用UpdateValue函数的地方都不会受到影响,因为函数签名(或约定)没有改变。
版本1
public class ValueHolder
{
public int Value;
}
public class ValueCustomer
{
private ValueHolder _valueHolder;
public void ChangeState()
{
....
_valueHolder.Value = 10;
....
}
}
版本2
public class ValueHolder
{
private const int MinValue = 0;
private const int MaxValue = 5;
private int _value;
public void UpdateValue(int newValue)
{
if (newValue < MinValue)
{
_value = MinValue;
}
else if (newValue > MaxValue)
{
_value = MaxValue;
}
else
{
_value = newValue;
}
}
}
public class ValueCustomer
{
private ValueHolder _valueHolder;
public void ChangeState()
{
....
_valueHolder.UpdateValue(10);
....
}
}
避免重复代码
重复代码是引起一个变化导致多处修改的重要原因,而函数可以用来封装变化。这是为什么我们需要将重复代码封装在一个函数中。绝大部分的程序猿都知道DRY(Don't repeate yourself),都会有意识去消除重复代码。有时候你会发现过分的消除重复代码,会导致程序在结构上变得复杂或导致不合理的依赖。这时我们就要问自己:消除重复代码是我们的目标吗?消除重复代码只是我们的手段,降低整个软件生命周期的成本才是我们的目标。所以有时候还需要平衡重复代码和结构清晰。我想这也是为什么有人说编程是门艺术。
简化复杂的条件表达式
函数还可以用来简化复杂的条件表达式。简化条件表达其实是函数降低复杂度的一个具体的例子。我们可以通过将复杂的条件表达封装在函数签名的背后,然后为函数取一个能够表达这个复杂条件表达式的目的或意图的名字,这个函数的名字通常来自于软件所应用的领域。
这时候函数和复杂条件表达式之间的关系就好像内存和硬盘的关系。从硬盘中获取数据代价相对较大,那么我们就是把硬盘的数据先读入内存,下次直接从内存中读取数据,这可以有效的提高读取效。封装复杂条件表达式的那个函数就是被加载到内存中的数据。
这一篇我们主要是尝试着回答一个问题:我们为什么需要函数?我们从几个不同的角度去审视函数的价值。但这些内容相对有些抽象,在接下里的文章中我们将通过一些具体的例子来理解函数这些作用。