随心所动

最近偶尔会听一听台湾大学孙中兴教授的社会学的公开课,偶尔能听到一些非常有意思的观点。今天听到这么一段,让人颇有感慨:孙老师年轻时也是颇迷茫,在大学毕业时并不清楚自己想干什么,只是觉得自己不想去考公务员,于是老师问他,既然你不知道自己想干什么,那你最擅长干什么?孙老师回答说是读书啊,老师说那你就继续读书吧。于是孙老师就继续去国外念书,然后一路走下来成了台湾大学教授,虽然物质生活未必丰富,但是读书教学还是自得其乐,也算是过上了自己想要的生活。所以这里的隐喻是,既然你不知道自己想要什么,不如就跟着内心去走。

回想自己的例子,2012年做了广州某商业银行的虚拟柜员机项目,用户操作方式和以往的 C/S 架构有很大不同,需要集成大量的其他周边设备,还需要和银行生产环境集成,对安全性,稳定性都有较高要求,由于该项目是国内首例虚拟柜员机项目,项目组也是临时组建的,客户对该项目非常重视,工期短、任务重,所以项目期间做得非常辛苦,加班到午夜1-2点是常有的事。在燥热的午夜,办公区为了节能连空调都停了,我常常到大厦旁边的711去给团队买上一大堆凉茶,聊以安抚大家的情绪。经过半年的苦干,项目一期终于上线,该银行在该领域获得了较好的知名度和社会效益。但此时我却在想,这是我要的生活么?此时正值公司内部架构调整,我于是提出了内部换岗的要求,我和领导沟通,希望结束差旅生活,能够回实验室继续做软件开发。

在换岗的过程中,也有其他机会,软件销售部门的C总给我打过两个电话,极力希望我能够去他的部门担任架构师。仔细思考后,我婉拒了。理由也不复杂,在以前的工作过程中,我所接触过的部分公司软件并不好用,自己都觉得不买账的软件,我不愿意推荐给客户,我过不了自己这关。既然自己做开发是比较擅长的,不如回实验室做软件开发,看有没有机会带来一些改变。事后来看,这个决定是正确的,在我回实验室部门一年后,随着公司的不断转型,软件销售部门就进行了大调整,现在正经的软件销售部门已经不存在了。

像这样持这种观点的同仁还是很多的,现实的例子也是不少的,且不去说商业领域的成功人士,在技术领域比较成功的几个:吴云洋(云风)在大学期间就一直从事热爱的游戏开发一直坚持下来最终成为知名的游戏开发者;阮一峰以前是经济学博士,却最终转型做了一名程序员,他的博客在程序员圈子里影响力也不少;最知名的算是侯捷老师,年轻时就以写译和技术教育为自己的目标,他的严谨的治学态度影响了一代程序员。又例如:IBM选出了2018年的IBM院士——IBM技术领域的最高头衔,以表彰在技术领域作出突出贡献的技术领导者。在他们之中,我们一定都能找到自己的榜样。

现在的IT圈子感觉充斥着浮躁的大多数,在职业道路技术方向上迷茫者甚众。当今社会变化极其迅捷,新技术不断涌现,许多公司尚且不断转型以适应市场变化,遑论我们个人。在做选择时,我的体会是有必要跳出自己的圈子来看问题,或者站在更高的层面来看问题,从历史哲学社会经济的角度进行批判性思考,倾听下自己内心深处的声音,则我们之前所遭遇的很多尴尬其实根本不算什么。想必迷惑之雾必能散尽,做人做事都可以从容起来。

芳华

2017年的圣诞节,公司放假一天,趁着人不多带了母亲去看冯小刚的《芳华》。故事在旁白中娓娓道来,音乐总在扣动心弦时准时想起,冯小刚以一种文艺的方式向他所逝去的青春致敬,但他还想讲述的更多:运动的狂热、人性的冷漠、战争的残酷,时间的无情,芳华再美好,仍旧脱不开人性和时代。看完片子,一时感慨,总觉得要写点什么。在那个时代背景下,小人物的命运被战争和时代所左右,充满了无力感,善良的人也仿佛只能低着头,用沉默和寡淡来抗争,用时间来填补内心的创伤。

豆瓣比较火的一个话题是:《芳华》中最打动你的片段或细节是什么?影片最后的那个场景也让我久久不能忘怀,刘峰和小萍一起坐在车站的长椅上,小萍说当年送刘峰走之前想说的一句话,放在心里十几年了。刘峰问是什么话。小萍鼓足勇气,只说出那句:你能不能抱一下我?刘峰于是坚定地用唯一的一只手抱住了小萍,两个人依偎在一起。此时旁白响起,说两人从此相依为命,若干年后战友见面时最是恬静和温和。那一句憋在心里的“你能不能抱一下我?!”是多么执着而纯粹的爱。这样的情感,可以藏在心里帮她撑过多少苦楚岁月?可以心心念念,多少年就那么单着。所以这句话让两个被无情岁月和时代洪流摧残到伤痕累累的人紧紧依靠在一起,时间久了,恬淡如水,有彼此足矣!

如果说我们的父辈50后60后们感动落泪是因为相同的时代背景而感同身受,而作为八零后被打动的就更多的是从人性和历史的角度来观察,在弱小被欺凌时的无奈哽咽在喉,在枪林弹雨中生命如狂风暴雨吹灭蜡烛般简单,我们或多或少的善良在音乐响起的那一刻触动。但影片最后告诉我们,暴风雨过后最终总是平静,正如马路上走过的某某某,当年可能就是战斗英雄。芳华终将逝去,生命最终将归于平凡,这是我们每个人终将面对的的归宿和结局。

但仅仅是像逝去的青春致敬够么?对待过往的那段历史洪流应该是一种怎样的态度,对那些在那个特殊年代里,伤害其他人的阴暗角落,作者和导演是不想再提还是被审查制度阉割了就不得而知。不过也许生活的真实可能正在于此,林丁丁们对于自己年轻时犯过的过失早已经抛诸脑后,并且在优渥的生活中发起福来,正如全片比较接地气的一段:刘峰在海口意外碰见穗子,看见发福的林丁丁的照片,刘峰扑哧暗笑了一笑。那个生活优渥,青春已逝,气质不再的中年女人,还是以前他暗恋许久的林丁丁么?他大概想,还好她当年拒绝了他的表白。这个场景,相信很多人都会感同身受,时间是把杀猪刀,所谓最美的爱情花朵,只能在那个青涩的芳华里盛开。

所以,再美丽的芳华,也摆脱不了时间和历史洪流的裹挟;再坚韧的芳华,也经不起人性中恶毒的摧残。芳华易逝,又或芳华已逝,又何须顿首叹惋,你我应在对自己、对周遭生命的最大的善意中且行且珍惜!

如何理解 Ruby 类的实例变量

最近在项目中需要定义不同的规则,由于规则是可能经常变化,且规则数量可能较多。于是用了一点 Ruby 的元编程特性,来实现一种简单的 DSL 从而来简化整个工程的结构。先来看看这个简单的 DSL 的用法来理解下我们的需求:


# 假设定义一个 Validator 用来检查用户密码是否合规
class PasswordValidator < RulesValidator 
    # 定义一条检查密码长度的规则 
    rule "rule1", "密码数据不能为空" do |data| 
        raise "密码数据不能为空" if (data.nil?) 
    end 

    # 定义一条检查密码长度的规则 
    rule "rule2", "检查密码长度是否大于等于8" do |data| 
        raise "密码长度至少为8位" unless data.length >= 8
    end
end

# 假设定义一个 Validator 用来检查用户密码是否合规
class UsernameValidator < RulesValidator
    # 定义一条检查密码长度的规则
    rule "rule3", "用户名不能为空" do |data|
        raise "用户名不能为空" if (data.nil?)
    end
end

这样,我们可以将不同的规则实现到不同的 class 中,熟悉 Sinatra 的同学会发现,这种使用方式和 Sinatra 中使用 post/get 等关键字定义 REST 服务的方式非常类似 。现在的问题是,如何实现 RulesValidator 以及如何使用 PasswordValidator 和 UsernameValidator 来取进行校验。在上述示例代码中,DSL 关键字 'rule' 所支持的逻辑,实际上是在类定义中实现的,有点类似于用 def 关键字定义一个类的方法。但实际上,'rule' 是 Ruby 解析器在解析类定义时所执行的一个函数。因此 'rule' 必须实现成类方法(可以利用 class << self ~ end 方式来定义),从而让解析器在解析子类时能够进行调用。从这两个类的使用方式来看,有两种实现方式,但实际的效果在 Ruby 中其实有很大差别。

1. 每次创建 RulesValidator 子类的实例进行校验,例如:


password_validator = PasswordValidator.new 
password_validator.validate("password")

username_validator = UsernameValidator.new
username_validator.validate("username")

对于这种方式,采用最直接的相关的 RulesValidator 实现如下:


class RulesValidator
    # 定义类方法 (class method),可以直接通过类名进行调用
    class << self
        # 定义一条规则,规则的检查逻辑通过 block 来执行
        # @@rules ||= [] 为初始化 @rules 对象为一个数组
        def rule(name, desc, &rule_block)
            (@@rules ||= []) << { :name => name, 
                :desc => desc, 
                :rule_block => rule_block }
        end 
    
    end
    
    # 调用添加的所有规则来对数据进行检查
    def validate(data)
        @@rules.each do |rule|
            rule[:rule_block].call(data)
        end
    end
    
    def print_rules
        puts @@rules
    end
end

在这种实现方式中,由于要创建类实例来进行调用,'validate' 只是普通的方法而非类方法(class method),因此仅在创建的类实例上可以进行调用。'rule' 实现为类方法,并且很自然地使用了类变量 @@rule 将动态创建的规则存放在其中,从而保证规则在子类解析时可以创建。但是我们在测试时会发现,我们只对其中的一个 Validator 进行调用,就输出了所有定义的所有三个规则内容:


validator = PasswordValidator.new
validator.print_rules

Output:

{:name=>"rule1", :desc=>"\u5BC6\u7801\u6570\u636E\u4E0D\u80FD\u4E3A\u7A7A", :rule_block=>#}
{:name=>"rule2", :desc=>"\u68C0\u67E5\u5BC6\u7801\u957F\u5EA6\u662F\u5426\u5927\u4E8E\u7B49\u4E8E8", :rule_block=>#}
{:name=>"rule3", :desc=>"\u7528\u6237\u540D\u4E0D\u80FD\u4E3A\u7A7A", :rule_block=>#}

很明显,@@rules 作为 class variable,在所有子类中共享了父类的 class variable,这不符合我们的要求,PasswordValidator 应当只包含其自身所定义的两个规则才对。

2. 直接使用 RulesValidator 子类的类方法进行校验,例如:


PasswordValidator.validate("password")
UsernameValidator.validate("username")

其对应的实现代码如下:


class RulesValidator
    # 定义类方法 (class method),可以直接通过类名进行调用
    class << self
        # 定义一条规则,规则的检查逻辑通过 block 来执行
        # @rules ||= [] 为初始化 @rules 对象为一个数组
        def rule(name, desc, &rule_block)
            (@rules ||= []) << {
                :name => name, 
                :desc => desc, 
                :rule_block => rule_block }
        end 

        # 调用添加的所有规则来对数据进行检查
        def validate(data)
            @rules.each do |rule|
                rule[:rule_block].call(data)
            end
        end

        def print_rules
            puts @rules
        end
    end
    
end

在这个过程中,使用了类方法(class method)来添加新的规则,而新的规则被存放到 @rule 实例变量 (instance variable) 中。此外,'validate' 也被实现成了 class method。让我们再来看看 PasswordValidator 有几条规则:


PasswordValidator.print_rules

Output:

{:name=>"rule1", :desc=>"\u5BC6\u7801\u6570\u636E\u4E0D\u80FD\u4E3A\u7A7A", :rule_block=>#}
{:name=>"rule2", :desc=>"\u68C0\u67E5\u5BC6\u7801\u957F\u5EA6\u662F\u5426\u5927\u4E8E\u7B49\u4E8E8", :rule_block=>#}

这时可以发现, PasswordValidator 中正确地输出了两条规则。这是由于用于存放 @rule 是实例变量 (instance variable) 分别由两个子类分别持有。那么问题来了,既然 @rule 是实例变量,那么能否将 ‘validate’ 方法移出 class << self ~ end,使其变成一个普通的方法,从而可以对子类进行实例化之后再进行调用呢?例如:


class RulesValidator
    # 定义类方法 (class method),可以直接通过类名进行调用
    class << self
        # 定义一条规则,规则的检查逻辑通过 block 来执行
        # @rules ||= [] 为初始化 @rules 对象为一个数组
        def rule(name, desc, &rule_block)
            (@rules ||= []) << {
                :name => name, 
                :desc => desc, 
                :rule_block => rule_block }
        end 

    end

    # 普通方法
    # 调用添加的所有规则来对数据进行检查
    def validate(data)
        @rules.each do |rule|
            rule[:rule_block].call(data)
        end
    end

    def print_rules
        puts @rules
    end
    
end

validator = PasswordValidator.new
validator.print_rules
validator.validate("abc")

Output:

rules.rb:17:in `validate': undefined method `each' for nil:NilClass (NoMethodError)
        from rules.rb:55:in `
'

我们很奇怪的发现出错了,print_rules 方法根本没有打印出任何东西,而且在 'validate' 方法调用时出错了,说明 @rule 时 nil,根本没有被初始化。为什么会这样呢?不是所有的 Ruby 公开文档都说 @variable 是实例变量,应该是属于类实例的吗?好吧,让我们来揭开谜底,实际上,在上一段代码中,虽然名称相同,但是'validate' 中的 @rules 和 'rule' @rules 根本就是两个不同的对象。在 Ruby 世界中,所有东西都是对象 (object),其实不存在类似于 Java/C++ 这种强类型语言的类成员。@variable 就应该理解为对象的成员,当 @variable 的 scope 在类方法中 (class method)时,它就属于类对象(class object)本身;当 @variable 在普通的方法中声明时,它就属于类实例化后的对象 (object)。

下面给出一段代码,可以更清楚地看清楚 Ruby 中各种不同 variable 的使用范围:


class TestVariables

    @class_instance_variable1 = "1"
    @class_instance_variable2 = "2"
    @@class_variable_1 = "1"
    @@class_variable_2 = "2"

    # variable2 = "2"

    def initialize
        @instance_variable1 = "1"
        @instance_variable2 = "2"
    end

    class << self
        def class_method
            @rules = []
        end
    end
end

class TestVariablesChild < TestVariables
end


object = TestVariables.new
puts "Class variables: #{TestVariables.class_variables}"
puts "Instance variables: #{object.instance_variables}"
puts "Instance variables of the TestVariables Class: #{TestVariables.instance_variables}"
TestVariables.class_method
puts "----"
puts "Instance variables: #{object.instance_variables}"
puts "Instance variables of the TestVariables Class: #{TestVariables.instance_variables}"
puts "----"
TestVariablesChild.class_method
puts "Instance variables of the TestVariablesChild Class: #{TestVariablesChild.instance_variables}"

输出结果如下:


Class variables: [:@@class_variable_1, :@@class_variable_2]
Instance variables: [:@instance_variable1, :@instance_variable2]
Instance variables of the Class: [:@class_instance_variable1, :@class_instance_variable2]
----
Instance variables: [:@instance_variable1, :@instance_variable2]
Instance variables of the Class: [:@class_instance_variable1, :@class_instance_variable2, :@rules]
----
Instance variables of the Class: [:@rules]

可以看到,父类和子类分别拥有自己的 instance variable,我姑且称之为 class instance variable 用来表示这种变量是创建在类对象上的。并且值得注意的是,子类并未直接继承父类的实例变量。对父类子类分别调用 class_method 初始化 @rule 变量,分别创建出两个不同的 @rule 变量。

通过这冗长的描述,可以大致看到 Ruby 编程(特别是元编程)的神奇和灵活之处,语言本身就像橡皮泥,可以随意捏成你想要的样子。但这也像是一把双刃剑,如果不细致观察,仔细体会其中的细微差别,就有可能“失之毫厘,谬以千里”。所以,编程语言之间还是存在诸多的风格差异,还是比较习惯 Java 这种强类型语言一是一、二是二的感觉,只可惜在互联网处处讲究快速部署的时代,Java 语言的各种生态确有些太重了。