Polymorphic Association

有这么一个需求,一个在线音乐商店系统,我们暂且叫它’online-store’,需要记录消费者对每首歌的评论,一个有经验的rails developer会很快地写出下面的代码:

class Music
  has_many :comments
end

class Comment
  belongs_to :music
end

对应的表结构如下:

#musics:   
  |  id  | integer|   
  | name | varchar|   
  | ...  |  ...   |   
#comments:   
  | id      | integer|   
  | text    | varchar|   
  | music_id| integer|   

如果需要给music添加,查看评论,可以通过如下代码实现:

  #添加评论
  music.comments.create {:text => 'this is realy nice'}
  #查看评论
  music.coments

风云变幻,斗转星移,apple的app store创造了软件销售的新模式,我们的vendor也坐不住了,决定在现有的音乐商店系统上出售应用程序,电影,游戏等内容,同样,这些内容也需要支持评论,有了前面成功的经验,你信心满满增加了下面几个model:

  class Application
    has_many :comments
  end

  class Movie
    has_many :comments
  end

  class Game
    has_many :comments
  end

再来看看我们之前写的model Comment:

class Comment
  belongs_to :music
end

现在需要支持多种内容,而且这些类内容之间出了都可以被评论外,再无其他关联,那这个belongs_to该怎么写呢?一个最直接的思路是,扩展Comment,让其支持对以上四类内容的评论,代码如下:

class Comment
  belongs_to :music, though => "music_id"
  belongs_to :game, though => "game_id"
  belongs_to :application, though => "application_id"
  belongs_to :movie, though => "movie_id"
end

表结构如下:

#comments:   
  | id             | integer|   
  | text           | varchar|   
  | music_id       | integer|   
  | game_id        | integer|
  | application_id | integer|
  | movie_id       | integer|

有了以上的model,你就可以给应用程序,电影,游戏增加评论了:

  #创建评论
  application.comments.create {:text => "this is a nice app"}
  movie.comments.create {:text => "this is a nice movie"}
  game.comments.create {:text => "this is a nice game"}
  #查看评论
  application.comments
  movie.comments
  game.comments

目前看来,这些代码工作得很好,然而,做为一个有着良好直觉的程序员,你敏锐地觉察到,将来可能有更多的内容出现在这个”onlne-store”中,也会有更多的内容需要支持评论——你成功地识别出一个”易变点“,每当新增一种内容的时候,你就需要打开这个Comment类,新增一个association,同时,还需要增加migration,这个设计明显违背了开闭原则”。那么,这个问题该怎么解决呢? 再来分析下这个问题,上面我们提到,这些model唯一的共性是 “可以被评论”,于是我们可以抽象出一个概念——“commentable”。如果我们让comment对象知道它所对应的”commentable”对象的id以及类型(game/application/movie),我们就可以获得一个“commentable”对象的所有comments,参考下面的代码:

  #查看id为1的music的评论
  Comment.find(:commentable_id => 1, :commentable_type => 'music')

  #查看id为1的application的评论
  Comment.find(:commentable_id => 1, :commentable_type => 'application')

如此,comments的表结构就可以简化为:

#comments:   
  | id              | integer|   
  | text            | varchar|   
  | commentable_id  | integer|   
  | commentable_type| varchar|

model代码简化为:

 #添加评论
 Comment.create({:text => "good staff", :commentable_id => "1", :commentable_type => "music"})
 #查看评论
 Comment.find(:commentable_id => 1, :commentable_type => 'music')
  class Comment
    belongs_to :commentable, :polymorphic => true
  end
  class Application
    has_many :comments, :as => :commentable
  end
  class Movie
    has_many :comments, :as => :commentable
  end
  class Game
    has_many :comments, :as => :commentable
  end
  #添加评论
  movie.comments.create {:text => 'this is realy nice'} 
  #查看评论
  movie.coments

更多关于“polymorphic association”的信息,请参考这里 怎么样,这样一来,再新增多少种内容类型,处理起来都非常容易,扩展下commentable_type的值就可以了。这个思路的根本出发点在于为识别、分离变化点。这类问题可以抽象为这样一个问题:”如何把一个概念应用到一组除了这个概念,没有其他任何关联的对象上?” ,此类问题可以采用上述思路解决。

声明:本文翻译自这里

递归101

我们都知道什么是递归吧?一个自己调用自己的函数,或者函数A调用函数B,函数B又调用函数A,或者是A调用B,B调用C, C调用A。但是我们大多数情况下所说的递归函数,是指一个调用自身的函数。
在Java世界中,递归函数的曝光率很低,说起来有不少原因。
第一,递归不直观,难以理解。对于一段循环代码(for, while), 你可以很直白地看到这段逻辑的全景,即便你是一个初学者,而对于一段递归代码,就不是那么容易了,你只能看到递归逻辑的一次调用,而不得不想象当递归调用发生时,这些多次调用是如何组合在一起的。
第二,比起递归,循环在java中更加容易实现,比如for ,for-each,while, do-while, 数组, iterator, ResultSet,这些结构都是用来实现循环的。
第三,Java中的递归有自己的Achille’s heel: call stack。

总的来说,当调用一个函数时,一个新的call stack frame会被放到call stack的顶部,用以保存局部变量,当前函数的caller,等等。但是,call stack的大小是有限的,当递归的深度不是很深时,调用递归函数是没有什么问题的,但是如果递归调用的深度无法预计,那么很有可能会导致stack overflow. 而循环却不会有这种问题(因为循环不会产生新的call stack frame),因此,使用循环更加安全。

Scala中的递归

Scala,作为一新兴的functional language,更偏爱递归胜过循环,那么在scala中,是如何解决call stack大小限制的问题的呢?我们来看一个例子:

  def listLength1(list: List[_]): Int = {
    if (list == Nil) 0
    else 1 + listLength1(list.tail)
  }

  var list1 = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
  var list2 = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
  1 to 15 foreach( x => list2 = list2 ++ list2 )

  println( listLength1( list1 ) )
  println( listLength1( list2 ) )

函数listLenght1递归地计算list中item的数量,这个函数在计算item数量较少的list时工作地很好,然而在item数量很大时,就会得到stack overflow错误. 递归是函数式语言的谋生手段,但是在Scala中,递归仍然倒在了stack overflow的脚下。

先别急着放弃,Scala有一个很重要的优化递归的方案,只要你用了正确的递归类型。

首递归和尾递归

根据递归调用的方式,递归可以分为首递归和尾递归。在首递归函数中,函数调用自身后,再进行其他运算(可能会把自用自身后的结果做为这些运算的输入)。在尾递归函数中,所有的计算都在函数调用自身前完成,调用自身是尾递归函数中做的最后一件事情。

这两种递归的区别的重要性目前看起来还不是那么明显,然而,它确实很重要!想象一下一个尾递归函数的执行过程,首先完成所有的计算,在最后一步,马上进行对自身递归的调用,一般情况下,这个时候就该使用call stack frame记录方法调用状态了,然而,这里却不需要:我们不需要记录局部变量,因为所有的计算都已经完成。我们也不需要知道目前在哪个函数中,因为我们始终在同一个函数中。基于以上前提,scala不会创建一个新的call stack frame,而是重用当前的call stack frame ,无论调用次数有多少,call stack也不会增长。这就是scala中尾递归函数的特殊性。在其他语言中,语言设计者通过把尾递归转换成循环的方式进行了优化。

而在首递归函数中,递归调用是不一样的,这是为什么呢?想象一下一个首递归函数的执行过程:先执行一些计算,在递归调用自身,然后在执行另一些计算。在调用自身前,需要记住当前的局部变量,以便在从递归调用返回后继续进行后面的运算,这样,就必须创建一个新的call stack frame来记录当前状态。因此首递归函数还是会有stack overflow的风险。并且无法被优化。

在这里问你一个问题,上面的listLenght1是一个尾递归还是首递归?让我们来看着这个函数做了哪些事情。
A) 检查参数是否为Nil。
B) 如果为空,返回零,因为Nil的长度是零。
C) 如果不为空,则返回1加上递归调用的结果。
递归调用逻辑在这个函数的最后一行,应该是尾递归函数吧?错!在尾递归调用结束后,然后对递归调用结果加一,然后返回最终结果。这实际上首递归(或者可以叫做中递归)因为递归调用并不是所有运算的最后一步。

尾递归例子

当你用scala写一个递归函数时,你的目标是写成尾递归以便编译器对尾递归进行优化。现在让我们把上面的那个函数重写为尾递归函数。

def listLength2(list: List[_]): Int = {
  def listLength2Helper(list: List[_], len: Int): Int = {
    if (list == Nil) len
    else listLength2Helper(list.tail, len + 1)
  }
  listLength2Helper(list, 0)
}

println( listLength2( list1 ) )
println( listLength2( list2 ) )

我写成两个函数(listLength2 和一个内部的helper函数)以便和上面的例子中的函数接口保持一致。如果你能给listLength2Helper的参数给个默认值,我们就能只提供一个函数,但是我不知道怎么做。长话短说:listLength2只调用了做了实际工作的listLength2Helper,而且listLength2Helper也是个递归函数。

listLength2Helper是个尾递归函数吗?递归调用是所有运算的最后一步,允许scala进行优化?就像listLenght1一样,listLength2首先检查参数是否为Nil,如果不是,就进行递归调用,但是仍然会有一个加一的操作 —— len + 1。难道这个就不是尾递归吗?不,len + 1 运算会在递归调用前运算。只有所有的参数运算完了以后,才会进行递归调用,这个函数确实是个尾递归函数。

什么是vagrant?

看<a =href=”http://vagrantup.com/docs/getting-started/index.html”>这里</a>

为什么要用vagrant?

看<a =href=”http://vagrantup.com/docs/getting-started/why.html”>这里</a>

怎样开始使用vagrant?

  1. 安装VirtualBox
  2. 安装基础虚拟机
$ vagrant box add base http://files.vagrantup.com/lucid32.box

这个基础虚拟机是一个运行着ubuntu,没有安装额外软件的虚拟机,以后的所有更改都是在这个基础虚拟机上进行的。

由于国内网络环境不好,推荐先把这个基础虚拟机镜像下载到本地,然后通过如下脚本安装:

$ vagrant box add base ./lucid32.box

然后运行如下命令初始化,启动虚拟机:

$ vagrant init base
$ vagrant up

至此,一个vagrant的基础虚拟机环境就创建成功了,想知道如何访问这个虚拟机?通过如下命令就可以登陆上虚拟机了。

$ vagrant ssh

怎么配置虚拟机?

#创建一个目录用于存放配置虚拟机的文件
$ mkdir mybox 
#初始化这个新虚拟机
$ cd mybox 
$ vagrant box add mybox http://files.vagrantup.com/lucid32.box
$ vagrant init

经过以上步骤,在mybox目录下会多出一个文件Vagrantfile,这个就是配置虚拟机的文件, 在这个文件里,vagrant支持通过chefpuppet对虚拟机进行配置。

如何使用这个虚拟机?

# 启动虚拟机
$ vagrant up
# 停止虚拟机
$ vagrant halt
# 登陆到虚拟机
$ vagrant ssh
# 删除虚拟机
$ vagrant destroy
# 把当前虚拟机打包成一个本地镜像,便于再次分发
$ vagrant package

OOCamp Day1

  • Tasking 做tasking的标准是,这个task具备可测试性
  • 封装 对一个类的属性的读取,修改都在该类的上下文,那么这个类就没有封装泄露。 tell, don’t ask

OOCamp Day2

使用自定义对象而不是java原生对象,更加容易适应需求变化

OOCamp Day3

  • 什么是重构? 不改变功能的情况下改变代码的架构,重构的时候可以随时停止
  • 如何识别bad smell? 如果有一个子类,没有比父类更多的状态,那么这其实一个strategy模式
  • 如何证明代码被改进了? 把一个badsmell经过重构变成了一个pattern,或者bad smell消失了,那么代码就是被改进了。
  • 什么是Pattern? Pattern是一类问题及该类问题的解决方案。使用Pattern很容易,关键是正确地识别出问题, 而问题一般都表现为一个bad smell。例如下面几个problem以及解决这些bad smell所使用pattern:
    class explosion Decorator
    object creation is dependent on outside Factory

OOCamp Day4

重构

  • 重构技巧:copy & paste => extract method & inline method
  • 代码中,什么是其组成的最小单元? 功能
  • 要有足够的bad smell支撑你来做重构

OOCamp Day5

一个很难发现的bad smell: 上下文分散, Pattern:Strategy,把业务逻辑放到更加集中的类里

OOCamp Day6

架构:

  • 什么是架构?
    软件架构回答了以下两个问题:
    1. 系统中有哪些组件
    2. 组件之间如何交互
  • 架构只有有了应用场景,才是有意义的

  • 五种常用的架构:
    单体: Singleton,最简单的架构
    黑板:模块之间的地位平等, 广播消息
    分层:上层把下层当做抽象机器, 拒绝跨层. 大多数问题都可以通过引入一个分层来解决, 但这通常都会带来新的问题.
    数据流:过滤器
    微核:分成平台和核. 带来的问题是隐性知识增多,一个典型的例子就是spring.

DRY:
DRY的代价是耦合, 绝大部分是值得的, 有些时候,需要故意引入重复来消除耦合.
DRY隐含了知识管理成本
note: 没有任何好的软件实践是不需要代价的.

Mock:
当我们提起Mock时,实际上在不同的上下文中,有着不同的含义。
如果你想测试的代码依赖一个尚未实现的外部接口,你需要“mock”这个外部接口,让其按照约定的行为工作, 然后测试你的程序,这时候,“mock”是一种技术。如果你写了了一个实现了这个外部接口的‘mock’ class,在这个class中返回假的数据以支撑你的测试,这时候,“mock”是一种“mock framework”。

当然,在实际开发过程中,你一般不需要自行实现一个“mock framework”,有太多的mock framework供你选择, easymock,jmock, mockito等等。

几种常见的mock手段:
fake, stub, dummy, mock

Q:什么时候决定了当前在用mock?
A:call verify, 在此之前,都是在stub.

State based testing:
内容实现不稳定时,采用基于状态的测试, 用stub即可。
Interaction based testing:
在明确系统的行为边界时,需采用基于交互的测试,需要使用mock来验证代码的行为。

Git save snapshot in each commit rather than store the delta

Local operations:

Three states:

modified

modified but not adding to git local database
Working directory

staged

modified and ready to add to git local database
Staging area

commited

data safely stored in local database
Git repository

Git config

you can put git config file in the following places:

  • /etc/gitconfig
  • ~/.gitconfig
  • /.git/config

you can show your git configs by

  • git config –list