对于Mixin(混合)、Trait(特性)这两个面向对象特性,总是让人觉得说不清道不明的感觉,其实众多设计语言里,这里面的一些概念也是相互参杂的,并不是又那么一个严格的定义或界限说哪种一定是Mixin,或者哪种一定是Trait。这两种语言设施的提出,它的本质实际上都是解决代码复用的问题。
并发(concurrency) 并发的关注点在于任务切分。举例来说,你是一个创业公司的CEO,开始只有你一个人,你一人分饰多角,一会做产品规划,一会写代码,一会见客户,虽然你不能见客户的同时写代码,但由于你切分了任务,分配了时间片,表现出来好像是多个任务一起在执行,任务交替执行。 并行(parallelism) 并行的关注点在于同时执行。还是上面的例子,你发现你自己太忙了,时间分配不过来,于是请了工程师,产品经理,市场总监,各司一职,这时候多个任务可以同时执行了。 所以总结下,并行只是并发的一种特殊情况,并发并不要求必须并行,可以用时间片切分的方式模拟,比如单核 CPU 上的多任务系统,并发的要求是任务能切分成独立执行的片段。而并行关注的是同时执行,必须是多(核)CPU,要能并行的程序必须是支持并发的。大多数情况下不会严格区分这两个概念,因为并行只是并发的一种特殊情况,并行需要考虑的所有共享冲突,竞争等问题,并发一样需要考虑。
Traits are becoming the best way to create code for reusability and composability. So, any plans in go for that
什么是回调地狱 异步的JavaScript程序,或者说使用了回调函数的JavaScript程序,很难地去直观顺畅地阅读,大量的代码以这种方式结束: fs.readdir(source, function (err, files) { if (err) { console.log(‘Error finding files: ‘ + err) } else { files.forEach(function (filename, fileIndex) { console.log(filename) gm(source + filename).size(function (err, values) { if (err) { console.log(‘Error identifying file size: ‘ + err) } else { console.log(filename + ‘ : ‘ + values) aspect = (values.width / values.height) widths.forEach(function (width, widthIndex) { height = Math.round(width / aspect) console.log(‘resizing ‘ + filename + ‘to ‘ + height + ‘x’ + height) this.resize(width, height).write(dest + ‘w’ + width + ‘_’ + filename, function(err) { if (err) console.log(‘Error writing file: ‘ + err) }) }.bind(this)) } }) }) } }) 有没有看到这些以”})”结尾的金字塔结构?这个形状为“亲切地”称为回调地狱。 二、什么是callback(回调)? “callback”仅仅只是一种使用JavaScript函数的一种通用称呼。在JavaScript语言中,并没有一种特定的东西称之为“callback”,这个只是一种方便的称呼。不同于大部分立即返回结果的函数,这些使用callback的函数需要消耗一些时间才能返回结果。“asynchronous”(异步),或者简称为“async”仅仅表示需要花费一些时间,或者是“在未来发生,而不是现在”。通常情况下,callback仅仅用于操作I/O的时候使用到。比如下载、读写文件、与数据库交互等。 当调用一个普通的函数的时候,你可以这样使用返回值: var result = multiplyTwoNumbers(5, 10) console.log(result) // 50 gets printed out 然而,异步函数,也就是使用了callback函数的不会立刻返回任何东西。 var photo = downloadPhoto(‘http://coolcats.com/cat.gif’) // photo is ‘undefined’! 在这种情况下,下载gif文件会花费相当长的时间,而且你并不希望你的程序在等待下载结束的过程中处于“暂停”(也就是阻塞,block)状态。 相反,你可以把下载结束后需要执行的操作存放到一个函数中,这个就是callback(回调)!你提供了一个“downloadPhoto”的函数,并且这个函数会在下载完成的时候执行callback(call you back later)函数,并且传递photo参数(或者出错的时候返回一个错误信息)。 downloadPhoto(‘http://coolcats.com/cat.gif’, handlePhoto) function handlePhoto (error, photo) { if (error) console.error(‘Download error!’, error) else console.log(‘Download finished’, photo) } console.log(‘Download started’) 人们在尝试理解“callback”这个概念的最大困难之处在于,程序运行的过程中,程序中的代码,是怎么样按照规则执行的。在这个例子中,有三件主要的代码段会发生:首先 “handlePhoto”函数被申明,然后是“downloadPhoto”函数会被调用并且把“handlePhoto”函数作为回调函数“callback”参数传递进入,最后,打印一句话“Download started”。 要注意以下,“handlePhoto”并没有立即被调用,只是创建并且作为一个参数传递给了“downloadPhoto”。但是,一旦“downloadPhoto”函数执行完成之后,”handlePhoto“就会运行。这个取决于网络连接到底有多快。 这个例子试图说明两个重要的概念: (1)回调函数“handlePhoto”仅仅是一种“存放”操作的方式,而这些操作需要延迟一段时间之后进行。 (2)代码的执行规则并不是按照阅读代码的“从上到下”的方式去遵守的,代码执行会根据事情结束的时间跳转嵌套。 三、我们如何解决“回调地狱”? 回调地狱的产生往往来源于对编码练习的缺乏,幸运的是,写出更好的代码并不困难! 我们只需要遵循如下三个原则: (1)保持代码浅显易读 下面是一个杂乱的JavaScript代码,这个代码用于通过使用“ browser-request ”从浏览器想服务端提交一个Ajax请求: var form = document.querySelector(‘form’) form.onsubmit = function (submitEvent) { var name = document.querySelector(‘input’).value request({ uri: “http://example.com/upload”, body: name, method: “POST” }, function (err, response, body) { var statusMessage = document.querySelector(‘.status’) if (err) return statusMessage.value = err statusMessage.value = body }) } 这段代码有两个匿名函数,我们给他们赋上名字吧! var form = document.querySelector(‘form’) form.onsubmit = function formSubmit (submitEvent) { var name = document.querySelector(‘input’).value request({ uri: “http://example.com/upload”, body: name, method: “POST” }, function postResponse (err, response, body) { var statusMessage = document.querySelector(‘.status’) if (err) return statusMessage.value = err statusMessage.value = body }) } 正如你看到的,给函数进行命名是一件超级简单的事情,而且会立刻体验到几个好处: 1)感谢这些具有描述性意义的函数名称把,这些名称使代码更加容易地阅读; 2)当异常发生的时候,你会在异常堆栈中看到确切的函数名称,而不是“anonymous”之类的名字; 3)你可以把函数移动出去,并且通过名字去引用他们; 现在,我们把这两个函数移动到我们程序的最顶层:
document.querySelector(‘form’).onsubmit = formSubmit function formSubmit (submitEvent) { var name = document.querySelector(‘input’).value request({ uri: “http://example.com/upload”, body: name, method: “POST” }, postResponse) } function postResponse (err, response, body) { var statusMessage = document.querySelector(‘.status’) if (err) return statusMessage.value = err statusMessage.value = body } 注意一下,函数声明在这里被移动到了文件最底部,这个要感谢 function hoisting. (2)模块化 这个是最重要的部分:任何人都有能力创建模块。引用 Isaac Schlueter (来源于node.js项目)的话说: Write small modules that each do one thing, and assemble them into other modules that do a bigger thing. You can’t get into callback hell if you don’t go there. “编写一个个小的模块,每个模块完成一件事情,然后把他们组装起来,去完成一个更大的事情,回调地狱这个坑,你不去往那走,你是不会陷进去的”。 让我们从上面的代码中提取模板代码,然后通过拆分到一组文件中的方式,将这些模板代码组装成module。我会展示一个module的格式,这种格式既可以用于浏览器的代码,也可以用在服务端。 这个是一个新的文件,叫做“formuploader.js”,里面包含两个从前面代码中提取的两个函数: module.exports.submit = formSubmit function formSubmit (submitEvent) { var name = document.querySelector(‘input’).value request({ uri: “http://example.com/upload”, body: name, method: “POST” }, postResponse) } function postResponse (err, response, body) { var statusMessage = document.querySelector(‘.status’) if (err) return statusMessage.value = err statusMessage.value = body } 其中“module.exports”部分是一个node.js模块系统的一个例子,electron 和 浏览器 使用browserify 。我非常喜欢这种模块化,因为它可以工作在任何地方,而且非常简单,并且不需要复杂的配置文件或者脚本。 现在我们有了“formuploader.js”(并且已经作为一个脚本,在页面加载完成之后载入到了页面中),我们只需要“require”这个模块并且使用它!这个是一个我们的程序中具体代码的样子: var formUploader = require(‘formuploader’) document.querySelector(‘form’).onsubmit = formUploader.submit 这样,我们的程序仅仅需要两行代码,并且有如下的好处: 1)对于一个新的开发者来说,更加容易理解了 —— 他们不用深陷于“被迫通读全部“formuploader”函数”。 2)“formuploader”可以用于其他地方,不用重复编写代码,并且可以轻松地分享到github或者npm上去。 (3)处理每一个独立的异常 错误(error)有很多种类型:程序员犯的语法错误(往往在第一次尝试运行程序的时候会被发现);程序员犯的运行时错误(程序可以运行但是里面有一个bug会把事情弄糟);其他情况下的平台错误比如无效的文件权限,硬件驱动失效、网络连接异常等问题。这个部分主要针对最后一类错误(error)。 前面两条原则可以让你的代码具有可读性,但是这一条可以让你的代码保持稳定健壮。当你在使用回调函数(callback)的时候,讲道理你其实是在和任务打交道,这些任务都是被分发给回调函数,并且回调函数会在后台执行,然后这个任务要么执行成功,要么由于失败而终止。任何有经验的开发者都会告诉你:你永远不会知道错误是谁么时候会发生,你只有去假定他们会一直出现。 目前在回调函数中处理错误最流行的方式是Node.js风格:所有的回调函数的第一个参数永远是给“error”保留着。 var fs = require(‘fs’)
fs.readFile(‘/Does/not/exist’, handleFile)
function handleFile (error, file) {
if (error) return console.error(‘Uhoh, there was an error’, error)
// otherwise, continue on and use file
in your code
}
在第一个参数中使用“error”是一个简单方便的鼓励记得处理错误的一个方式。如果把这个参数放到第二的位置,你在写代码的时候往往会容易忽略第二个“error”参数,而只关注第一个参数,比如“function handleFile(file){}”。
Code linters(检查代码的小工具)也可以通过配置,实现提醒你要处理这些回调函数错误。最简单的一个小工具就是 standard。这个工具你仅仅只需要在你的代码文件的路径中执行 “$ standard”命令,它就会把你每一个没有进行错误处理的回调函数标记出来。
四、总结
1、不要嵌套函数。给这些函数进行命名,并且放到你的程序的最顶层。
2、使用 function hoisting(函数提升)机制将你的函数移到文件的末尾。
3、在每一个回调函数中去处理每一个错误。可以使用一个代码检查工具去帮你完成这个事情。
4、创建可以服用的函数,并且把他们放置在一个模块中,这样可以提高代码可读性。把代码分割成一个个小的部分,可以帮助你更好的处理error,测试,强迫你去为你的代码创建一个稳定的、文档完善的公共API模块,而且有助于代码的重构。
最重要的避免回调地狱的方面就是,移出你的函数,这样程序的流程可以更容易理解,新手也就不用去啃每一个函数究竟是干什么的。
从现在开始,你首先就可以把函数移到文件的底部,然后逐渐地把函数移到另一个文件中并且使用类似“require(‘./photo-helpers.js’)”的方式去关联,最终,把他们放进一个独立的模块比如“require(‘image-resize’)”.
下面是一些创建模块的一些原则:
1、通过把一些经常重复使用的代码封装成一个函数;
2、当你的函数(或者一组具有类似主题功能的函数)足够大的时候,移动到另一个文件,并且通过“module.exports“的方式去发布,你可以使用类似“require(‘./photo-helpers.js’)”的方式去关联这个文件。
3、如果你的代码可以用于很多个项目的时候,你需要提供“readme”文件、测试以及“package.json”文件,并且把他们发布到github和npm中。
4、一个优秀的模块是很小的而且只聚焦于一个问题;
5、JavaScript的模块中的一个独立的文件行数不应该超过150行;
5、在整个JavaScript的文件结构组织中,一个模块不应该拥有超过一层的嵌套文件夹。如果这种情况发生了,那么意味着整个模块要做的事情有点过多了。
6、让更有经验的程序员给你演示下好的模块构建的方式,直到你了解究竟什么是优秀的模块。如果有一个模块,你需要花不止几分钟的时间去了解它是干嘛的,那么这个模块并不是一个多么好
traits,又被叫做特性萃取技术,说得简单点就是提取“被传进的对象”对应的返回类型,让同一个接口实现对应的功能。因为STL的算法和容器是分离的,两者通过迭代器链接。算法的实现并不知道自己被传进来什么。萃取器相当于在接口和实现之间加一层封装,来隐藏一些细节并协助调用合适的方法,这需要一些技巧(例如,偏特化)。最后附带一个小小的例子,应该能更好地理解 特性萃取。