自顶向下与自底向上编程思想的对比

在国内的网站上搜索什么叫“自底向上”编程,给人的感受似乎是同一个问题有两种解决思路,一个是“自顶向下”,一个是“自底向上”。但你仔细看那些文章的讲解,其实说的都只是“自顶向下”。

为了说清楚“自底向上”编程,首先赘述一下什么叫做“自顶向下”编程。

 

自顶向下编程

自顶向下编程一般会好理解一点。首先从整体分析一个比较复杂的大问题。为了解决这一问题,必须把它拆分来看,应当解决哪几个小问题,然后再逐步细分到更小的问题,直到每个问题对我们来说,都已经很简单。解决了所有的小问题,逐步向上汇总,就完成了最初的复杂问题。值得强调的是:从小问题汇总到最后的复杂问题,这是“自顶向下”编程的一个过程,这不叫“自底向上”编程。

 

它的本质是什么

“自顶向下”编程是典型的工程师思维方式,一事一议地解决问题。大问题分解为小问题的具体方法,视实际操作者水平高低,会有很大的差异。但即使是一个新手,也有办法入手。这种解决问题的方式,效率是比较高的,但可重复性很低。下次遇到一个即使是类似的问题,我们会发现,分析方法或许是可以重用的,但具体的代码和工作量往往难以重用。

 

自底向上编程

自底向上编程,是这样一种操作过程:先描述,后编程。当我们面对一个复杂的大问题的时候,我们首先把它泛化为一大类问题,用一些基本概念对所有这些问题进行描述。然后逐步增加那些必不可少的概念,直到我们能够完整而细致地把这些问题描绘清楚。这一过程有点像构建一个公理体系。我们逐渐增加公理的数量,直到整个体系中的所有感兴趣的命题都可以用这些公理推导出来。

又像构建一种语言(DSL),这种语言比我们所使用的编程语言的粒度大很大,提供了描述问题时所用的大块抽象积木。同时它比我们描述问题所用的自然语言更加清晰准确,因为它是可以由计算机理解的语言。

在这样的描述工作完成之后,我们开始编程。首先实现的是这些公理体系,或者说是这些DSL的基本概念。这是整个复杂问题的底部。在此之上,我们继续添加定义和定理,或者说添加DSL中的高阶概念。这些逐步构建起来的更加复杂的模块,让我们距离最初的复杂问题越来越近。直到最初的问题被证明,或者说被DSL中的高阶概念表达,也即被解决。这就是“自底向上”的编程过程。

它的本质又是什么

自底向上”的编程过程,远比“自顶向下”编程复杂得多。它的目标不是解决一个具体的问题,而是解决一类具有普遍性问题。它的着眼点不仅仅是眼前问题的解决,而是程序在需求改变下的健壮性。

两种编程方式举例

我们从一个具体的例子,来看两者的不同。

比如现在我们的需求是:求一个数字列表的每个数的平方和。

自顶向下的编程思路是这样:

  1. 首先设一个累加器并预设初始值为0
  2. 然后遍历整个列表,
  3. 取出列表中的每个数字,
  4. 计算平方,
  5. 并且累加。

思路很简单,具体代码如下:


a=[1,2, 3, 4,5]

def calc(lst):
    sum= 0
    for i in lst:
        sum+= i**2
    return sum

squareSum= calc(a)
print(squareSum)

它的特点是思路直接,效率高。这是一个紧密耦合的高度定制化的功能,除了解决这个问题之外,不会再有什么其他的用处。如果需求发生了某种变化,只能重建一个新的函数来实现。虽然代码的基本思路是可以重用的,但给人以某种一事一议的特殊感。

自底向上的编程思路是这样:

  1. 首先这个问题的本质是:对一个列表中的每个数字做某种处理后再进行某种形式的合并。
  2. 第一个某种处理:是在进行平方处理,我们应当有一个平方的概念(函数)。
  3. 第二个某种形式的合并,是在累加,我们应当有一个累加的概念(函数)。
  4. 再然我们实现一个先处理后合并的机制。
  5. 将这三者组合起来,解决问题。

代码如下:

from functools import reduce

a=[1,2, 3, 4,5]

def sum(a, b):
    return a+b
def sqare(a):
    return a**2
def reduceMap(mapFunc, redFunc, lst):
    return reduce(redFunc, map(mapFunc, lst))

squareSum= reduceMap(sqare, sum, a)
print(squareSum)

它的特点是思考方式不那么直接,开发效率和运行效率可能都会略低一些。但这种解决问题的方法似乎在某种角度来看“更接近问题的本质”,它是试图解决一大类问题,这类问题的需求由三个独立的函数来描述,如果三处的具体需求发生改变,我们只须修改一个函数即可。

如果需求从此不变,当然第一种方法是简单的。但需求是必然变化的。

如果需求发生了改变

比如,我们改一下需求:在有些情况下只对列表中的奇数求平方和。

自顶向下的编程思路就不展开了,增加一个函数即可。复制粘贴后略做修改:


a=[1,2, 3, 4,5]

def calc(lst):
    sum= 0
    for i in lst:
        sum+= i**2
    return sum

def calc2(lst):
    sum= 0
    for i in lst:
        if i%2==1:
            sum+= i**2
    return sum

# squareSum= calc(a)
squareSum= calc2(a)
print(squareSum)

这两个函数看起来就多少感觉有些别扭了,不但重复了相似的逻辑结构,而且都相当特殊,几乎不会有复用的机会。可以预见到如果继续增加需求,还会继续增加函数。

自底向上的编程思路是这样:

  1. 新的需求增加了判断奇偶数的概念,增加一个函数。
  2. 新的需求增加了从列表种进行挑选的概念,增加一个函数。
  3. 同时新的函数可以覆盖原来的函数,旧的函数可以做修改(可选优化)

代码如下:

from functools import reduce

a=[1,2, 3, 4,5]

def sum(a, b):
    return a+b
def sqare(a):
    return a**2
def isOdd(a):
    return a%2==1
def reduceMapFilter(mapFunc, redFunc, fltFunc, lst):
    return reduce(redFunc, map(mapFunc, filter(fltFunc, lst)))
def reduceMap(mapFunc, redFunc, lst):
    # return reduce(redFunc, map(mapFunc, lst))
    return reduceMapFilter(mapFunc, redFunc, lambda x:x, lst)

# squareSum= reduceMap(sqare, sum, a)
squareSum= reduceMapFilter(sqare, sum, isOdd, a)
print(squareSum)

虽然这个思路对于一个需求的改动,增加了两个函数,但函数的功能非常基础,且逻辑结构不重复。随着需求的继续增加,可以预见,这些函数会有更多复用的机会(实际上,考虑到旧函数的修改,新函数已经开始复用了)。如果我们能够预见到整个软件的需求会向这个方向发展,我们会考虑按这个思路来实现代码。

总结一下

自顶向下的编程思路适合规模较小、需求高度稳定、短期项目。思路的重点的问题分解。简单直接好理解,上手速度快,代码运行效率高。如果你给别人做外包开发,相信这种编程思路是最佳选择。

自底向上的编程思路是否规模较大、需求变化较多、长期项目。思路的重点是设计描述语言。思维过程复杂,运行效率略低。初期上手速度慢,后期随着复用程度的提高,开发会有加速效应。如果你做一个自己的研究项目,应当尝试这种编程思路。

猜你喜欢

转载自blog.csdn.net/xiaorang/article/details/105464122
今日推荐