什么是简单
什么是简单
一、什么是简单
苹果的风靡引爆了世界,改变了人们的生活。其最吸引人的一点,就是简单。乔布斯说:“简单之所以比复杂更难,是因为你必须努力清空你的大脑,让它变得简单。”平时,也经常听到同事们在说,要把系统做简单一点,好用一点。
那么究竟什么是简单呢?
百事不知问问百度百科:
词语:简单
反义词:复杂 困难
近义词:容易
释义:
①结构单纯;头绪少;与复杂相对;容易理解、使用或处理:情节~ㄧ~扼要ㄧ这种机器比较~ㄧ他简简单单说了几句话。
②(经历、能力等)平凡(多用于否定式):李队长主意多,有魄力,可真不~。
③草率;不细致:~从事。
④对自然规律的一种理解和追求。如自然定律的简单性。W.海森伯说:今天,我们甚至可以以更极端的形式说,“静止”一词是由地球静止着这个陈述来定义的,并且我们把相对于地球是不动的每一物体描述为静止的。如果对“静止”一词作如此理解——而这是普遍接受的意义——那么,托勒密是对的,哥白尼却错了。只有当我们沉思了“运动”和“静止”的概念,并把运动理解为至少是关于两个物体的关系的陈述,我们才能够把关系倒转过来,使太阳成为行星系的不动的中心并获得一个简单得多、也更统一的关于行星系的观点,后来牛顿充分评价了这种观点的阐明的力量。因此,哥白尼把一个全新的要素加到直接经验之上,我称这个要素为“自然定律的简单性”,而它与直接经验毫无关系。
⑤中国古代哲人对世界的一种理解。聂文涛说:《系辞上》认为乾卦通过变化来显示智慧,坤卦通过简单来显示能力。把握变化和简单,就把握了天地万物之道。所以“乾以易知,坤以简能。”“易简,而天下之理得矣。”古人以此研究天地、万物、社会、生命和健康。辨证的世界让我们对每件事的拿捏都要恰到好处,简单有时就是容易,简单有时却恰恰是不容易。
⑥一种现代审美。如~人生、~法则。
⑦基本的,不能缩小或减少的。如~再生产、~分吗组、~物质等。
⑧动词。有简化的意思。如,把问题~一点。
看看释义⑤,“简单有时就是容易,简单有时却恰恰是不容易。”换句话说:简单的事物难以构造,但易于理解;复杂的事物易于构造,但难于理解。
因此,简单和容易,是完全两个概念。简单是一个客观的描述,即它能否反映事物本质的东西,就像上面所说的“自然定律的简单性”。而容易,则更加主观,它表明了事物能否在我的能力范围之内去解决它。如果我们只做容易的事情,会容易地导致复杂度的快速膨胀,最终导致失控。
事实上,我们目前就是在容易的写程序。
二、软件领域中的简单
好吧,回到我们要讨论的软件领域。
我们不妨先看看《代码大全》怎么说的:
“设计的首要目标就是要让复杂度最小。要避免做出“聪明的”设计,因为“聪明的”设计常常是难于理解的。应该做出简单且易于理解的设计。如果你的程序设计方案不能让你在专注于程序的一部分时安心的忽视其他部分的话,这一设计就没有什么作用了。”【注:第五章软件构建中的设计,P80】
我们再看看简单设计的最初提出者Kent Beck怎么说的。他在《实现模式》一书中,充分强调了简单:
“在各个设计层次上都应当要求简单。对代码进行调整,删除所有不提供信息的代码。设计中不出现无关的元素。对需求提出质疑,找出最本质的概念。去掉多余的复杂性后,就好像有一束光照亮了余下的代码,你就有机会用全新的视角来处理它们。”【注:第三章一种编程理论 P14】
用Kent Beck再体现的一个隐喻来说,就是“把麦子和糠分开,是编程中不可或缺的一部分。”【注:第三章 一种编程理论 P14】
因此,请不要再随意的使用“简单”这个词,除非你理解了它。
那么如果在软件领域中,非要再给简单下个定义,我认为下面这个定义是比较精确的:
简单就是:
1) 清晰的抽象:聚焦领域,形成抽象层并隐藏细节
2) 清楚的表达:对概念进行有意义的,简洁而不晦涩的展现与沟通
三、如何理解简单
对于第一点,我们也许认为已经有所掌握,比如会说我会写类来表达对象以及隐藏细节,会用继承、多态来表达对象关系。但这里强调的是,要把焦点放在问题定义上。就像多根细细的棉线,如何在大风中保持每根互不缠绕?就像周立波的头势,每时每刻每根头发都保持清爽?清晰的抽象表达了本质的概念以及概念之间的关系,这才是不变的东西。它和程序没有关系,和C++、C、Java也没有关系。
我们拿最简单的例子来说,我们说值。值是什么?本质上就是一个数字,1,2,3。值本身不会变。1能变成2吗?不能。但这个概念,映射到实现层面,就变成了C++里面的常量,变量。最近流行的趋势是,不要用变量,要尽量用常量const。这就是业界在朝值的本质概念进化的过程。那么变量是什么?I = 1;经过一个运算,I=2了。这就违背了数学意义上值的本质,使得程序不再是确定性的,从来带来了复杂度。我们没觉得它有问题,那只是说,这种复杂度在你的控制之下。如果我们换种写法呢?I=1;I = I+1。I最后是2。那么这种问题就会更大。为什么呢?如果你在多线程环境下,访问和修改I,这就要处理它的并发保护,这就是把I的值的变化和时间进行耦合的结果,它的复杂度又增加了。在某些语言中,I=1后,I的值就不再变化,始终是1,比如Erlang。
1> X=1.
1
2> X=2.
** exception error: no match of right hand side value 2
这样,语言特性就保证了对概念的精确表达。这就是第二个问题:清楚的表达。
清楚的表达是实现层面的问题。要特别注意的是,它应该建立在清晰抽象的基础上。那么这个时候就涉及到面向对象、分层、编程语言等等各种实现层面的表达力问题。但值得我们担忧的是,清楚的表达在很大程度上被我们忽略了。比如我们看一个例子:按钮。我们说,按钮是个概念。那么这个概念如何表达呢?是不是这样?
Button text=“Apply”;color= Green;command=accumulate
好,那问题来了,我们如何用C++实现这个表达?代码会是什么样子?比如是不是这样:
Button button(“Apply”, Green,accumulate)
还是这样:
Button button(“text=\“Apply\”;color=Green;command=accumulate”)
我们再想想,在各种语言里面,它是怎样的?有没有这种表达力?
那么如何实现表达力呢?出于对表达力的深刻认识,我们开始使用DSL(Domain Specification Language)、使用原语、使用XML。
但问题是,我们现在确实充分使用了这些工具,但却对第一点(清晰的抽象)没有分析透彻,结果导致了难以使用的API。
所以说,简单的事物难以构造,但一旦真正能够构造成功,它确实很容易使用和扩展。
我们再看一个Python的例子,它来源于ZTE CCCG(Clean Code Coach Group)核心教练团队为代码大全培训提高班提供的LayerOutDSL实战例题。这个题目设计的初衷就是实现控件的布局,起因是因为Python提供的布局管理器Sizer太难用。
首先我们看问题定义部分。
我们知道,控件的布局,其实就是相对的位置。比如Button1在Button2的左边,Edit1在TreeView1的上边。这种相对位置就是布局核心的概念。那么我们的控件是什么?就是一个待显示的元素:Element,Element需要知道它在哪里显示,它的父控件是谁。
我们先看一个用上述模型构建出来的计算器模型(纯粹的界面,无事件响应):
然后我们再看如何清晰表达。
代码摘选:
calcblk = block_with_margin([
btn('7'),btn('8'), btn('9'), btn('/'), btn('Mod'), btn('And'),
btn('4'),btn('5'), btn('6'), btn('*'), btn('Or'), btn('Xor'),
btn('1'),btn('2'), btn('3'), btn('-'), btn('Lsh'), btn('Not'),
btn('0'),btn('+/-'), btn('.'), btn('+'), btn('='), btn('Int'),
btn('A'),btn('B'), btn('C'), btn('D'), btn('E'), btn('F')], 5 ,6, 0.05)
这是输入数字盘的代码。它表明了这是一组button的block,而且带有留白的。[btn…]是button列表,5为行数,6为列数,0.05表明留白占据整个区域的比例(四周均匀留白)。
block_with_margin返回的是什么呢?是和Element一样的闭包。
def block_with_margin(eles, r, c, ratio):
cc = lambda x: center(x,ratio, ratio)
return block([cc(ele)for ele in eles], r, c)
为什么要这样设计?
因为从概念上讲,block_with_margin之后,应该还可以和其他的Element闭包一起再组合。这就给程序代码带来了极大的灵活性。它本质上反映的是一种世界观,即:
1) 定义多种原子,其具有统一原型,可以认为就是“值”
2) 定义多种原子操作,其为“值”的处理,返回的还是原子,可以认为是“函数”
3) 定义原子之间的控制,可以认为是“过程”。过程是对外有实体意义的东东。
其实,这就是函数式编程的主要内容。
这种表达我们认为是清楚的,因为我们很容易看懂(这里需要特别注明,所谓容易看懂,不是指你第一眼就能理解它,而是一旦你理解了它,你就能彻底掌握它。它区别与你貌似懂了,但以后再遇到,你还是需要重新理解),而且很容易扩展。
四、工具集
在清晰的抽象和清楚的表达的表达之间,还有一个转换过程,我们称之为工具集。
工具集是指像面向对象、分层、语言、重构、TDD等各种使能手段。就是说,如果你知道了问题定义,也知道怎么去表达,你总归要实现这种表达,工具集就是提供你实现的手段。你要知道各种工具是解决什么问题的,让让你的工具和你的问题进行良好的匹配。比如说你实现并发,那么用线程就不一定是一个好的选择,因为线程是一个重量型的工具,你要处理同步、维护状态变化,这往往使得线程难以控制。这时你可以考虑进程,甚至你可以学习Erlang来解决它。如果你实在不想学,那么你就必须要把线程内的状态和时序变化,变成确定性的行为,隔离开线程间的交换和变化,使得线程在你的控制之下,而不是依靠debug去寻找莫名其妙的故障。要知道,debug的本质含义,是你理解问题,并可以在脑子里把代码流程顺一遍,而不是说你用IDE去帮助你理解问题。IDE不可能帮助你理解问题,除非你真的理解了问题。
从前面分析,我们知道简单是很难的。所以,你要始终关注简单性,这贯穿于整个代码周期。要对简单理解到位,并始终坚持,就是你要做的。至于如何能够做到简单,如何保持简单,这是另外的话题。
五、总结
我们总结一下前面说的内容。在这里,我直接引用了常高伟的blog《什么是简单的设计》【注:www.cnblogs.com/chgaowei/archive/2011/07/16/2108321.html】的文章:
1)首先要能够解决实际问题的;
这是所有设计要达到的首要目标,虽然实现的手段和方法,效果不同。简单的设计也必须达到这个目标。
2)易于理解的;易于实现的;易于维护的;
我认为这是简单的设计最吸引人的地方,也是它最有价值的地方。
3)把握问题的本质的设计;
物体运动的表现形式很复杂,但是它的本质:牛顿定理却简单。所以,我认为事物的本质是简单的,之所以复杂,可能是因为我们还没有把握事物的本质。
4)避免了过度设计;
过度设计是追求简单设计的一个非常大的障碍。多未必好,过犹不及。
5)往往是对复杂设计的再加工,经历了简单——复杂——简单——复杂——简单的循环的过程。
追求简单设计的过程是一个逐渐深入的过程,最终会深入到问题的本质。
6)往往比复杂的设计更加需要思考的投入;
编程是一项很复杂的工作,是一个很有挑战性的智力活动,程序员完全不是IT民工,也不是软件蓝领。如果你真的是想把公司做大做强,那就雇佣最好的程序员,让他们把事情做简单。
声明:
文中引用了ZTE资深架构师邓辉的一些观点和代码示例,仅供学习使用。