/** * 谨献给Yoyo * * 原文出处:https://www.toptal.com/software/declarative-programming * @author dogstar.huang <chanzonghuang@gmail.com> 2016-05-15 */
目前,声明式编程是诸如数据库,模板和配置管理这样普遍而多样领域的主导范式。html
简而言之,声明式编程由须要指导一个程序须要作什么,而不是告诉它如何作到组成。在实践中,这种方法须要提供一个用于表达用户想要什么,并经过屏蔽底层结构(循环,条件,任务)实现指望的最终状态的领域特定语言(DSL)。node
虽然这种模式在其必要的地方是有着显著的改善,但我主张,声明式编程有明显的局限性,此限制我会在本文中进行探讨。此外,我建议左右开弓,既抓住声明式编程的好处,同时又取代其局限性。git
警告:这篇文章是多年来我的在声明工具中奋斗的结果。许多我在这里的说法都是没有完全证实的,有的甚至基于事实价值。一个适当的、批评的声明式编程会花费大量的时间,精力,而且我要回到过去使用不少这样的工具;个人心是不会给这样的承诺的。这篇文章的目的是毫无保留与你们分享一些想法,并展现为我工做的东西。若是你已经挣扎于声明性编程工具中,你可能会发现喘息的机会和选择。若是你喜欢它的范式及其工具,不要把我看得过重。程序员
若是声明式编程对于你工做得很好,那么我没什么好说的了。github
对于声明式编程,你能够爱,能够恨,但不能忽略。sql
在咱们探讨声明式编程的限制前,有必要了解它的优势。shell
能够说最成功的声明性编程工具是关系数据库(RDB)。它甚至多是第一个声明工具。在任何状况下,RDBS体现了我认为声明式编程原型应具有的的两个属性:数据库
领域特定语言(DSL):对于关系数据库通用的接口是一个叫作结构化查询语言的DSL,也就是最为大众所知的SQL。编程
DSL为用户隐藏了较低层级:自从埃德加·科德关于RDBS的最初论文后,很朴素地,这种模式的力量就把指望的查询从实现它们的基本回路,索引和访问路径分离出来。数组
在RDB前,大部分数据库系统是经过必要的代码来访问的,这严重依赖于底层的细节,如记录的顺序,索引和数据自己的物理路径。由于这些元素随着时间的推移而变化,因为数据结构中一些基础性的变化,代码常常会中止工做。由此产生的代码是很难写的,难以调试,难以阅读和难以维护。我会用我夸张的肢体动做告诉你,大部分的代码极度多是,长长的、尽是众所周知的大鼠条件句、重复的和微妙的、状态依赖的bug的。
在面对这种状况,RDB为系统开发人员提供了一个巨大的生产力的飞跃。如今,不用成千上万行必要的代码,你有一个明肯定义的数据格式,再加上数百(甚至是数十)个查询。所以,应用程序只须要处理一个抽象的,有意义的而且持久化的数据,并经过一个强大却又简单的查询语言接入它。采用它们,RDB中可能提升了程序员,以及招聘他们的公司的生产效率。
通常声明式编程列出来的优势有哪些?
声明式编程的拥护者很快指出它的优点。然而,即便他们也会权衡地认可。
一、可读性/可用性:一个DSL一般比伪代码更接近天然语言(如英语),所以对于非程序员更具可读性,也更容易学习。
二、简洁:大部分模板是由DSL抽象,几行就作一样的工做。
三、重用:更容易建立可用于不一样目的的代码;一些使用命令式结构时,那是出了名的难。
四、幂等性:能够与最终状态工做,并让程序来帮你搞定。例如,经过一个update操做,若是它不存在,你能够插入一行,或者若是它已经存在,就修改它,而不是编写代码来处理这两种状况。
五、错误恢复:很容易指定一个在第一个错误就中止的结构,而无需为每个可能的错误添加错误侦听者。(若是你曾经在node.js中写了三层嵌套的回调,你就明白个人意思了。)
六、引用透明:尽管这个优点一般与函数式编程相关的,实际上对于任何最小化状态的手工处理以及反作用依赖的方法都是有效的。
七、交换性:没必要指定将在其中实现的实际顺序,便可表达最终状态的可能性。
虽然上面这些常常会做为声明式编程的优点而引用,我想将它们凝结成两种属性,这将做为当我提出了一个替代方法时的指导原则。
一个针对特定领域的高级层:声明式编程使用了其适用领域的信息来建立一个高级层。很显然,若是咱们处理数据库,咱们要的一组操做来处理数据。上面七个优势大部分来自精确针对特定问题领域的高级层的建立。
防错(傻瓜proofness):一个域量身定制的高级层隐藏了实现的必要细节。这意味着你犯更少的错误,由于该系统的低层细节简直没法访问。这一限制消除了你代码中的许多类的错误。
在接下来的两节中,我将介绍声明式编程的两个主要问题:独立性(separateness)和缺少展开(lack of unfolding)。每一个批判须要其可怕之处,因此我会用HTML模板系统做为声明式编程的缺陷的具体例子。
试想一下,你须要编写一个有很是很是多页面的Web应用程序。把这些视图硬编码到一系列HTML文件不是一种好的选择,由于这些网页的不少组件都会改变。
最简单的解决办法,就是经过链接字符串来生成HTML,这彷佛很可怕,你很快就会寻找一种替代。标准解决方案是使用一个模板系统。虽然有不一样类型的模板系统,出于本次分析的目的咱们将回避他们的差别。咱们能够把他们所有类似地考虑成,他们在该模板系统的主要任务是提供使用条件和循环链接HTML字符串的替代方案,就像RDB经过数据记录编写代码来循环做为替代方案那样。
假设咱们用了一个标准模板系统;你会遇到摩擦的三个来源,我将按重要程序升序列出。第一是模板必须驻留在与代码分隔的独立的文件。由于模板系统使用的是某个DSL,其语法是不一样的,所以不能在同一个文件。在文件数量不多的简单的项目里,须要保持模板文件隔离可能会扩大三倍的文件的数量。
对于嵌入式Ruby模板(ERB),我打开了一个例外,由于这些都集成到了Ruby源代码。这不是用其它语言编写ERB-启发工具的场景,由于这些模板也必须被存储为不一样的文件。
摩擦的第二来源是DSL有其本身的语法,和你的编程语言有所不一样。所以,修改DSL(更不用说编写本身的)是至关困难。为了走进幕后并改变工具,你须要了解标记化和解析,这是有趣且富有挑战性的,但很难。我刚好把这看做是一个缺点。
如何才能可视化DSL?这并不容易,咱们只是说DSL是低层次结构之上干净,闪亮的层。
你可能会问,“究竟为何你要修改你的工具?若是你正在作一个标准的项目,一个精心编写的标准工具应该能符合要求。”也许是,也许不是。
DSL历来都没有编程语言的强大功能。若是有,它就再也不是一个DSL了,而是一个完整的编程语言。
可是,这不是DSL的关键所在吗?没有编程语言可用的强大功能,因此咱们能够实现抽象并消除大部分缺陷的来源?也许是吧。然而,大多数的DSL开始简单,而后逐步包含愈来愈多编程语言的设施,直到事实上,它变成了一个。模板系统是一个很好的例子。让咱们来看看模板系统的标准功能,以及它们如何关联到编程语言设施:
在模板中替换文本:变量替换。
模板重复:循环 。
若是条件不知足,避免打印模板:条件语句。
小部件(Partial):子程序。
小助手(Helper):子程序(与小部件的惟一区别是,小助手能够访问底层的编程语言,让你能够跳出DSL)。
这种的说法,即一个DSL是有限的,由于它同时渴望和拒绝编程语言的能力,是直接正比于所述DSL的特征,是直接可映射到一个编程语言的特征的。在SQL的状况下,参数是弱的,由于大多数SQL提供的事情不会像你在一个正常的编程语言找到那样。在光谱的另外一端,咱们发现那里几乎每个功能使得DSL模板系统朝着BASIC收敛。
如今让咱们退一步考虑摩擦的三个典型的来源,由独立性的概念总结出来的。由于它是分离的,DSL须要放置在一个单独的文件;这是很难修改(甚至更难写你本身的),和(常常,但不老是)须要你来一个接一个地添加缺乏的一个真正编程语言的功能。
不管如何精心设计,独立性是任何DSL与生俱来的问题。
咱们如今转到声明工具的第二个问题,这是广泛存在的,但并非与生俱来。
若是几个月前我写了这篇文章,这部分将被命名为:大多数声明工具是#@!$#@!复杂的,但我不知道为何。在写这篇文章过程当中,我找到了一个更好的名字:大多数声明工具比须要他们的还要复杂得多。我会在这部分剩下的部分解释为何。为了分析工具的复杂性,我提出了一个所谓的复杂性间隙(complexity gap) 度量。复杂性间隙是指解决一个给定的问题,与在工具打算取代的低级别(据推测,普通的必要代码)解决问题的工具之间的差别。当前者比后者更复杂时,咱们就陷入了复杂性间隙。对于更复杂,个人意思是更多的代码行,这些代码难以阅读,难以修改和难以维护,但所有这些不必定老是须要的。
请注意,咱们不是把较低级的解决方案和可能的最佳工具相比,而是和没有工具相比。这反映了医疗原则“第一,不伤害”。
具备较大复杂性间隙的工具的迹象有:
当务之急须要几分钟描述丰富的细节的东西,使用工具将须要花几个小时来编码,甚至当你知道如何使用工具时。
你以为你老是不断在围绕工具工做,而使用工具工做。
你在苦苦解决正属于你正在使用工具的领域一个简单的问题,但你找到最好的Stack Overflow答案描述了一个解决方法。
当这个很是简单的问题能够经过通用特征来解决(这并不在存在于工具中),并你在Github的issue上看到这个类库有着此特征漫长的讨论,以及+1s 引用。
一个慢热型,挠痒,渴望挖坑的工具,并要在你本身的for-loop中作了所有的事情。
这里可能会一些骗人的情绪,由于模板系统并不复杂,但这种比较小的复杂性差距并非其设计的优势,而是由于适用领域很是简单(记住,这里咱们只生成HTML)。每当一样的方法被用于更复杂的领域时(如配置管理),复杂性间隙可能很快就让您的项目陷入泥潭。
这就是说,一个工具在某种程度上比它打算取代的低级更加复杂,并不必定是不能接受的;若是该工具的代码更易读,更简洁,更正确,它是值得的。当工具比它要替换的问题还要复杂好几倍时,这是一个问题;这是不可接受的。正如Brian Kernighan的名言说道,“控制复杂性是计算机编程的本质。” 若是一个工具明显增长了你项目的复杂性,为何还要用它呢?
那么问题是,为何一些声明式工具比他们须要的还要复杂得多?我认为将其将其归咎于设计缺陷是错误的。这样通常性的解释,对这些工具的做者进行的人身攻击,是不公平的。必须有一个更精确的和启发性的解释。
折纸时间!折纸日期!具备高层接口的、抽象低级别的工具须要能从较低级别展开较高级别。
个人论点是,任何提供了一个高层接口以抽象较低级的工具必须能从较低级展开较高级。此展开的概念来自克里斯托弗 亚历山大的巨著,秩序的性质 - 卷二。它是(绝望地)超出了本文的范畴(更不用提个人理解)来总结这一巨著对于软件设计的影响范围;我相信在随后的几年其影响是巨大的。提供展开过程的严格定义也超出了本文的范畴。我将在这里以启发式的方式使用此概念。
解折叠过程是指,以逐步的、不否认已有的方式建立进一步的结构。在每个步骤,每一次变化(或分化,用亚历山大的术语就是)与以往任何的结构保持和谐,以往的结构,简单地说,是过去变化的结晶序列。
有趣的是,Unix是从一个较低级展开较高级的一个很好的例子。在Unix中,操做系统中两个复杂的功能,批处理做业和协同程序(管道),都只是基本的命令简单的扩展。因为固化的基本的设计决策,如让一切皆为字节流,shell是一个用户级程序以及标准I/O文件,UNIX可以以最小的复杂性提供这些复杂的功能。
为了强调为何这些是展开很好的例子,我想引用丹尼斯里奇,UNIX的做者之一,在1979年的一些节选:
关于批做业:
“......新的过程控制方案即刻渲染一些很是有价值的且易于实现的功能;例如分离进程(带
&
)和做为一个命令递归使用shell。大多数系统都提供某种特殊的批处理做业提交设施,以及与交互使用彻底不一样文件的特殊命令解释器。”
关于协同:
“Unix管道的天才之处偏偏在于它是从单纯的方式不断用一样的命令构造。”
UNIX开拓者丹尼斯里奇和肯·汤普逊在他们的操做系统中创造了展开的有力证实。他们还把咱们从所有皆是Windows的反乌托邦的将来拯救了出来。
这种优雅和简洁,我认为,来自一个展开的过程。批处理做业和协同程序展开于以前的结构(在用户端的shell中运行命令)。我认为,正是极简的哲学和资源的限制,团队才建立了Unix,系统才得以逐步演变而来,也正由于如此,才得以兼并先进的功能而不背弃基本的功能,由于没有足够的资源来不这样作。
在没有展开的过程当中,高层将比须要的更远为复杂。换言之,大部分声明工具的复杂性的缘由是,它们的高层没有从他们打算替换的低层展开。
这种展开(unfoldance)的缺少,若是你原谅新词,从低级别保护用户一般是无必要的。这种防错的强调(从低级错误上保护用户)会弄巧成拙形成很大复杂性间隙是,由于额外的复杂度会产生新类的错误。雪上加霜的是,这些类的错误与领域无关,而是与工具自己有关。若是咱们把这些错误描述为医源性,咱们就不会走得太远。
声明性模板工具,至少当应用到生成HTML视图任务时,是一个背弃它所意图替换的低层次的高层次的原型状况。 怎么会这样?由于生成任何不平凡的视图须要逻辑,和模板系统,特别逻辑更少的,经过正门放逐逻辑而后经过猫门走私一些回来。
注意:对于大型复杂性间隙的一个更弱的理由是,当一个工具做为魔法销售时,或诸如只是工做的一些东西时,低级别的不透明被认为是一种资产,由于魔法工具老是不须要你明白为何或如何就能工做的。在个人经验,更魔法的工具声称是,它会更快地把个人激情变成挫败。
可是对于关注点分离是什么?不该该把视图和逻辑保持独立吗?这里核心错误,是把业务逻辑和展现逻辑放在同一个包。业务逻辑确定在模板中没有立锥之地,但展现逻辑不管怎样都存在。从模板排除逻辑会推使展现逻辑进入笨拙地被容纳的服务。关于这一点,我还欠Alexei Boronine一个明确的说法,他在这篇文章中为它创造了一个超级棒的案例。
个人感受是,大约三分之二的模板的工做在于它的展现逻辑,而另外的三分之一则处理通常的问题,诸如链接字符串,闭合标签,转义特殊字符,等等。这是生成HTML视图低级别本质的两面。模板系统能适当处理第二部分的一半,但它们处理很差第一部分。更少逻辑的模板背弃了这个问题,迫使你来笨拙地解决它。其余的模板系统受苦,由于他们真正须要提供一个不平凡的编程语言,使他们的用户确实能够编写展现逻辑。
总结一下;声明模板工具受苦,是由于:
若是他们是从他们的问题域展开,则不得不提供生成逻辑模式的方式;
提供逻辑的DSL并非一个真正的DSL,而是一种编程语言。须要注意的是其余领域,如配置管理,也遭受缺少“unfoldance”之苦。
我想以一个在逻辑上与这篇文章的线索无关,但在其情感核心上有着深深的共鸣的声明来关闭此批判:咱们用来学习的时间是有限的。人生苦短,而最重要的是,咱们还要工做。在咱们的限制面前,咱们须要花时间学习有用而且经得起时间的东西,即便面对突飞猛进的技术。这就是为何我劝你使用不仅是提供了一个解决方案,更对其自身适用性的领域域实际上有着耀眼光芒的工具。RDB教会你数据,而Unix教会你操做系统的概念,可是使用不可接受的未展开的工具,我老是以为我是在学习一个次优解决方案的复杂性,而仍停留在它意图解决的问题本质的黑暗之中。
我建议你要考虑的是启发式的,照亮他们问题领域的、有价值的工具,而不是在声称的功能背后掩盖问题领域的工具。
为了克服我在这里提出的声明式编程的两个问题,我提出了一个双子方法:
使用数据结构领域特定语言(dsDSL),以克服独立性。
创造一个从低级别展开的高级别,以克服复杂间隙。
数据结构DSL(dsDSL)是一种由编程语言的数据结构构建的DSL。其核心思想是使用你已有的基本数据结构,如字符串,数字,数组,对象和函数,并结合他们以创造处理特定的领域的抽象。
咱们但愿保持声明结构的能力或行为(高级别)而没必要指定实现这些构建的模式(低级别)。咱们想克服DSL和咱们编程语言之间的独立性,以便每当咱们须要它时能够自由使用编程语言的强大功能。这不只是可能的,还可直接经过dsDSL。
若是你在一年前问我,我原本觉得dsDSL的概念是新的,而后有一天,我意识到JSON自己就是这种作法的一个很好的例子!解析过的JSON对象包括了声明展现数据实体的数据结构,以便获得DSL的优点,同时也使其在编程语言中易于分析和处理。(可能还有其余的dsDSL,但到目前为止,我尚未遇到过。若是你知道,我会很感激你在评论部分说起它)。
像JSON,一个dsDSL具备如下属性:
一、它包含一个很是小的函数集:JSON有两个主要功能,parse
和stringify
。
二、它的功能最经常使用是接收复杂和递归的参数:一个解析的JSON是数组或对象,一般包含进一步的数组和对象在内。
三、这些功能的输入很是符合指定的形式:JSON有明确和严格执行验证的模式以告诉有效或无效的结构。
四、这些功能的输入和输出均可以被编程语言包含和生成,而不须要单独的语法。
但dsDSLs在许多方面超越JSON。让咱们建立一个使用JavaScript生成HTML的dsDSL。稍候我会谈到这种作法是否能够延伸到其余语言的问题(预告:它绝对能够在Ruby和Python完成,但可能在C不行)。
HTML是由尖括号(<
和>
)分隔tag
组成的标记语言。这些标签能够有可选的属性和内容。属性是简单的键/值对属性的列表,而且内容能够是文本或其它标签。属性和内容对于任何给定的标签都是可选的。我有点简化,但它是正确的。
在dsDSL中展现一个HTML标签一个直接的方法是经过使用具备三个元素的一个数组:- 标签:一个字符串;- 属性:一个对象(扁平的,键/值类型)或是undefined
(若是没有属性是必要的);- 内容:一个字符串(文本),一个数组(另外一个标签)或undefined
(若是没有内容的话)。
例如,<a href="views">Index</a>
能够写成:['a', {href: 'views'}, 'Index']
。
若是咱们想把这个锚点元素嵌入到一个div
并用类links
,咱们能够这样写:
['div', {class: 'links'}, ['a', {href: 'views'}, 'Index']]
。
为了在同级别列出几个html标签,咱们能够在一个数组里包装它们:
[ ['h1', 'Hello!'], ['a', {href: 'views'}, 'Index'] ]
相同的原理能够应用到在标签中建立多个标签:
['body', [ ['h1', 'Hello!'], ['a', {href: 'views'}, 'Index'] ]]
固然,若是咱们不能经过它生成的HTML,这dsDSL不会让咱们走得很远。咱们须要一个generate
函数,它会接收咱们的dsDSL而且输出一个HTML字符串。因此若是咱们运行generate (['a', {href: 'views'}, 'Index'])
,咱们会获得这样的字符串:<a href="views">Index</a>
。
任何DSL背后的想法是指定一些带有一个特定结构的构造,这些结构随后会传递给某个函数。在这种状况下,构成了dsDSL的结构是这样有一到三个元素的数组;这些数组具备一个特定的结构。若是generate
完全验证其输入(完全验证输入是既简单又重要的,由于这些验证规则是DSL语法的精确模拟),它会告诉你你的输入到底在哪里错了。一段时间后,你就会开始意识到在一个dsDSL中是什么区分了一个有效的结构,这种结构将会高度暗示它所生成的底层的东西。
如今,相对于DSL,dsDSL的优劣是什么?
dsDSL是你代码组成的一部分。它会带来更低的行数、文件数和总体开销的降低。
dsDSL易于解析(所以更容易实现和修改)。解析仅仅是遍历一个数组或对象的元素。一样,dsDSL相对容易设计,由于不是建立一个新的语法(每一个人讨厌的),而是能够坚持你的编程语言的语法(人人都讨厌,但至少他们已经知道了)。
dsDSL拥有编程语言所有的功能。这意味着dsDSL,当采用适当时,能同时具备高级和低级工具的优点。
如今,最后的主张是强大的一个,因此我准备花剩下的此部分来支持它。关于采用适当,个人意思是什么呢?为了在实践中明白这点,让咱们考虑一个示例,在此示例中咱们要构建一个表来显示来自名为DATA
数组的信息。
var DATA = [ {id: 1, description: 'Product 1', price: 20, onSale: true, categories: ['a']}, {id: 2, description: 'Product 2', price: 60, onSale: false, categories: ['b']}, {id: 3, description: 'Product 3', price: 120, onSale: false, categories: ['a', 'c']}, {id: 4, description: 'Product 4', price: 45, onSale: true, categories: ['a', 'b']} ]
在实际应用中,DATA
将从数据库的查询动态生成。
此外,咱们有一个FILTER
变量,当初始化时将会是一个带有咱们想要显示的类别的数组。
咱们想要的表格是:
显示表格头部。
对于每个产品,显示这些字段:描述、价格和分类。
不要打印id
字段,但把它看成一个id
属性添加到每一行。另外一个版本:添加一个id
属性到每一个tr
元素。
若是产品是在售的,就放置一个onSale
类。
按价格降序排列产品。
按分类过滤某些产品。若是FILTER
是一个空数组,咱们将展现全部产品。不然,咱们将只显示分类包含在FILTER
里面的产品。
咱们能够经过20行左右的代码建立匹配这个需求的逻辑展现:
function drawTable (DATA, FILTER) { var printableFields = ['description', 'price', 'categories']; DATA.sort (function (a, b) {return a.price - b.price}); return ['table', [ ['tr', dale.do (printableFields, function (field) { return ['th', field]; })], dale.do (DATA, function (product) { var matches = (! FILTER || FILTER.length === 0) || dale.stop (product.categories, true, function (category) { return FILTER.indexOf (category) !== -1; }); return matches === false ? [] : ['tr', { id: product.id, class: product.onSale ? 'onsale' : undefined }, dale.do (printableFields, function (field) { return ['td', product [field]]; })]; }) ]]; }
我认可这不是一个简单的例子,然而,它表明了持久化存储的四个基本功能中至关简单的视图,也就是众所周知的CRUD。任何不平凡的网页应用程序都有比这更复杂的视图。
如今,让咱们看看这些代码作了什么。首先,它定义了一个函数,drawTable
,包含绘画产品表的逻辑展现。此函数接收DATA
和FILTER
做为参数,因此它可用于不一样的数据集和过滤器。drawTable
履行了小部件(Partial)和小助手(Helper)双重做用。
var drawTable = function (DATA, FILTER) {
内部变量printableFields
,只有在这里你须要指定哪些字段是可打印的,避免应对不断变化的需求下的重复和不一致。
var printableFields = ['description', 'price', 'categories'];
而后咱们根据其产品的价格排序DATA
。请注意,不一样的和更复杂的排序标准能够直接实现,由于咱们有咱们所掌握的所有编程语言。
DATA.sort (function (a, b) {return a.price - b.price});
这里咱们返回了一个对象序列;一个数组:第一个元素是table
,第二个元素是它的内容。。这就是咱们要建立的<table>
的dsDSL表示。
return ['table', [
如今,咱们建立了一个带表头的行。为了建立它的内容,咱们使用了dale.do,这是相似Array.map的一个函数,但也可适用于对象。咱们将遍历printableFields
并为每个生成表格标题:
['tr', dale.do (printableFields, function (field) { return ['th', field]; })],
请注意,咱们刚刚实现了迭代,生成HTML的主力,而且咱们不须要任何DSL结构;咱们只须要一个遍历数据结构并返回dsDSL的函数。一个相似的本地的,或用户实现的功能,也能够作到这一点。
如今遍历的产品包含在DATA
中。
dale.do (DATA, function (product) {
咱们经过FILTER
检查该产品是否排除在外。若是FILTER
是空的,咱们将打印该产品。若是FILTER
不是空的,咱们将遍历产品的分类,直到找到一个包含在FILTER
中的。咱们用dale.stop来作这一点。
var matches = (! FILTER || FILTER.length === 0) || dale.stop (product.categories, true, function (category) { return FILTER.indexOf (category) !== -1; });
注意条件的复杂性;它偏偏知足了咱们的需求,而且咱们有充分的自由来表达它,由于咱们是在一种编程语言里,而不是一个DSL里。
若是matches
为false
,咱们将返回一个空数组(因此不会打印此产品)。不然,咱们返回一个带有其正确的id和class的<tr>
,而且经过printableFields
来遍历打印这些字段。
return matches === false ? [] : ['tr', { id: product.id, class: product.onSale ? 'onsale' : undefined }, dale.do (printableFields, function (field) { return ['td', product [field]];
固然,咱们闭合了所有打开的东西。语法不是颇有趣吗?
})]; }) ]]; }
如今,咱们如何将这一表格应用到更广阔的上下文中?咱们编写了一个叫作drawAll
的函数来调用所有生成视图的函数。除了drawTable
,咱们可能还会有drawHeader
,drawFooter
和其余相似的函数,全部这些都会返回dsDSL 。
var drawAll = function () { return generate ([ drawHeader (), drawTable (DATA, FILTER), drawFooter () ]); }
若是你不喜欢上面代码长的样子,那么我说什么都不能说服你了。这是最好的一个dsDSL。你可能也会中止阅读这篇文章(而且也会留下一个有意义的评论,由于若是你已经研究这一块好久的话,你赢得了这样作的权利)。但严肃来讲,若是上面的代码你以为不优雅,那么这篇文章的其余东西也不会。
对于那些仍然和我在一块儿的人,我会回到本章节的主要主张,即一个dsDSL同时具备高层和低层的优点:
低层的优势在于无论什么时候咱们想写代码,均可走出DSL的紧箍咒。
高层的优势在于使用字符表示咱们想声明的东西,并让工具的函数将其转换成最终须要的状态(在此是HTML字符串)。
可是,这和单纯的命令式代码真正有什么不一样?我以为dsDSL方式最终的优雅归结于这样的事实:以这种方式编写的代码更可能是包含表达式,而不是声明。更精确地说,使用dsDSL的代码几乎彻底组成于:
映射到较低层结构字符。
带有这些字符结构的函数调用或者lambda表达式,返回相同类型的结构。
包括大部分表达式和封装众多声明在函数中的代码是很是简洁的,由于全部重复的模式均可以很容易提取。只要该代码返回符合一个很是具体的,非任意形式的字符,你即可以编写任意代码。
另外一个dsDSL(在这里咱们没有时间探索)的特色是使用类型来增长字符结构的丰富性和简洁性的可能性。关于这个问题,我将会在后续的文章进行阐述。
有没可能在Javascript以外建立dsDSL,用一个真正的语言?我认为这的确是可能的,只要语言支持:
对于字符:数组,对象(关联数组),函数调用和lambda表达式。
运行时类型声明。
多态性和动态的返回类型。
我认为,这意味着,dsDSL能实现于任何现代的动态语言(即:Ruby,Python,Perl和PHP),但可能在C或Java中不行。
在这一节中,我将试图演示从其领域展开高层次工具的一种方式。简而言之,该方法包括如下步骤:
一、须要二到四个问题领域表明性的实例。这些问题应该是真实的。从低层到高层展开是一个概括问题,因此你须要真实的数据以便能想出表明性的解决方案。
二、以最直接的方式,并不用工具解决问题。
三、退后一步,好好看看你的解决方案,而发现其中的通用模式。
四、寻找展现(高层)的模式。
五、寻找生成(底层)的模式。
六、用你的高层解决一样的问题,并验证解决方案确实是正确的。
七、若是你以为能够很容易地使用你的展现模式表明全部的问题,而且对于这些每一个实例的生成模式都能产生正确的实现,那么你就大功告成了。不然,回到白板前。
八、若是有新的问题出现,用此工具解决这些问题,并相应修改。
九、该工具应渐近收敛于一个完成状态,无论有多少问题要解决。换句话说,该工具的复杂性应保持不变,而不是随着它解决的问题数量愈来愈大。
如今,到底什么是展现的模式,什么是生成的模式?很高兴你这样问。展现的模式是,你应该可以表达一个涉及你工具的、属于该领域的问题。它是结构字符,让你能编写任何可能但愿在其领域适用性内表达的模式。在一个DSL里,这些能够是产品规则。让咱们回到咱们生成HTML的dsDSL。
不起眼的HTML标签是展现模式一个很好例子。让咱们一探这些基本模式的究竟。
对于HTML展现模式以下:
一个单一标签:['TAG']
一个带属性的单一标签:['TAG', {attribute1: value1, attribute2: value2, ...}]
一个带内容的单一标签:['TAG', 'CONTENTS']
一个带属性和内容的单一标签:['TAG', {attribute1: value1, ...}, 'CONTENTS']
一个带有另外一个标签在内的单一标签:['TAG1', ['TAG2', ...]]
一组标签(独立或者在另外一个标签内):[['TAG1', ...], ['TAG2', ...]]
依赖于一个条件,放置一个标签或者无标签:condition ? ['TAG', ...] : []
/ 依赖于一个条件,放置一个属性或者无属性:['TAG', {class: condition ? 'someClass': undefined}, ...]
这些实例能够用上一节中所肯定的dsDSL符号来表示。而这就是你须要用于表明你可能须要的任何HTML的。更复杂的模式,例如遍历一个对象的条件以产生一个表格,能够经过返回上述展现模式的函数来实现,而且这些模式直接映射到HTML标签。
若是说展现模式是你用于表达你所想要的结构,那么生成模式则是你工具将用于把展现模式转换成较低的级结构的结构。对于HTML,这些有如下几种:
验证输入(实际上这是一个通用生成模式)。
开启和关闭标签(但非空标签,如<input>
,即自闭合的)。
放置属性和内容,转义特殊字符(但不是有style
和script
这样标签的内容)。
无论你信不信,这些就是你须要建立一个生成HTML的展开dsDSL层的模式。能够找到用于生成CSS的相似模式。事实上,这两点,lith经过250行左右的代码都作到了。
最后一个有待回答的问题:我说的从走到滑是什么意思?当咱们处理一个问题领域时,咱们但愿使用一个工具能传递咱们该领域讨厌的细节。换句话说,咱们要把底层扫到地毯下,速度越快越好。从走到滑是的方式提出了彻底相反的:花一些时间在底层。接受其怪癖,而且在面对一系列真实、多样、有用的问题,明白哪一个是必要的,哪一个是能够避免。
在底层走一段时间并解决有用问题后,你将对他们领域有一个足够深入的理解。展现和生成模式天然就会慢慢浮现;它们彻底派生于他们打算解决的问题的本质。而后,你能够编写雇用他们的代码。若是他们工做,你将能滑过你最近不得不走过的问题。滑过意味着不少东西:这意味着速度,精度和缺少摩擦。也许更重要的是,这种品质是能够感觉到;当使用这个工具解决问题时,你是以为你是走过问题,仍是以为你是滑过问题?
也许关于一个展开的工具,最重要的不在于它使咱们免于处理底层这一事实。而是,经过捕捉在底层的重复的经验模式,一个良好的高层次工具使咱们可以充分了解此适用性的领域。
一个展开的工具不只解决了一个问题 -- 它还会启发你问题的结构。
因此,不要从一个值得的问题跑开。首先围着它走,而后滑过它。
相关文章:并发编程简介:初学者指南
------------------------