使用Jasmine测试

最后更新于:2022-04-01 10:52:22

## 问题 你使用CoffeeeScript写了一个简单的计算器,然后你想确认一下它的函数是否如你所想。你决定使用[Jasmine](http://pivotal.github.com/jasmine/)测试框架。 ## 详解 使用Jasmine测试框架,把测试写在一个特殊的文件(spec)中,测试中描述了被测代码所期望的功能点。 例如,我们希望我们的计算机可以做加减法,并且能够正确地处理正负数。我们的测试用例如下。 ~~~ # calculatorSpec.coffee describe 'Calculator', -> it 'can add two positive numbers', -> calculator = new Calculator() result = calculator.add 2, 3 expect(result).toBe 5 it 'can handle negative number addition', -> calculator = new Calculator() result = calculator.add -10, 5 expect(result).toBe -5 it 'can subtract two positive numbers', -> calculator = new Calculator() result = calculator.subtract 10, 6 expect(result).toBe 4 it 'can handle negative number subtraction', -> calculator = new Calculator() result = calculator.subtract 4, -6 expect(result).toBe 10 ~~~ ### 配置Jasmine 想要运行你的测试用例,你必须下载Jasmine,并对其进行配置,具体步骤如下: 1. 下载最新的[Jasmine](http://pivotal.github.com/jasmine/download.html) zip文件; 2. 在你的项目中创建两个文件夹,spec和spec/jasmine; 3. 把下载好的Jasmine文件解压到spec/jasmine文件夹中; 4. 创建一个测试的runner。 ### 创建Test Runner Jasmine能够通过一个spec runner HTML文件在浏览器中运行你的测试用例。spec runner是一个简单的HTML页面,引用了一些必要的JavaScript和CSS文件,包括Jasmine和你的代码。示例如下。 ~~~ 1 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" 2 "http://www.w3.org/TR/html4/loose.dtd"> 3 <html> 4 <head> 5 <title>Jasmine Spec Runner</title> 6 <link rel="shortcut icon" type="image/png" href="spec/jasmine/jasmine_favicon.png"> 7 <link rel="stylesheet" type="text/css" href="spec/jasmine/jasmine.css"> 8 <script src="http://code.jquery.com/jquery.min.js"></script> 9 <script src="spec/jasmine/jasmine.js"></script> 10 <script src="spec/jasmine/jasmine-html.js"></script> 11 <script src="spec/jasmine/jasmine-jquery-1.3.1.js"></script> 12 13 <!-- include source files here... --> 14 <script src="js/calculator.js"></script> 15 16 <!-- include spec files here... --> 17 <script src="spec/calculatorSpec.js"></script> 18 19 </head> 20 21 <body> 22 <script type="text/javascript"> 23 (function() { 24 var jasmineEnv = jasmine.getEnv(); 25 jasmineEnv.updateInterval = 1000; 26 27 var trivialReporter = new jasmine.TrivialReporter(); 28 29 jasmineEnv.addReporter(trivialReporter); 30 31 jasmineEnv.specFilter = function(spec) { 32 return trivialReporter.specFilter(spec); 33 }; 34 35 var currentWindowOnload = window.onload; 36 37 window.onload = function() { 38 if (currentWindowOnload) { 39 currentWindowOnload(); 40 } 41 execJasmine(); 42 }; 43 44 function execJasmine() { 45 jasmineEnv.execute(); 46 } 47 48 })(); 49 </script> 50 </body> 51 </html> ~~~ 可以从[gist](https://gist.github.com/2623232)上下载这个spec runner。 SpecRunner.html使用起来也很简单,引入jasmine.js和相关的依赖,然后接着引入编译好的JavaScript文件和测试用例即可。 在上例中,我们引入了有待开发的calculator.js文件(14行)以及编译好的calculatorSpec.js文件(17行)。 ## 运行测试用例 只需在浏览器中打开SpecRunner.js就能运行我们的测试用例。在我们的例子中,我们会看到4个失败的spec,总共包含8个没有通过的测试用例(如下)。 ![All failing tests](http://island205.com/coffeescript-cookbook.github.com/chapters/testing/images/jasmine_failing_all.jpg) 看起来我们的测试用例没有通过是因为Jasmine无法找到Calculator变量。因为它就没被创建出来过。我们现在来创建,我们创建一个名为js/calculator.coffee的文件。 ~~~ # calculator.coffee window.Calculator = class Calculator ~~~ 编译calculator.coffee,然后刷新浏览器,重新运行测试用例。 ![Still failing, but better](http://island205.com/coffeescript-cookbook.github.com/chapters/testing/images/jasmine_failing_better.jpg) 现在,未通过的测试用例从原来的8个变成了现在的4个。只增加了一行代码,就有50%的提升。 ## 让测试都通过 让我们把方法都实现出来,看看这些测试用例能够通过么? ~~~ # calculator.coffee window.Calculator = class Calculator add: (a, b) -> a + b subtract: (a, b) -> a - b ~~~ 刷新后,我们看到所有的测试用例都通过了。 ![All passing](http://island205.com/coffeescript-cookbook.github.com/chapters/testing/images/jasmine_passing.jpg) ## 重构测试用例 现在我们的测试通过了,我们应该检查一下我们的代码或者测试用例是否可以重构一下。 在我们的spec文件中,每个测试用例都会创建它自己的calculator实例。这会让我们的测试用例过于重复,对于较大的测试用例尤其如此。理想情况下,我们应当考虑把那些初始化的代码移进每个测试运行之前都需例行运行的代码中。 恰好Jasmine有一个名为beforeEach的函数可以实现这种需求。 ~~~ describe 'Calculator', -> calculator = null beforeEach -> calculator = new Calculator() it 'can add two positive numbers', -> result = calculator.add 2, 3 expect(result).toBe 5 it 'can handle negative number addition', -> result = calculator.add -10, 5 expect(result).toBe -5 it 'can subtract two positive numbers', -> result = calculator.subtract 10, 6 expect(result).toBe 4 it 'can handle negative number subtraction', -> result = calculator.subtract 4, -6 expect(result).toBe 10 ~~~ 当我们重新编译我们的spec刷新浏览器之后,测试还是都通过了。 ![All passing](http://island205.com/coffeescript-cookbook.github.com/chapters/testing/images/jasmine_passing.jpg)
';

15、Testing

最后更新于:2022-04-01 10:52:20

';

SQLite

最后更新于:2022-04-01 10:52:17

## 问题 你想在Node.js中访问[SQLite](http://www.sqlite.org/)数据库。 ## 方法 使用[SQLite module](http://code.google.com/p/node-sqlite/)。 ~~~ sqlite = require 'sqlite' db = new sqlite.Database # The module uses asynchronous methods, # so we chain the calls the db.execute exampleCreate = -> db.execute "CREATE TABLE snacks (name TEXT(25), flavor TEXT(25))", (exeErr, rows) -> throw exeErr if exeErr exampleInsert() exampleInsert = -> db.execute "INSERT INTO snacks (name, flavor) VALUES ($name, $flavor)", { $name: "Potato Chips", $flavor: "BBQ" }, (exeErr, rows) -> throw exeErr if exeErr exampleSelect() exampleSelect = -> db.execute "SELECT name, flavor FROM snacks", (exeErr, rows) -> throw exeErr if exeErr console.log rows[0] # => { name: 'Potato Chips', flavor: 'BBQ' } # :memory: creates a DB in RAM # You can supply a filepath (like './example.sqlite') to create/open one on disk db.open ":memory:", (openErr) -> throw openErr if openErr exampleCreate() ~~~ ## 讨论 你也可以事先准备好SQL语句: ~~~ sqlite = require 'sqlite' async = require 'async' # Not required but added to make the example more concise db = new sqlite.Database createSQL = "CREATE TABLE drinks (name TEXT(25), price NUM)" insertSQL = "INSERT INTO drinks (name, price) VALUES (?, ?)" selectSQL = "SELECT name, price FROM drinks WHERE price < ?" create = (onFinish) -> db.execute createSQL, (exeErr) -> throw exeErr if exeErr onFinish() prepareInsert = (name, price, onFinish) -> db.prepare insertSQL, (prepErr, statement) -> statement.bindArray [name, price], (bindErr) -> statement.fetchAll (fetchErr, rows) -> # Called so that it executes the insert onFinish() prepareSelect = (onFinish) -> db.prepare selectSQL, (prepErr, statement) -> statement.bindArray [1.00], (bindErr) -> statement.fetchAll (fetchErr, rows) -> console.log rows[0] # => { name: "Mia's Root Beer", price: 0.75 } onFinish() db.open ":memory:", (openErr) -> async.series([ (onFinish) -> create onFinish, (onFinish) -> prepareInsert "LunaSqueeze", 7.95, onFinish, (onFinish) -> prepareInsert "Viking Sparkling Grog", 4.00, onFinish, (onFinish) -> prepareInsert "Mia's Root Beer", 0.75, onFinish, (onFinish) -> prepareSelect onFinish ]) ~~~ [SQLite版的SQL](http://www.sqlite.org/lang.html)和[node-sqlite模块文档](https://github.com/orlandov/node-sqlite#readme)提供了更完整的信息。
';

MongoDB

最后更新于:2022-04-01 10:52:15

## 问题 你想与MongoDB数据库进行交互。 ## Solution ## 方法 ### 使用Node.js ### 安装 * 如果你的电脑中还没有的话请先[安装MongoDB](http://www.mongodb.org/display/DOCS/Quickstart) 。 * [安装MongoDB原生的模块](https://github.com/christkv/node-mongodb-native)。 #### 保存记录 ~~~ mongo = require 'mongodb' server = new mongo.Server "127.0.0.1", 27017, {} client = new mongo.Db 'test', server # save() updates existing records or inserts new ones as needed exampleSave = (dbErr, collection) -> console.log "Unable to access database: #{dbErr}" if dbErr collection.save { _id: "my_favorite_latte", flavor: "honeysuckle" }, (err, docs) -> console.log "Unable to save record: #{err}" if err client.close() client.open (err, database) -> client.collection 'coffeescript_example', exampleSave ~~~ #### 查找记录 ~~~ mongo = require 'mongodb' server = new mongo.Server "127.0.0.1", 27017, {} client = new mongo.Db 'test', server exampleFind = (dbErr, collection) -> console.log "Unable to access database: #{dbErr}" if dbErr collection.find({ _id: "my_favorite_latte" }).nextObject (err, result) -> if err console.log "Unable to find record: #{err}" else console.log result # => { id: "my_favorite_latte", flavor: "honeysuckle" } client.close() client.open (err, database) -> client.collection 'coffeescript_example', exampleFind ~~~ ### 浏览器端 一个[基于REST的接口](https://github.com/tdegrunt/mongodb-rest)为此而生。它提供了基于AJAX的访问方式。 ## 讨论 本菜谱把_save_和_find_分成了两个示例,目的是为了把MongoDB独有的链接任务和回调管理方面的担忧分离。[async](https://github.com/caolan/async)模块可以帮上忙。
';

14、Databases

最后更新于:2022-04-01 10:52:13

';

工厂方法模式

最后更新于:2022-04-01 10:52:11

## 问题 只到运行时,你才知道你需要什么样的对象。 ## 方法 使用[工厂方法模式](http://en.wikipedia.org/wiki/Factory_method_pattern),动态地选择要生成的对象。 假设你需要加载文件到编辑器中,在用户选择文件之前,你无法知道文件的格式。一个使用[工厂方法模式](http://en.wikipedia.org/wiki/Factory_method_pattern)的类可以根据文件的扩展名来给出不一样的解析器。 ~~~ class HTMLParser constructor: -> @type = "HTML parser" class MarkdownParser constructor: -> @type = "Markdown parser" class JSONParser constructor: -> @type = "JSON parser" class ParserFactory makeParser: (filename) -> matches = filename.match /\.(\w*)$/ extension = matches[1] switch extension when "html" then new HTMLParser when "htm" then new HTMLParser when "markdown" then new MarkdownParser when "md" then new MarkdownParser when "json" then new JSONParser factory = new ParserFactory factory.makeParser("example.html").type # => "HTML parser" factory.makeParser("example.md").type # => "Markdown parser" factory.makeParser("example.json").type # => "JSON parser" ~~~ ## 详解 在上例中,你可以忽略不用文件格式的特点,把注意力放在解析的内容。甚至还有更高级的工厂方法,例如,这些方法可以在文件中查找文件版本数据,返回更加精确的解析器(例如使用HTML5解析器代替HTML4解析器)。
';

桥接模式

最后更新于:2022-04-01 10:52:08

## 问题 你需要维护一个可靠的接口,这个接口能够应对频繁的代码变化,或者能够在不同的实现间切换。 ## 方法 在不同的实现或者是其他的代码中间,使用桥接模式作为媒介。 假设你开发了一个文本编辑器,把内容保存到云端。但是,现在你需要把它移植到一个单独的客户端,把内容保存在本地。 ~~~ class TextSaver constructor: (@filename, @options) -> save: (data) -> class CloudSaver extends TextSaver constructor: (@filename, @options) -> super @filename, @options save: (data) -> # Assuming jQuery # Note the fat arrows $( => $.post "#{@options.url}/#{@filename}", data, => alert "Saved '#{data}' to #{@filename} at #{@options.url}." ) class FileSaver extends TextSaver constructor: (@filename, @options) -> super @filename, @options @fs = require 'fs' save: (data) -> @fs.writeFile @filename, data, (err) => # Note the fat arrow if err? then console.log err else console.log "Saved '#{data}' to #{@filename} in #{@options.directory}." filename = "temp.txt" data = "Example data" saver = if window? new CloudSaver filename, url: 'http://localhost' # => Saved "Example data" to temp.txt at http://localhost else if root? new FileSaver filename, directory: './' # => Saved "Example data" to temp.txt in ./ saver.save data ~~~ ## 详解 桥接模式帮助你把特殊实现的代码从眼前移走,可以让你把注意力放在你程序中更重要的代码中。在上面的例子中,你程序的其他地方可以调用`saver.save data`,无需考虑文件最终会放到哪里。
';

装饰者模式

最后更新于:2022-04-01 10:52:06

## 问题 你有一组数据,你需要对它们进行多次不同可处理。 ## 方法 使用装饰者模式来构建你的处理方式。 ~~~ miniMarkdown = (line) -> if match = line.match /^(#+)\s*(.*)$/ headerLevel = match[1].length headerText = match[2] "<h#{headerLevel}>#{headerText}</h#{headerLevel}>" else if line.length > 0 "<p>#{line}</p>" else '' stripComments = (line) -> line.replace /\s*\/\/.*$/, '' # Removes one-line, double-slash C-style comments TextProcessor = (@processors) -> reducer: (existing, processor) -> if processor processor(existing or '') else existing processLine: (text) -> @processors.reduce @reducer, text processString: (text) -> (@processLine(line) for line in text.split("\n")).join("\n") exampleText = ''' # A level 1 header A regular line // a comment ## A level 2 header A line // with a comment ''' processor = new TextProcessor [stripComments, miniMarkdown] processor.processString exampleText # => "<h1>A level 1 header</h1>\n<p>A regular line</p>\n\n<h2>A level 2 header</h2>\n<p>A line</p>" ~~~ ### 结果 ~~~ <h1>A level 1 header</h1> <p>A regular line</p> <h2>A level 1 header</h2> <p>A line</p> ~~~ ## 详解 TextProcessor正在这里就是一个装饰者的角色,把独立的不同的文本处理方式放到了一起。miniMarkdown和stripComments这两个组件被解放出来,专注于处理单行文本。其他开发者只需要编写这样的函数,返回一个字符串,并且把这个函数添加到处理器组中。 我们甚至可以随时修改现有的装饰者: ~~~ smilies = ':)' : "smile" ':D' : "huge_grin" ':(' : "frown" ';)' : "wink" smilieExpander = (line) -> if line (line = line.replace symbol, "<img src='#{text}.png' alt='#{text}' />") for symbol, text of smilies line processor.processors.unshift smilieExpander processor.processString "# A header that makes you :) // you may even laugh" # => "<h1>A header that makes you <img src='smile.png' alt='smile' /></h1>" processor.processors.shift() # => "<h1>A header that makes you :)</h1>" ~~~
';

解释器模式 Interpreter Pattern

最后更新于:2022-04-01 10:52:04

## 问题 Problem 有人需要在限制重重的地方运行部分你的代码,或者你的语言无法简洁地表达其他领域的问题。 Someone else needs to run parts of your code in a controlled fashion. Alternately, your language of choice cannot express the problem domain in a concise fashion. ## 方案 Solution 使用解释器模式创建一个领域语言,翻译成特殊的代码。 Use the Interpreter pattern to create a domain-specific language that you translate into specific code. 例如,有用户想在你的程序中执行数学计算。你可以让他们把代码交给_eval_运行,但是他们有可能会运行各种各样的代码。更好的方案,你可以提供一个小型的“栈计算器”语言,单独解析,这样可以只允许运行数学操作,同时还可以反馈更有用的错误信息。 Assume, for example, that the user wants to perform math inside of your application. You could let them forward code to _eval_ but that would let them run arbitrary code. Instead, you can provide a miniature “stack calculator” language that you parse separately in order to only run mathematical operations while reporting more useful error messages. ~~~ class StackCalculator parseString: (string) -> @stack = [ ] for token in string.split /\s+/ @parseToken token if @stack.length > 1 throw "Not enough operators: numbers left over" else @stack[0] parseToken: (token, lastNumber) -> if isNaN parseFloat(token) # Assume that anything other than a number is an operator @parseOperator token else @stack.push parseFloat(token) parseOperator: (operator) -> if @stack.length < 2 throw "Can't operate on a stack without at least 2 items" right = @stack.pop() left = @stack.pop() result = switch operator when "+" then left + right when "-" then left - right when "*" then left * right when "/" if right is 0 throw "Can't divide by 0" else left / right else throw "Unrecognized operator: #{operator}" @stack.push result calc = new StackCalculator calc.parseString "5 5 +" # => { result: 10 } calc.parseString "4.0 5.5 +" # => { result: 9.5 } calc.parseString "5 5 + 5 5 + *" # => { result: 100 } try calc.parseString "5 0 /" catch error error # => "Can't divide by 0" try calc.parseString "5 -" catch error error # => "Can't operate on a stack without at least 2 items" try calc.parseString "5 5 5 -" catch error error # => "Not enough operators: numbers left over" try calc.parseString "5 5 5 foo" catch error error # => "Unrecognized operator: foo" ~~~ ## 讨论 Discussion 如果不自己写解释器, 你可以结合目前有的CoffeeScrtipt解释器,以某种方式,这种方式使用它平常的语法创建算法的更加自然的(因此更易于理解)表达方式。 As an alternative to writing our own interpreter, you can co-op the existing CoffeeScript interpreter in a such a way that its normal syntax makes for more natural (and therefore more comprehensible) expressions of your algorithm. ~~~ class Sandwich constructor: (@customer, @bread='white', @toppings=[], @toasted=false)-> white = (sw) -> sw.bread = 'white' sw wheat = (sw) -> sw.bread = 'wheat' sw turkey = (sw) -> sw.toppings.push 'turkey' sw ham = (sw) -> sw.toppings.push 'ham' sw swiss = (sw) -> sw.toppings.push 'swiss' sw mayo = (sw) -> sw.toppings.push 'mayo' sw toasted = (sw) -> sw.toasted = true sw sandwich = (customer) -> new Sandwich customer to = (customer) -> customer send = (sw) -> toastedState = sw.toasted and 'a toasted' or 'an untoasted' toppingState = '' if sw.toppings.length > 0 if sw.toppings.length > 1 toppingState = " with #{sw.toppings[0..sw.toppings.length-2].join ', '} and #{sw.toppings[sw.toppings.length-1]}" else toppingState = " with #{sw.toppings[0]}" "#{sw.customer} requested #{toastedState}, #{sw.bread} bread sandwich#{toppingState}" send sandwich to 'Charlie' # => "Charlie requested an untoasted, white bread sandwich" send turkey sandwich to 'Judy' # => "Judy requested an untoasted, white bread sandwich with turkey" send toasted ham turkey sandwich to 'Rachel' # => "Rachel requested a toasted, white bread sandwich with turkey and ham" send toasted turkey ham swiss sandwich to 'Matt' # => "Matt requested a toasted, white bread sandwich with swiss, ham and turkey" ~~~ 上面这个例子,允许连续调用多个函数,因为每个函数都会返回修改后的对象,这样外围的函数就可以轮流地修改该对象。借用一个一个非常小的_to_,该例子给于构造过程了一个更加自然的语法,并且如果使用正确的话,读起来就像爱那个一个自然的句子。这样的话,你的CoffeeScript技术和你在使用的语言技术都能帮助你找到代码的问题。 This example allows for layers of functions by how it returns the modified object so that outer functions can modify it in turn. By borrowing a very and the particle _to_, the example lends natural grammar to the construction and ends up reading like an actual sentence when used correctly. This way, both your CoffeeScript skills and your existing language skills can help catch code problems.
';

备忘录模式 Memento Pattern

最后更新于:2022-04-01 10:52:02

## 问题 Problem 你想记录一个对象的变化。 You want to anticipate the reversion of changes to an object. ## 方法 Solution 使用[备忘录模式](http://en.wikipedia.org/wiki/Memento_pattern)来跟踪一个对象的变化。使用这个模式的类会返回一个`memento`对象,可以保存到其他地方。 Use the [Memento pattern](http://en.wikipedia.org/wiki/Memento_pattern) to track changes to an object. The class using the pattern will export a `memento` object stored elsewhere. 例如,你有个程序,用户可以编辑文本文件,他们应该有需要取消他们最后一次编辑。你可以在用户改变文件之前保存当前的状态,之后可以进行恢复。 If you have application where the user can edit a text file, for example, they may want to undo their last action. You can save the current state of the file before the user changes it and then roll back to that at a later point. ~~~ class PreserveableText class Memento constructor: (@text) -> constructor: (@text) -> save: (newText) -> memento = new Memento @text @text = newText memento restore: (memento) -> @text = memento.text pt = new PreserveableText "The original string" pt.text # => "The original string" memento = pt.save "A new string" pt.text # => "A new string" pt.save "Yet another string" pt.text # => "Yet another string" pt.restore memento pt.text # => "The original string" ~~~ ## 讨论 Discussion 由`PreserveableText#save`返回的备忘录对象单独保管着重要的状态信息。 你甚至可以把这个备忘录对象序列化,便于在硬盘上维护一个“撤销”缓冲区,或者remotely for such data-intensive objects as edited images。 The Memento object returned by `PreserveableText#save` stores the important state information separately for safe-keeping. You could even serialize this Memento in order to maintain an “undo” buffer on the hard disk or remotely for such data-intensive objects as edited images.
';

建造者模式 Builder Pattern

最后更新于:2022-04-01 10:51:59

## 问题 Problem 你需要准备一个复杂的、由多个部分组成的对象,但是你希望生成多次,或者使用不同的配置生成。 You need to prepare a complicated, multi-part object, but you expect to do it more than once or with varying configurations. ## 方法 Solution 创建一个“建造者”来分装对象的创建过程。 Create a Builder to encapsulate the object production process. [Todo.txt](http://todotxt.com/)格式提供了一种高级的(仍然是纯文本)方法来管理todo列表。手动打出每一项很累也很容易出错,因而一个TodoTxtBuilder类可以避免这些麻烦: The [Todo.txt](http://todotxt.com/) format provides an advanced but still plain-text method for maintaining lists of to-do items. Typing out each item by hand would provide exhausting and error-prone, however, so a TodoTxtBuilder class could save us the trouble: ~~~ class TodoTxtBuilder constructor: (defaultParameters={ }) -> @date = new Date(defaultParameters.date) or new Date @contexts = defaultParameters.contexts or [ ] @projects = defaultParameters.projects or [ ] @priority = defaultParameters.priority or undefined newTodo: (description, parameters={ }) -> date = (parameters.date and new Date(parameters.date)) or @date contexts = @contexts.concat(parameters.contexts or [ ]) projects = @projects.concat(parameters.projects or [ ]) priorityLevel = parameters.priority or @priority createdAt = [date.getFullYear(), date.getMonth()+1, date.getDate()].join("-") contextNames = ("@#{context}" for context in contexts when context).join(" ") projectNames = ("+#{project}" for project in projects when project).join(" ") priority = if priorityLevel then "(#{priorityLevel})" else "" todoParts = [priority, createdAt, description, contextNames, projectNames] (part for part in todoParts when part.length > 0).join " " builder = new TodoTxtBuilder(date: "10/13/2011") builder.newTodo "Wash laundry" # => '2011-10-13 Wash laundry' workBuilder = new TodoTxtBuilder(date: "10/13/2011", contexts: ["work"]) workBuilder.newTodo "Show the new design pattern to Lucy", contexts: ["desk", "xpSession"] # => '2011-10-13 Show the new design pattern to Lucy @work @desk @xpSession' workBuilder.newTodo "Remind Sean about the failing unit tests", contexts: ["meeting"], projects: ["compilerRefactor"], priority: 'A' # => '(A) 2011-10-13 Remind Sean about the failing unit tests @work @meeting +compilerRefactor' ~~~ ## 讨论 Discussion TodoTxtBuilder包揽了所有繁重的生成文本的工作,使得程序员把注意力放在每个todo项不一样的部分。而且,可以把命令行工具或者GUI插入到这段代码中,and still retain support for later, more advanced versions of the format with ease. The TodoTxtBuilder class takes care of all the heavy lifting of text generation and lets the programmer focus on the unique elements of each to-do item. Additionally, a command line tool or GUI could plug into this code and still retain support for later, more advanced versions of the format with ease. ### 开工前 Pre-Construction 无需每次都重新创建一个需要对象的实例,我们可以组建转移到别的对象上,这样我们可以在随后的创建过程中擦改。 Instead of creating a new instance of the needed object from scratch every time, we shift the burden to a separate object that we can then tweak during the object creation process. ~~~ builder = new TodoTxtBuilder(date: "10/13/2011") builder.newTodo "Order new netbook" # => '2011-10-13 Order new netbook' builder.projects.push "summerVacation" builder.newTodo "Buy suntan lotion" # => '2011-10-13 Buy suntan lotion +summerVacation' builder.contexts.push "phone" builder.newTodo "Order tickets" # => '2011-10-13 Order tickets @phone +summerVacation' delete builder.contexts[0] builder.newTodo "Fill gas tank" # => '2011-10-13 Fill gas tank +summerVacation' ~~~ ### 练习 Exercises * 补充project-和context-tag产生器的代码,过滤掉重复的项; * 有些Todo.txt的用户常常会把project和context标记插入到todo项的描述中,添加一些代码吧这些标识出这些记号,并把它们从后面的标记剔除掉。 * Expand the project- and context-tag generation code to filter out duplicate entries. * Some Todo.txt users like to insert project and context tags inside the description of their to-do items. Add code to identify these tags and filter them out of the end tags.
';

策略模式 Strategy Pattern

最后更新于:2022-04-01 10:51:57

## 问题 Problem 你很多方法用来解决某个问题,但是需要在运行时选择或者切换这些方法。 You have more than one way to solve a problem but you need to choose (or even switch) between these methods at run time. ## 方法 Solution 将你的算法分装到策略对象中。 Encapsulate your algorithms inside of Strategy objects. 例如,给定一个无序列表,我们可以在不同的情景中选择不同的算法。 Given an unsorted list, for example, we can change the sorting algorithm under different circumstances. ### 基类 The base class: ~~~ class StringSorter constructor:(@algorithm)-> sort: (list) -> @algorithm list ~~~ ### 策略 The strategies: ~~~ bubbleSort = (list) -> anySwaps = false swapPass = -> for r in [0..list.length-2] if list[r] > list[r+1] anySwaps = true [list[r], list[r+1]] = [list[r+1], list[r]] swapPass() while anySwaps anySwaps = false swapPass() list reverseBubbleSort = (list) -> anySwaps = false swapPass = -> for r in [list.length-1..1] if list[r] < list[r-1] anySwaps = true [list[r], list[r-1]] = [list[r-1], list[r]] swapPass() while anySwaps anySwaps = false swapPass() list ~~~ ### 使用策略 Using the strategies: ~~~ sorter = new StringSorter bubbleSort unsortedList = ['e', 'b', 'd', 'c', 'x', 'a'] sorter.sort unsortedList # => ['a', 'b', 'c', 'd', 'e', 'x'] unsortedList.push 'w' # => ['a', 'b', 'c', 'd', 'e', 'x', 'w'] sorter.algorithm = reverseBubbleSort sorter.sort unsortedList # => ['a', 'b', 'c', 'd', 'e', 'w', 'x'] ~~~ ## 讨论 Discussion “遇到敌人后一切战斗计划都失效了”,用户也同样,但是我们可以使用已掌握的知识来适应环境。例如,在上面的例子中,新加项打乱了兴趣。了解细节,针对确切的场景,我们可以通过切换算法来加快排序,仅仅只需一次简单的再赋值即可。 “No plan survives first contact with the enemy”, nor users, but we can use the knowledge gained from changing circumstances to adapt. Near the end of the example, for instance, the newest item in the array now lies out of order. Knowing that detail, we can then speed the sort up by switching to an algorithm optimized for that exact scenario with nothing but a simple reassignment. ### 练习 Exercises * 把`StringSorter`扩展成一个`AlwaysSortedArray`类,实现原生数组的所有功能,而且有通过插入方法新项时能够自动地排序(例如,`push`和`shift`)。 * Expand `StringSorter` into an `AlwaysSortedArray` class that implements all of the functionality of a regular array but which automatically sorts new items based on the method of insertion (e.g. `push` vs. `shift`).
';

单例模式

最后更新于:2022-04-01 10:51:55

## 问题 很多时候你需要一个仅且一个类的实例。例如,你也许只需要一个类实例,这个类创建服务端资源,并且你想保证只有一个对象可以控制这些资源。然而,要注意单例模式很容易引入不必要的全局变量。 ## 方法 公共可用的类只包含一个方法,使用该方法获取那个唯一的实例。这个实例被保存在这个公共对象的毕包中,并且每次返回的都是这个对象。 单例类的真实定义跟在后面。 注意,我使用了惯用的模块暴露的特性,来强调这个模块公共可访问的部分。别忘了CoffeeScript会把所有的文件内容包含在一个函数块中,以此保护全局作用域不被污染。 ~~~ root = exports ? this # http://stackoverflow.com/questions/4214731/coffeescript-global-variables # The publicly accessible Singleton fetcher class root.Singleton _instance = undefined # Must be declared here to force the closure on the class @get: (args) -> # Must be a static method _instance ?= new _Singleton args # The actual Singleton class class _Singleton constructor: (@args) -> echo: -> @args a = root.Singleton.get 'Hello A' a.echo() # => 'Hello A' b = root.Singleton.get 'Hello B' a.echo() # => 'Hello A' b.echo() # => 'Hello A' root.Singleton._instance # => undefined root.Singleton._instance = 'foo' root.Singleton._instance # => 'foo' c = root.Singleton.get 'Hello C' c.foo() # => 'Hello A' a.foo() # => 'Hello A' ~~~ ## 详解 在上例中,可以看出,所有的实例如此从同一个单例类的实例中输出的。 看看CoffeeScript是如何让这个设计模式变得如此简单的,对应JavaScript实现的参考和讨论,请参看[Essential JavaScript Design Patterns For Beginners](http://addyosmani.com/resources/essentialjsdesignpatterns/book/).
';

命令模式

最后更新于:2022-04-01 10:51:53

## 问题 当你自己的代码运行时,你需要使用其他另外的对象来处理。 ## 方法 使用[命令模式](http://en.wikipedia.org/wiki/Command_pattern)把引用传递给你的函数。 ~~~ # Using a private variable to simulate external scripts or modules incrementers = (() -> privateVar = 0 singleIncrementer = () -> privateVar += 1 doubleIncrementer = () -> privateVar += 2 commands = single: singleIncrementer double: doubleIncrementer value: -> privateVar )() class RunsAll constructor: (@commands...) -> run: -> command() for command in @commands runner = new RunsAll(incrementers.single, incrementers.double, incrementers.single, incrementers.double) runner.run() incrementers.value() # => 6 ~~~ ## 详解 承袭自JavaScript,由于函数是第一类对象,且函数与变量作用域绑定的关系,CoffeeScript让这个模式几乎不可见。事实上,所有把函数作为一个回调传递时都可以看作是一个_命令_。 jQuery AJAX方法返回的 `jqXHR`对象使用的就是这种模式。 ~~~ jqxhr = $.ajax url: "/" logMessages = "" jqxhr.success -> logMessages += "Success!\n" jqxhr.error -> logMessages += "Error!\n" jqxhr.complete -> logMessages += "Completed!\n" # On a valid AJAX request: # logMessages == "Success!\nCompleted!\n" ~~~
';

13、Design Patterns

最后更新于:2022-04-01 10:51:50

';

双向服务端 Bi-Directional Server

最后更新于:2022-04-01 10:51:48

## 问题 Problem 你希望网络中能有一个持久的服务,能够与它的客户端保持一个持续的链接。 You want to provide a persistent service over a network, one which maintains an on-going connection with a client. ## 方法 Solution 创建一个双向通信的TCP服务器。 Create a bi-directional TCP server. ### 用Node.js In Node.js ~~~ net = require 'net' domain = 'localhost' port = 9001 server = net.createServer (socket) -> console.log "New connection from #{socket.remoteAddress}" socket.on 'data', (data) -> console.log "#{socket.remoteAddress} sent: #{data}" others = server.connections - 1 socket.write "You have #{others} #{others == 1 and "peer" or "peers"} on this server" console.log "Listening to #{domain}:#{port}" server.listen port, domain ~~~ ### 示例 Example Usage 使用[双向通信的客户端](http://island205.com/chapters/networking/bi-directional-client)。 Accessed by the [Bi-Directional Client](http://island205.com/chapters/networking/bi-directional-client): ~~~ $ coffee bi-directional-server.coffee Listening to localhost:9001 New connection from 127.0.0.1 127.0.0.1 sent: Ping 127.0.0.1 sent: Ping 127.0.0.1 sent: Ping [...] ~~~ ## 讨论 Discussion 大部分工作都在@socket.on ‘data’@这个处理器中,所有从客户端传过来的数据都在这里处理。一个真实的服务器会把数据传递给另外一个函数,对其进行处理,并产生响应供原始的处理器使用。 The bulk of the work lies in the @socket.on ‘data’@ handler, which processes all of the input from the client. A real server would likely pass the data onto another function to process it and generate any responses so that the original handler. 参看[双向的客户端](http://island205.com/chapters/networking/bi-directional-client), [最简单的客户端](http://island205.com/chapters/networking/basic-client), 以及[最简单的服务器](http://island205.com/chapters/networking/basic-server)这几个菜谱。 See also the [Bi-Directional Client](http://island205.com/chapters/networking/bi-directional-client), [Basic Client](http://island205.com/chapters/networking/basic-client), and [Basic Server](http://island205.com/chapters/networking/basic-server)recipes. ### 练习 Exercises * 添加自定义domain和端口的支持,可基于命令行参数,也可以使用配置文件。 * Add support for choosing the target domain and port based on command-line arguments or on a configuration file.
';

简单的客户端

最后更新于:2022-04-01 10:51:46

## 问题 你想访问一个网络客户端。 ## 方法 创建一个简单的TCP客户端。 ### 使用Node.js ~~~ net = require 'net' domain = 'localhost' port = 9001 connection = net.createConnection port, domain connection.on 'connect', () -> console.log "Opened connection to #{domain}:#{port}." connection.on 'data', (data) -> console.log "Received: #{data}" connection.end() ~~~ ### 用法示例 访问这个[简单的客户端](http://island205.com/chapters/networking/basic-server): ~~~ $ coffee basic-client.coffee Opened connection to localhost:9001 Received: Hello, World! ~~~ ## 详解 _connection.on ‘data’_处理器中包含了最关键的地方,客户端从服务端接受数据,也许还需要安排返回的数据。 参看[简单的服务器]、[Bi-Directional Client](http://island205.com/chapters/networking/bi-directional-client)和[Bi-Directional Server](http://island205.com/chapters/networking/bi-directional-server)。 ### 练习 * 支持domian和端口的自定义,从命令行接受参数,或者从一个配置文件。
';

最简单的HTTP服务器

最后更新于:2022-04-01 10:51:43

## 问题 你想到网络中搭建一个HTTP服务器。在本菜谱中,我们从最简单的服务器到一个功能完好的键值对存储服务器,一步步地学习创建HTTP服务器。 ## 方法 处于自私的目的,我们将使用[node.js](http://nodejs.org/)的HTTP类库,使用CoffeeScript创建最简单的服务器。 ### 问候 ‘hi\n’ 我们可以从引入node.js的HTTP模块开始。该模块包含了`createServer`,这是一个简单的请求处理器,返回一个HTTP服务器。我们让这个服务器监听在一个TCP端口上。 ~~~ http = require 'http' server = http.createServer (req, res) -> res.end 'hi\n' server.listen 8000 ~~~ 把这些代码放在一个文件中运行,就可以执行这个示例。你可以使用`Ctrl-C`来关掉它。我们可以使用`curl`命令测试,在绝大多数的*nix平台上都可以运行: ~~~ $ curl -D - http://localhost:8000/ HTTP/1.1 200 OK Connection: keep-alive Transfer-Encoding: chunked hi ~~~ ### 接下来呢? 让我们弄点反馈,看看我们服务器上发生了什么。同时,我们还对我们用户更友好一点,并为他们提供一些HTTP头。 ~~~ http = require 'http' server = http.createServer (req, res) -> console.log req.method, req.url data = 'hi\n' res.writeHead 200, 'Content-Type': 'text/plain' 'Content-Length': data.length res.end data server.listen 8000 ~~~ 试着再访问一次这个服务器,但是这次要使用其他的URL地址。比如`http://localhost:8000/coffee`。你可以在服务器上看到如下的调试信息: ~~~ $ coffee http-server.coffee GET / GET /coffee GET /user/1337 ~~~ ### GET点啥 服务器上放点数据?我们就放一个简单的键值存储吧,键值元素可以通过GET请求获取。把key放到请求路径中,服务器就会返回相应的value &mdash,如果不错在的话就返回404。 ~~~ http = require 'http' store = # we'll use a simple object as our store foo: 'bar' coffee: 'script' server = http.createServer (req, res) -> console.log req.method, req.url value = store[req.url[1..]] if not value res.writeHead 404 else res.writeHead 200, 'Content-Type': 'text/plain' 'Content-Length': value.length + 1 res.write value + '\n' res.end() server.listen 8000 ~~~ 我们可以找几个URLs尝试一下,看看他是如何返回的: ~~~ $ curl -D - http://localhost:8000/coffee HTTP/1.1 200 OK Content-Type: text/plain Content-Length: 7 Connection: keep-alive script $ curl -D - http://localhost:8000/oops HTTP/1.1 404 Not Found Connection: keep-alive Transfer-Encoding: chunked ~~~ ### 加上headers 承认吧,`text/plain`挺无聊的。我们要不使用`application/json`或者`text/xml`等试试看?并且,我们的读取存储的过程是不是应该重构一下&mdash,添加点异常处理?让我们看看能产生什么效果: ~~~ http = require 'http' # known mime types [any, json, xml] = ['*/*', 'application/json', 'text/xml'] # gets a value from the db in format [value, contentType] get = (store, key, format) -> value = store[key] throw 'Unknown key' if not value switch format when any, json then [JSON.stringify({ key: key, value: value }), json] when xml then ["<key>#{ key }</key>\n<value>#{ value }</value>", xml] else throw 'Unknown format' store = foo: 'bar' coffee: 'script' server = http.createServer (req, res) -> console.log req.method, req.url try key = req.url[1..] [value, contentType] = get store, key, req.headers.accept code = 200 catch error contentType = 'text/plain' value = error code = 404 res.writeHead code, 'Content-Type': contentType 'Content-Length': value.length + 1 res.write value + '\n' res.end() server.listen 8000 ~~~ 服务器返回的还是key能够匹配到的值,无匹配的话就返回404。但是它根据`Accept`头,把返回值格式化成了JSON或者XML。自己试试看: ~~~ $ curl http://localhost:8000/ Unknown key $ curl http://localhost:8000/coffee {"key":"coffee","value":"script"} $ curl -H "Accept: text/xml" http://localhost:8000/coffee <key>coffee</key> <value>script</value> $ curl -H "Accept: image/png" http://localhost:8000/coffee Unknown format ~~~ ### 你必须让他们有恢复的能力 在我们冒险旅行的上一步,给我们的客户端提供了存储数据的能力。我们会保证我们是RESTfull的,提供对POST请求的监听。 ~~~ http = require 'http' # known mime types [any, json, xml] = ['*/*', 'application/json', 'text/xml'] # gets a value from the db in format [value, contentType] get = (store, key, format) -> value = store[key] throw 'Unknown key' if not value switch format when any, json then [JSON.stringify({ key: key, value: value }), json] when xml then ["<key>#{ key }</key>\n<value>#{ value }</value>", xml] else throw 'Unknown format' # puts a value in the db put = (store, key, value) -> throw 'Invalid key' if not key or key is '' store[key] = value store = foo: 'bar' coffee: 'script' # helper function that responds to the client respond = (res, code, contentType, data) -> res.writeHead code, 'Content-Type': contentType 'Content-Length': data.length res.write data res.end() server = http.createServer (req, res) -> console.log req.method, req.url key = req.url[1..] contentType = 'text/plain' code = 404 switch req.method when 'GET' try [value, contentType] = get store, key, req.headers.accept code = 200 catch error value = error respond res, code, contentType, value + '\n' when 'POST' value = '' req.on 'data', (chunk) -> value += chunk req.on 'end', () -> try put store, key, value value = '' code = 200 catch error value = error + '\n' respond res, code, contentType, value server.listen 8000 ~~~ 请注意是如何接受POST请求中的数据的。听过给请求对象的`'data'`和`'end'`事件绑定处理器来实现。我们可以把来来自客户端的数据暂存或者最终存储到`store`中。 ~~~ $ curl -D - http://localhost:8000/cookie HTTP/1.1 404 Not Found # ... Unknown key $ curl -D - -d "monster" http://localhost:8000/cookie HTTP/1.1 200 OK # ... $ curl -D - http://localhost:8000/cookie HTTP/1.1 200 OK # ... {"key":"cookie","value":"monster"} ~~~ ## Discussion ## 讨论 给`http.createServer`传递一个型如`(request, respone) ->...`这样的函数,它就会返回一个server对象,我们可以使用这个server对象监听某个端口。与`request`和`response`对象交互,实现server的行为。使用`server.listen 8000`来监听8000端口。 关于这个主题的API或者更为详细的信息,请参考[http](http://nodejs.org/docs/latest/api/http.html)以及[https](http://nodejs.org/docs/latest/api/https.html)这两页文档。而且[HTTP spec](http://www.ietf.org/rfc/rfc2616.txt)迟早也会用到。 ### 练习 * 在服务器和开发者之间建立一个layer(layer),允许开发者可以像下面这样写: ~~~ server = layer.createServer 'GET /': (req, res) -> ... 'GET /page': (req, res) -> ... 'PUT /image': (req, res) -> ... ~~~
';

最简单的HTTP客户端

最后更新于:2022-04-01 10:51:41

## 问题 你想创建一个HTTP客户端 ## 方法 在本菜谱中,我们将使用[node.js](http://nodejs.org/)的HTTP库。我们先从一个简单的GET请求示例开始,然后实现可以返回电脑真实IP的客户端。 ## GET些啥 ~~~ http = require 'http' http.get { host: 'www.google.com' }, (res) -> console.log res.statusCode ~~~ `get`函数是node.js的`http`模块提供,可以向HTTP服务器发送一个GET请求。响应会以回调的形式返回,我们可以在一个函数中处理它。本例只是简单地把响应的状态码打印出来。请看: ~~~ $ coffee http-client.coffee 200 ~~~ ### 我的IP地址是多杀? 如果你处在一个像LAN这样的网络中,依赖于[NAT](http://en.wikipedia.org/wiki/Network_address_translation),你有时候可能会碰到这样的问题,我真实的IP地址是多少呢?让我编写一小段coffeescript来搞定它: ~~~ http = require 'http' http.get { host: 'checkip.dyndns.org' }, (res) -> data = '' res.on 'data', (chunk) -> data += chunk.toString() res.on 'end', () -> console.log data.match(/([0-9]+\.){3}[0-9]+/)[0] ~~~ 我们可以监听`'data'`事件,从返回的对象中获取数据;并且当`'end'`事件触发时,我们可以知道数据传送完了。当传送结束时,我们可以使用一个简单的正则表达式来匹配出我们的IP地址,试试看: ~~~ $ coffee http-client.coffee 123.123.123.123 ~~~ ## 详解 要知道`http.get`是`http.request`的快捷方式。后者允许你使用不同的方法发送HTTP请求,比如说POST或者PUT。 关于这个主题的API或者更为详细的信息,请参考[http](http://nodejs.org/docs/latest/api/http.html)以及[https](http://nodejs.org/docs/latest/api/https.html)这两页文档。而且[HTTP spec](http://www.ietf.org/rfc/rfc2616.txt)迟早也会用到。 ### 练习 * 基于[Basic HTTP Server](http://island205.com/coffeescript-cookbook.github.com/chapters/networking/basic-http-server),创建一个针对键值对存储的HTTP服务器的客户端。
';

双向客户端

最后更新于:2022-04-01 10:51:39

## 问题 你希望网络中能有一个持久的服务,能够与它的客户端保持一个持续的链接。 ## 方法 创建一个双向的TCP客户端。 ### 使用Node.js来实现 ~~~ net = require 'net' domain = 'localhost' port = 9001 ping = (socket, delay) -> console.log "Pinging server" socket.write "Ping" nextPing = -> ping(socket, delay) setTimeout nextPing, delay connection = net.createConnection port, domain connection.on 'connect', () -> console.log "Opened connection to #{domain}:#{port}" ping connection, 2000 connection.on 'data', (data) -> console.log "Received: #{data}" connection.on 'end', (data) -> console.log "Connection closed" process.exit() ~~~ ### 示例 访问[双向服务器](http://island205.com/chapters/networking/bi-directional-server): ~~~ $ coffee bi-directional-client.coffee Opened connection to localhost:9001 Pinging server Received: You have 0 peers on this server Pinging server Received: You have 0 peers on this server Pinging server Received: You have 1 peer on this server [...] Connection closed ~~~ ## 讨论 这个特例开始与服务器交互,并在@connection.on ‘connect’@处理器中与服务器交流。然而,在一个真实的客户端中,绝大部分的工作都依赖与@connection.on ‘data’@处理器,它能够处理服务端的输出。重复的@ping@函数,仅仅只是为了表示与服务端的交互是持续性的,实际上在真实的客户端中可以把它移除。 参看[双向的服务器](http://island205.com/chapters/networking/bi-directional-server), [最基本的客户端](http://island205.com/chapters/networking/basic-client)这两个菜谱。 ### 练习 * 添加自定义domain和端口的支持,可基于命令行参数,也可以使用配置文件。
';