自顶向上和底向下两者集成的区别?,自顶向下开发方法的优点是

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

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

  自顶向下编程

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

  它的本质是什么

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

  自底向上编程

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

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

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

 

它的本质又是什么

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

 

两种编程方式举例

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

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

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

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

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

a=[1,2, 3, 4,5]def calc(lst): sum= 0 for i in lst: sum+= i**2 return sumsquareSum= calc(a)print(squareSum)

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

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

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

代码如下:

from functools import reducea=[1,2, 3, 4,5]def sum(a, b): return a+bdef sqare(a): return a**2def 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 sumdef 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)

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

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

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

代码如下:

from functools import reducea=[1,2, 3, 4,5]def sum(a, b): return a+bdef sqare(a): return a**2def isOdd(a): return a%2==1def 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)

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

总结一下

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

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

 

Published by

风君子

独自遨游何稽首 揭天掀地慰生平

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注