Goliath - Non Blocking Ruby Web Server Framework

Ruby世界有很多成熟的Web Server供开发者选择,这些App Server在功能上各具特色,都有各自适合的应用场景。但如果从并发方式的角度来分类的话,大体可以分为两类:一类是基于进程和线程的并发模式,典型的Web Server包括Unicorn, RainbowsPuma等;另一类是基于Non-Blocking Event Loop的并发方式,代表Web Server有ThinGoliath

进程和线程并发

这种类型的Ruby Web Server主要通过操作系统级别的并发方式实现Web请求的并行处理,对于进程并发的Web Server,每个请求由一个VM进程独立处理,如果Web Server支持在每个进程中动态创建多个线程来处理Web请求,就构成了线程并发类型的Web Server。对于进程线程类Web Server的并发能力的估算可以简化为:Number of Processes * Number of Threads

下面列出了此类Web Server的中比较流行的几个:

  • 单进程:WeBrick
  • 多进程单线程:Unicorn
  • 多进程多线程:Rainbows,Puma

这类Web Server在并发较低,I/O操作较少的场景下是可以很好的满足要求的,但在I/O操作耗时较长的场景下往往难以支持较高的并发量。

EventMachine

要提到Non-Blocking并发就不得不提到EventMachine,前面提到的Thin和Goliath都是基于它开发的。

简单来说,EventMachine是一个事件驱动的轻量级并发库,其事件驱动的本质上就是Reactor Pattern,其他语言中类似的实现包括JBoss Netty,Python Twisted以及Node.js。

下面是一个EventMachine异步HTTP请求的栗子

1
2
3
4
5
6
7
8
require 'eventmachine'
require 'em-http'

EM.run {
EM::HttpRequest.new('http://www.sitepoint.com/').get.callback {|http|
puts http.response
}
}

EM::run的执行会初始化一个EventMachine Reactor,这个操作会持续阻塞当前线程,除非有EM::stop之类的命令被执行。在传入的Block中,可以使用基于EM的异步I/O库来完成业务逻辑。

这个例子中我们发起了一个Http请求并打印Response,与传统Httparty#get的方式不同,EM::HttpRequest#get不会阻塞线程,系统资源可以继续被用于处理其他任务,当请求返回时callback方法传入的Block会被Event Loop回调,从而继续处理当前的业务逻辑。

Goliath

Goliath就是基于上述的EventMachine实现的Non-Blocking Ruby Web Server Framework,Goliath官方提供的性能测试结果表示其性能与Node.js相当,可以达到3000 req/s的并发处理能力。

下面是一个使用Goliath实现的API服务

1
2
3
4
5
6
7
8
9
10
require 'goliath'

class Hello < Goliath::API
# default to JSON output, allow Yaml as secondary
use Goliath::Rack::Render, ['json', 'yaml']

def response(env)
[200, {}, "Hello World"]
end
end

Goliath除了直接作为API开发框架之外,还可以作为中间件与其他API框架(如Grape API)结合使用。

Non-Callback异步执行

Goliath另外一个重要的亮点就是Non-Callback事件驱动,试想一个嵌套Http请求的业务场景,基于EM你会写出如下的代码:

1
2
3
4
5
6
7
8
EM::HttpRequest.new('http://www.sitepoint.com/').get.callback {|http|
# extract_next_url is a fake method, you get the idea
url = extract_next_url(http.response)

EM::HttpRequest.new(url).get.callback {|http2|
puts http2.response
}
}

虽然熟悉JS程序猿可能已经习惯了callback风格的代码,但在类似复杂的情况下,callback的反直觉语法仍然会给代码可读性和可维护性带来问题

但如果你在使用Goliash开发,则不会再有这样的烦恼:

1
2
3
4
http = EM::HttpRequest.new("http://www.sitepoint.com").get
# extract_next_url is a fake method, you get the idea
url = extract_next_url(http.response)
http2 = EM::HttpRequest.new(url).get

上面的代码片段实现了以同步的风格编写代码,但两个Http请求却是通过异步的方式执行的。What!?,要想了解这里发生了什么神奇的事情,还要从Ruby Fiber说起。

Fiber

Fiber是Ruby 1.9.3版本引入的协作式并发机制,引用一下官方说明

Fibers are primitives for implementing light weight cooperative concurrency in Ruby. Basically they are a means of creating code blocks that can be paused and resumed, much like threads. The main difference is that they are never preempted and that the scheduling must be done by the programmer and not the VM.

简单来说,Thread是系统级别的概念,其运行是VM通过抢占式的调度实现的,而Fiber是一种协程(coroutine)的概念,一个Fiber何时获得系统资源是由程序猿控制的。下面这个栗子可以帮助你回顾/了解一下Fiber是怎么工作的:

1
2
3
4
5
6
7
fiber = Fiber.new do |first|
second = Fiber.yield first + 2
end

puts fiber.resume 10
puts fiber.resume 14
puts fiber.resume 18

outputs

1
2
3
12
14
FiberError: dead fiber called

如果你已经了解了Fiber是如何工作的,我们可以来看看Goliath中是如何使用Fiber实现Non-Callback异步执行的

EM-Synchrony

EM-Synchrony是Goliath项目的一个重要组成部分。EM-Synchrony使用Fiber改造了EventMachine以及基于EventMachine实现的许多异步I/O库,使程序猿可以以同步的代码风格实现异步的功能。

每一个Goliath服务都只有一个线程,但每个用户request都是在一个独立的Fiber中处理的,我们可以看看之前的EM::HttpRequest究竟是如何实现的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# em-synchrony/lib/em-synchrony/em-http.rb
begin
require "em-http-request"
rescue LoadError => error
raise "Missing EM-Synchrony dependency: gem install em-http-request"
end

module EventMachine
module HTTPMethods
%w[get head post delete put patch options].each do |type|
class_eval %[
alias :a#{type} :#{type}
def #{type}(options = {}, &blk)
f = Fiber.current
conn = setup_request(:#{type}, options, &blk)
if conn.error.nil?
conn.callback { f.resume(conn) }
conn.errback { f.resume(conn) }
Fiber.yield
else
conn
end
end
]
end
end
end

ah~, Monkey Patch,在get方法新的实现中,callbackresume相应的Fiber,从而使得处理该用户请求的Fiber重新获得系统资源,并在之前调用get方法的地方继续执行。这样对程序猿来说就只需要以同步的方式编写业务代码即可。

HTML5 Geolocation in China

HTML5 Geolocation API允许用户在Web应用中共享他们的位置,使其能够享受LBS应用所带来的独特魅力。HTML5 Geolocation API的使用方法相当简单(仅限地球^_^)。请求共享位置信息,如果用户同意,浏览器就会返回位置信息(主要是十进制的经纬度),该位置信息是通过支持HTML5地理定位功能的底层设备(如笔记本、手机)提供给浏览器的。

信息来源

底层设备通常可以通过哪些数据源来获取位置信息呢?

  • IP地址
  • 坐标定位
    • GPS
    • 基站辅助定位

IP地址定位随处可以使用,但定位不精确,甚至可能出现非常离谱的定位结果;GPS定位非常精确,但需要额外的硬件设备,耗时长,而且室内效果很差;基站辅助定位精度较为精确,定位快速且可以在室内使用。

如何使用HTML5 Geolocation API

HTML5 Geolocation API是通过navigator.geolocation对象暴露给Web应用的,用户使用时可以有两种方式:单次定位请求,周期定位请求。

单次定位

1
navigator.geolocation.getCurrentPosition(successCallback, errorCallback, options);

如果浏览器获取位置信息成功,则会调用successCallback并传入Position对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//Position object

{
timestamp: "位置信息的获取时间",
coords: {
latitude: "维度(十进制)",
longitude: "精度(十进制)",
accuracy: "准确度(以m为单位,置信度95%)",
altitude: "海拔(以m为单位)",
altitudeAccuracy: "海拔准确度(以m为单位,置信度95%)",
heading: "行进方向(相对于正北)",
speed: "速度(m/s)"
}
}

关于定位请求选项options,可以以map的形式传入如下参数的组合

  • enableHighAccuracy,高精度模式,true/false(default);
  • timeout,设置定位超时时间,ms;
  • maximumAge,设置浏览器重新定位的时间间隔,ms;

周期定位

如果用户希望随时获得浏览器定位信息的更新,则可以使用周期定位接口,接口的使用方法与单次定位完全一致。

1
navigator.geolocation.watchPosition(successCallback, errorCallback, options);

唯一不同的一点是,该接口提供了取消定位通知的功能

1
2
3
var watchId = navigator.geolocation.watchPosition(successCallback, errorCallback, options);

navigator.geolocation.clearWatch(watchId);

地球坐标,火星坐标,XX坐标

好简单啊,是么?!

不过在天朝,使用HTML5 Geolocation API就没那么简单了,如果你尝试着将自己当前位置的经纬度绘制在百度地图上,你会发现自己可能正在某个楼顶,马路中间,甚至是湖中间…发生了什么。

这里不得不从地球坐标和火星坐标说起,用户通过HTML5 Geolocation获取的经纬度信息是国际通用的WGS84坐标系统,俗称为地球坐标。而我国国测局出于安全的目的,要求境内所有地图和位置信息数据必须通过加密算法进行非线性偏移,这就是GCJ-02坐标体系,也就是大家常说的火星坐标。再进一步,国内一些地图服务提供商出于响应国家政策以及自身商业目的原因,又在此基础上进行了二次加密,如百度的BD-09坐标系统等。

坐标系列表

国内各大LBS都在用什么坐标系统呢

API 坐标系
百度地图 百度坐标
腾讯搜搜地图 火星坐标
搜狗地图 搜狗坐标
阿里云地图 火星坐标
图吧MapBar地图 图吧坐标
高德MapABC地图 火星坐标

坐标转换

网上有大量关于在各个坐标系之间进行转换的文章甚至代码,这里就不再重复了。但如果你恰好在使用百度地图的服务,建议直接使用百度坐标转换API来将各种坐标系数据转换为百度坐标。

HTML5 - Web Worker

Javascript是单线程的。因此,持续时间较长的计算会阻塞UI线程,进而导致用户无法正常使用页面中的功能,并且在大多数浏览器中,除非控制权返回,否则甚至无法打开新的标签页。HTML5 Web Worker正是这类问题的解决方案,Web Worker可以使得web页面具备后台处理能力,它对多线程的支持非常好,因此,使用了Web Worker的web应用可以充分利用多核CPU带来的优势。

进入正题之前

在决定使用Web Worker之前,你还需要了解一些信息

应用场景

如果web应用需要执行一些后台数据处理,但又这些数据处理工作影响web页面本身的交互性,那么可以通过Web Worker去执行数据处理任务,同时添加一个事件监听器去监听和处理Web Worker发送的消息。

Web Worker的另一个用途是监听由后台服务器广播的新闻信息,收到后台服务器的信息后,将其显示在Web页面上。像这种与后台服务器对话的场景中,Web Worker可能会使用到Web Socket或Server-Sent Event。

限制

尽管Web Worker功能强大,但也不是万能的,有些事情是Web Worker无能为力的。例如,Web Worker中执行的脚本不能访问该页面的window对象,换句话说,Web Worker不能直接访问web页面和DOM API。另外,虽然Web Worker不会导致浏览器UI停止响应,但是仍然会消耗CPU周期,导致系统反应速度变慢。

浏览器支持

Web Worker目前已被绝大多数主流浏览器所支持,推荐一个网站:Can I use,在网站上搜索Web Worker可以查看最新的浏览器支持情况。

使用Web Workers API

简而言之,使用Web Workers API需要编写两部分的JS代码:

  • 主页面中的JS代码 - 创建Web Worker,接收Web Worker的返回消息和错误信息
  • Web Worker中执行的JS代码 - 处理后台任务,也可以接收主页面发来的消息

创建Web Worker

可以用两种方式创建Web Worker,一种是直接传入Web Worker所要运行的JS的URL

1
worker = new Worker("job.js");

如果你的浏览器支持文件系统API(BlobBlobBuilder),你还可以加载<script>标签中inline的JS代码来创建Web Worker,例如html页面中包含如下标签

1
2
3
<script id="worker" type="javascript/worker">
...some job...
</script>

可以用下面的JS代码来创建Web Worker

1
2
blob = new Blob([document.getElementById('worker').innerText], { type: "text/javascript" });
worker = new Worker(window.URL.createObjectURL(blob));

发送消息

使用postMessage方法给Worker发送消息,可以是字符串或者任何JS对象

1
2
3
4
// someData = "some text";
// someData = {};

worker.postMessage(someData);

监听消息和错误

主页面可以注册Callback来接收Worker返回的消息或者抛出的错误信息

1
2
3
4
5
6
7
8
9
10
worker.addEventListener("message", messageHandler, true);
worker.addEventListener("error", errorHandler, true);

function messageHandler(e) {
// do something with e.data
}

function errorHandler(e) {
console.log(e.message, e);
}

销毁Web Worker

1
worker.terminate();

Web Worker导入其他JS文件

如果Web Worker中要执行复杂的处理任务,可能还需要导入其他JS文件

1
importScripts("someLib.js", "otherLib.js");

接收主页面消息

Web Worker同样可以监听主页面发来的消息,方式与主页面监听消息是相同的

1
2
3
4
5
addEventListener("message", messageHandler, true);

function messageHandler(e) {
// do something with e.data
}

向主页面发送消息

Web Worker可以向主页面发送任务运行的结果或中间状态等,其方式与主页面发送消息的方式也是相同的

1
worker.postMessage(someData);

举个栗子

一个基于Canvas的图像模糊应用,实现方法是不断对图像中的每个像素点与周围的8个像素点进行线性模糊。由于图像模糊运算任务被分配给一个或多个Web Worker在后台执行,大家可以看到无论是log打印,还是按钮的交互都没有受到影响。请查看源代码或试试Live Demo

Image Overlap in Email

Web前端需要使用到图片叠放的应用场景有很多,一个比较典型的情况是在Youtube视频的预览图片上添加一个Play按钮,上网搜索一下就会发现有很多方式可以将一个图片叠放到另一个图片上:

  1. 使用绝对定位,position:absolute
  2. 将叠放下方的图作为背景图片;
  3. 使用负的margin将一个图片移动到另一个之上。
  4. ……

##Email世界

当你需要在Email的内容中实现图片叠放时,我不得不遗憾的告诉你:上述方法均不可行,其主要原因是在Gmail, Hotmail,YahooMail等Web邮件客户端中,诸如position,background,margin等样式都是不被支持的。

如果你正在被上面提到的问题所困扰,不用着急,下面就来揭晓Email世界中的完美解决方案,不需要fancy的标签和css,你需要的只是平凡的table span。(推荐你在JsBin上面试试下面的Html代码以帮助理解。)

##独立图片

当你仅需要在一个独立的图片上叠加图片时,解决方法是相对简单的,可以利用rowspan和colspan来重合两个表格单元完成图片的叠放:

1
2
3
4
5
6
7
8
9
10
11
12
13
<table>
<tr>
<td></td>
<td rowspan=2>
<img style="border: 1px solid;background-color: aqua; width:200px; height:150px"/>
</td>
</tr>
<tr>
<td colspan=2 style="text-align: center">
<img style="border: 1px solid;background-color: aquamarine; width:60px; height:45px"/>
</td>
</tr>
</table>

效果如下:
email_image_1

##一组图片

当你需要给一组图片中的一张上叠加图片时,事情会变的稍微有些复杂,此时你可能发现使用上面的办法会破坏该图片与其他图片的对齐,大多数情况会有令人恼火的1px偏移,试试下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<table>
<tr>
<td>
<img style="border: 1px solid;background-color: aqua; width:200px; height:150px"/>
</td>
<td>
<table>
<tr>
<td></td>
<td rowspan=2>
<img style="border: 1px solid;background-color: aqua; width:200px; height:150px"/>
</td>
</tr>
<tr>
<td colspan=2 style="text-align: center">
<img style="border: 1px solid;background-color: aquamarine; width:60px; height:45px"/>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td>
<img style="border: 1px solid;background-color: aqua; width:200px; height:150px"/>
</td>
<td>
<img style="border: 1px solid;background-color: aqua; width:200px; height:150px"/>
</td>
</tr>
</table>

效果如下:
email_image_2

一个可行的解决方案是在所有要对齐的图片上都应用上相同的table span结构,试试下面的解决代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<table>
<tr>
<td rowspan=2>
<img style="border: 1px solid;background-color: aqua; width:200px; height:150px"/>
</td>
<td></td>
<td rowspan=2>
<img style="border: 1px solid;background-color: aqua; width:200px; height:150px"/>
</td>
</tr>
<tr>
<td colspan=2 style="text-align: center">
<img style="border: 1px solid;background-color: aquamarine; width:60px; height:45px"/>
</td>
</tr>
<tr>
<td rowspan=2>
<img style="border: 1px solid;background-color: aqua; width:200px; height:150px"/>
</td>
<td></td>
<td rowspan=2>
<img style="border: 1px solid;background-color: aqua; width:200px; height:150px"/>
</td>
</tr>
</table>

效果如下:
email_image_3

相信你可以将这种模式推广应用于其他更加复杂的场景:)

RSpec Matchers

RSpec是Ruby世界所熟知的测试框架,它可以用于单元测试功能测试(与Cucumber配合)。与其他测试框架一样,完成测试功能、实现红绿循环的核心是断言语句,而RSpec的断言是通过{}.shouldexpect{...}.to+Matcher实现的。下面就总结一下RSpec内建的Matcher以及如何定制自己的Matcher。

##内建Matcher

###include

数组中是否包含某元素

1
milan_players.should include('Maldini')

###respond_to

对象是否有某个方法

1
'Milan'.should respond_to(:length)

###raise_error, throw_symbol

代码执行时是否抛出异常,如果不传参数则验证任意异常的抛出;如果给一个参数,可以是某个具体的异常类或者异常描述(支持正则匹配);当然也可以给两个参数,第一个为异常类,第二个为异常描述

1
lambda { Object.new.explode! }.should raise_error(NameError)

###==, ===, equal, eql

上述几个Matcher用于测试相等,其效果与Ruby中对应的操作符或方法相同

1
(3 * 5).should == 15

注意:RSpec不建议使用!=,可以用should_not ==代替

###be_close

可以用来测试浮点型数据是否相差在某个误差内

1
result.should be_close(5.25, 0.005)

###match, =~

验证正则表达式匹配

1
result.should =~ /this expression/

###change

判断数值变化,相关使用方法非常接近自然语言,不多做解释,看例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
expect {
User.create!(:role => "admin")
}.to change{ User.admins.count }

expect {
User.create!(:role => "admin")
}.to change{ User.admins.count }.by(1)

expect {
User.create!(:role => "admin")
}.to change{ User.admins.count }.to(1)

expect {
User.create!(:role => "admin")
}.to change{ User.admins.count }.from(0).to(1)

###布尔Matcher

当一个对象中有布尔方法XXX?,则RSpec会自动为其生成如下的几个Matcher

be_XXX
be_a_XXX
be_an_XXX

###be_true(false)

用来断言真假时,还可以直接使用如下的Matcher

be_true
be_false

###have开头的布尔Matcher

当一个对象中有has_XXX?方法时,RSpec会为其自动生成have_XXX的Matcher

1
request_parameters.should have_key(:id)

补充:为了使断言语句更加接近自然语言,如果在Matcher上调用不存在的方法,该方法会被代理到Matcher所传给的目标对象,漂亮的语法糖

1
home_team.should have(9).players_on(field)

###集合Matcher

对于集合对象来说,还有几个内建的Matcher非常直观和实用

1
2
3
day.should have_exactly(24).hours
dozen_bagels.should have_at_least(12).bagels
internet.should have_at_most(2037).killer_social_networking_apps

###运算符Matcher

处理前面提到的一些运算符Matcher之外,常用的还有

1
2
3
4
result.should be < 7
result.should be <= 7
result.should be >= 7
result.should be > 7

##定制Matcher

如果RSpec内建提供的Matcher不能满足需求,你还可以自己定制专用的Matcher,具体的定义方法如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
RSpec::Matcher.define :report_to do |boss|
match do |employee|
employee.reports_to?(boss)
end

failure_message_for_should do |employee|
"expected the team run by #{boss}" to include #{employee}"
end

failure_message_for_should_not do |employee|
"expected the team run by #{boss}" to exclude #{employee}"
end

description do
"expected a member of the team run by #{boss}"
end
end

上面代码中给出了定义一个Matcher较为完整的方式,定义中的4个块传递并不都是必须的,要完成一个Matcher的定义必须提供的内容为match块和failure_message_for_should块,另外两个块是可选的。

掌握了自定义Matcher的方法,相信从技术上应该没有什么断言语句是你写不出来的喽:)

Ruby Class Secret(Part II)

让我们继续探索Ruby的Class

##类定义

静态语言中,类的定义给人的感觉更多是在定义一种数据结构,及该结构上的一系列行为,只有实例化一个对象的时候,其代码才真正的生效或者被执行。而Ruby则不同,class关键字之后,实际就是在执行一段普通的代码,举个例子

1
2
3
class MyClass
puts 'Hello!' # Hello!
end

运行该脚本,除了定义MyClass类之外,还会直接在控制台输出字符串。

##类实例变量&类方法

先看一段代码

1
2
3
4
5
6
7
class MyClass
@my_var = 1

def self.read
@my_var
end
end

大家一定要注意@my_var可不是这个类的对象的实例变量,而是MyClass类的实例变量。实例变量的归属取决于上下文,在class关键字后,self所代表的当前对象是MyClass类也是对象。也许你会觉得类实例变量类似于Java中的静态成员,但事实并非如此,类实例变量与对象实例变量处于完全不同的两个上下文,普通的对象无法访问类实例变量,但Java中类的对象可以访问类的静态成员(Ruby中类似的概念应该是@@开头的变量,但这种变量不推荐使用)。再进一步,类实例变量完全是该类独有的,即使是该类的子类也看不到这个变量。

MyClass中定义的read方法也不是普通的实例方法,而是类方法,由于类方法的方法体内处于类的上下文中,可以访问到类实例变量。

##单件方法

Ruby允许你为某个具体对象添加一个方法,这种方法即为单件方法,定义单件方法的方式如下

1
2
3
4
5
6
7
str = "just a regular string"

def str.title?
self.upcase == self
end

str.title? # false

其实上节中的类方法也是一个单件方法,与上面的例子稍有不同的是,该方法添加在了一个类上,这里又要强调了,类也是对象,因此本质是完全一样的。类方法的一个应用就是在Module中定义的访问器(attr_accessor等)。

##终极藏宝图

引入了单件方法的概念后,我们有必要重新审视一下类与对象的关系图,单件方法应该存在于哪里呢
class_and_object_3

看来类的真相上篇。中获得的藏宝图并不全,Ruby刻意隐藏了一些内幕,让我们发掘发掘,单件方法在哪里呢?答案是eigenclass。每一个对象对应一个特有的隐藏类,这个类就是eigenclass,该类只有一个实例,且不能被继承,单件方法就存在于eigenclass中,后续表述中将一个对象obj对应eigenclass记为#obj。以下面的代码为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class C
def a_method
'C#a_method'
end

def self.a_class_method
'C.a_class_method'
end
end

class D < C; end

obj = D.new

def obj.a_singleton_method
'obj#a_singleton_method'
end

当方法查找过程向一个对象索要类时,你首先得到的是该对象的eigenclass,如果eigenclass中没有要调用的方法才会继续沿着原有祖先链向上查找
class_and_object_4

调用一个类的类方法时,方法查找的过程也类似,首先会向右一步进入该类的eigenclass,再向上,这里还有一个知识点就是eigenclass的超类就是超类的eigenclass,完整藏宝图如下
class_and_object_5

##心法口诀

总结一下类的真相,共有7招:

  1. 只有一种对象——要么是普通对象,要么是模块、类;
  2. 只有一种模块——可以是普通模块、类、eigenclass;
  3. 只有一个方法,它存在于一种模块中——通常是类中;
  4. 每个对象(包括类)都有自己的“真正的类”——要么是普通类,要么是eigenclass;
  5. 除了BasicObject类无超类外,每个类有且只有一个超类。这意味着从任何类只有一条向上直到BasicObject的祖先链;
  6. 一个对象的eigenclass的超类是这个对象的类;一个类的eigenclass的超类是这个类的超类的eigenclass;
  7. 当调用一个方法,Ruby先向“右”迈一步进入接收者真正的类,然后向“上”进入祖先链。

Ruby Class Secret(Part I)

提起Ruby,大家更多会联想到Rails,正是Rails的蓬勃发展,使得Ruby为更多的Coder所熟悉。但换位思考一下,如果不是Ruby语言特性的灵活,又怎么会有Rails的敏捷。因此在这里想跟大家分享一点Ruby语言的元编程特性,特别是Ruby类背后的故事。

##打开类

关于打开类,先看一个简单的例子

1
2
3
4
5
6
7
8
9
10
11
class Fijo
def x; 'x'; end
end

class Fijo
def y; 'y'; end
end

obj = Fijo.new
obj.x # 'x'
obj.y # 'y'

上面是一个最简单的打开类的实例,你可以将第一个class认为是定义一个不存在的类,但怎么理解第二个class的工作方式呢?

从某种意义上来说Ruby的class关键字更像是一个作用域操作符,而不是一个类型声明语句。它的确可以创建一个不存在的类,但对于class关键字,其核心任务是将你带到类的上下文中,让你可以修改类的行为。

如果你的工作中仅使用一些静态语言,如Java, C++等,打开类这样的特性一定让你感到耳目一新。但我不得不将你拉回现实,事物都有两面性,打开类也会引发严重的问题,猴子补丁就是其中之一,有兴趣的童鞋可以Google之。

##对象中有什么

先来一段code

1
2
3
4
5
6
7
8
class MyClass
def my_method
@v=1
end
end

obj=MyClass.new
obj.my_method

与其他静态语言不同,Ruby中类的实例并不会在初始化时获得所有的实例变量,而仅在对某实例变量赋值后才会生成,以上面的代码为例,obj在被初始化时还没有实例变量@v,但执行my_method后,@v就生成了。

而实例方法则不同,它们存在与对象所属的类中。这里有一个概念,就是一个对象的方法称为其类的实例方法。相应的关系可以用下图来描述,后续关于Ruby类故事的主线就是对这个图的演进:
class_and_object_1

##类也是对象

类也是对象,类的类是Class,因此所有适用于对象的的规则,也同样适用于类。既然一个对象的方法是其类的实例方法,这就意味着一个类的方法是Class的实例方法(至少目前可以仅仅这样理解:))。

另外,虽然可能会让你有些混乱,但还是要将类关系补充完整,在继承关系中,Class的父类是ModuleModule的父类是ObjectObject的类是ClassClass的类是其自身。
class_and_object_2

##方法查找

当一个对象调用一个方法时,首先需要做的就是方法查找,为了了解方法查找的过程,需要先了解两个概念:接收者祖先链接收者就是调用方法的那个对象;祖先链则是从对象的类到Object的类路径。

方法查找的过程就是首先在对象所在类中查找被调用的方法,如果无法找到则在一层层的祖先链中查找,如果直到Object都无法找到对应的方法,则会调用神奇的method_missing方法。这个方法查找的过程可以总结为“向右一步,再向上”。
class_and_object_3

这里需要额外说明一点,这里的祖先链也包括模块,一个类所包含的模块会被插入到祖先链中该类与超类(或模块)之间。


到这里已经介绍了对于Ruby类与对象的基本知识,但进一步的内容还请移步下篇

Markdown Grammar Summary

Markdown的最初目标是让网络上的阅读和写作都更加方便快捷。所有的Markdown符号都使用的标点符号,可以说它是一个专门用于网络写作的技术,这个博客就是使用Markdown来编写的。

Markdown主要涵盖了Html中文本相关的功能,如果希望在Markdown中使用其他更加绚丽的表现方式,最简单的方法就是直接内联嵌入Html代码。这里提醒一点,对于<div><table><pre><p>,必须在标签的前后保留空格,便于Markdown的解释器辨识,但这些标签中不能再嵌入Markdown代码。

##换行

在行末增加2个以上的空格可以实现换行

##标题

行首插入#来创建标题,#个数决定标题级别

# This is H1
## This is H2

##引用

在行首加入>来创建引用,引用中还可以创建其他Markdown标记,甚至是引用

> Quote block

##列表

使用*创建无序列表,使用数字加英文句号x.创建有序列表,注意这里列表符号和文字内容之间要有2个空格

*  ul
1.  ol

##代码

使用tab创建一个代码片段(No example)

##行内代码

使用撇号`包围内容创建行内的代码块

Try this `code` on your console

##分割线

使用3个或以上的减号---创建一个分割线

---

##链接

方括号内是链接文字,小括号内是url,双引号内是链接的提示

[JustinFeng](http://justinfeng.github.io/ "Blog")

##直接链接

使用尖括号包围url创建直接链接

<http://justinfeng.github.io>

##强调

*包围实现强调,双**包围实现加粗

*emphasis*
**strong**

##图片

使用下面格式创建一个图片

![Alt text](/path/to/img.jpg)

##转义

在使用Markdown时如果需要使用特殊符号,同样可以使用\进行转义处理

##写在最后

本文以简洁的方式列出了Markdown中最常用的标记和语法,目的是让读者快速上手,或者进行速查。完整内容请深入学习Markdown