问题

Rails 项目随着代码量不断增大,复杂度不断增加,开始越来越难以维护。表现在:

  1. fat controller
    • 不断增加的需求使 controller 塞满了各种 action, 比如 search, order 等等。
    • 某些 action 的逻辑特别复杂,比如 search 会根据 params 实现不同的 filter 逻辑。
  2. fat model
    • 不断增加的需求使 model 塞满了各种 method, scope and callback

fat model 和 controller 降低了代码的可读性和可扩展性,让我们在增加新功能时束手束脚,让测试速度降低,让新人陷入阅读面条代码的痛苦中。

‘Growing Rails Applications In Practice’ (以下简写为 ‘Growing Rails’) 这本书提出了一套解决方法,让 Rails 项目在不断扩张的同时保持良好的可读性和可扩展性。

观点

‘Growing Rails’ 提出了几点观点,书中所有的方法都是建立在这些观点之上的,它们是:

  • complex is complex. 复杂的代码就是复杂的,我们能做的只是用模块的方法管理复杂度
  • we can manage the complexity in Rails way. 我们选择了 Rails 框架,就要利用 Rails 推荐的方法来写代码。也许会有些反直觉的做法,把这当成是一种 trade-off

方法

Better Controller

采用一种一致的 controller 设计:Rails CRUD

咋看上去不适于 CRUD 的交互行为,也 map 到 CRUD 上,比如订阅/退订可以用 subscription create/destroy

Use ActiveRecord’s validation and callback

使用 validation and callback 而不是自定义的方法来保证 model 不被误用

User ActiveRecord-like model

那些不和数据库打交道的交互,比如搜索,登陆等,仍然使用像ActiveRecord一样的建模方式,除了可读性好,心理负担轻以外,还有如下好处:

  • 利用表单,比如 simple_form 的友好的错误提示
  • 自动转换 params 的 string 到 integer 等等格式
  • i18n
  • Transaction

Dealing with fat model

避免 fat model 的关键是多写 model,复杂的就是复杂的,我们只是用模块的方式来管理复杂度。

书中举了一个例子,就像你每天都有一堆邮件寄到家里,你要么做一个邮箱装它们,要么这些邮件就散落在书桌,饭桌,床等各处。”Code never goes away”, 代码不会凭空消失的,我们要么新建一个模块放置它,要么把它散落到现有的模块中。而好的管理方法是一个模块只做一件事。

我们把 model 分成一个核心类和n个附属类,核心类只包括:

  • 最少的 validation, callback 保证数据完整性
  • assciations
  • 通用的 scope

那些只在特定页面出现的特定逻辑不应该放在核心类中:

  • 只在某些页面出现的 validation,比如注册才需要密码
  • 与数据库不一一对应的属性,比如用逗号分隔的tags
  • 只在某些场景需要的 callback, 比如欢迎邮件
  • 权限管理,比如 admin
  • 与 view 有关的 helper

以上这些都应该放在 form model 并在核心类的 namespace 下管理。

结论

提出了一套构建 Rails 项目的方法,不用 SOA, 纯 Rails way。

缘起

前两天同事晓孟给我们做了一期 JS 框架的分享,秀了一段神奇的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def ("Person") ({
init: function(name){
this.name = name;
},

speak: function(text){
alert(text || "Hi, my name is " + this.name);
}
});

def ("Ninja") << Person ({
init: function(name){
this._super();
},

kick: function(){
this.speak("I kick u!");
}
});

var ninjy = new Ninja("JDD");

ninjy.speak();
ninjy.kick();

这段 JS 代码是在模拟 Ruby,定义了一个“类” Person 以及一个继承了 Person 的“类” Ninja。看到这段代码的时候,我最惊讶的是 11 行里面的 << 是怎么做到继承的,JS 里面是没有运算符重载的。这个神奇的效果吸引我在分享会后打开了 def.js 的代码库。

定义类

预备

在分析源代码之前,我们先复习一下 JS 中的两个概念,构造函数和原型链。

构造函数和原型链

在 JS 中,任何函数都可以成为构造函数;只要通过 new 关键字方式调用,就称其为构造函数。new关键字调用构造函数时会

  1. 创建一个对象
  2. 把构造函数内的this指向这个对象
  3. 把这个对象的 __proto__ 指向构造函数的prototype
  4. 这个对象就是new表达式的值

JS 中对象会沿着原型链,即__proto__查找属性,一直到原型链的顶部。

1
2
3
4
5
6
7
8
9
10
11
12
function Foo() {
this.value = 42;
}

Foo.prototype.method = function() {
this.value = this.value - 1;
}

var foo = new Foo();
foo.value; // 42
foo.method();
foo.value; // 41

上面代码中第 1 行到第 9 行就是 def.js 定义一个类的示例,重写如下

1
2
3
4
5
6
7
8
9
10
var P = def ("Person");
P ({
init: function(name){
this.name = name;
},

speak: function(text){
alert(text || "Hi, my name is " + this.name);
}
});

我特意把它分成两个部分,第一部分是第 1 行 def ("Person"), 其中 def 是 def.js 定义的一个函数(删除了部分代码方便理解)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
(function(global){
var deferred;
// ...
function def(context, klassName) {
klassName || (klassName = context, context = global);
// 定义一个构造函数
var Klass = context[klassName] = function Klass(){
return this.init && this.init.apply(this, arguments);
}
// make this class extendable
Klass.extend = extend;

// 设置属性
deferred = function(props){
return Klass.extend(props);
};

// ...
return deferred;
}
global.def = def;
}(this));

def 做了两件事

  1. 定义了构造函数,并把这个构造函数Klass赋给一个全局变量。
  2. 返回一个函数,这个函数输入一个定义各种属性的对象,用这些属性扩展(extend)构造函数Klassprototype
1
2
3
4
5
6
7
8
9
10
function extend(source) {
var prop, target = this.prototype;

for (var key in source)
if (source.hasOwnProperty(key)) {
prop = target[key] = source[key];
}
}
return this;
}

那么,def ("Person") 就是一个函数,我把它赋给变量P。同时,生成了一个全局变量Person,它是一个构造函数。

第二部分,调用函数P({...}),扩展了Person这个构造函数,把构造函数的prototype增添上了initspeak属性。

Person

继承一个类

接下来看看示例代码中最神奇的一段

1
2
3
4
5
6
7
8
9
def ("Ninja") << Person ({
init: function(name){
this._super();
},

kick: function(){
this.speak("I kick u!");
}
});

为了方便理解,把它重写成

1
2
3
4
5
6
7
8
9
10
11
12
13
var N = def ("Ninja");

var temp = Person ({
init: function(name){
this._super();
},

kick: function(){
this.speak("I kick u!");
}
});

N << temp;

先看第一段。 def ("Ninja")本身是一个函数,我把它赋给变量N;同时会生成一个全局的构造函数Nijia

再看第二段。这里把Person这个构造函数当做普通函数使用,会发生什么事情呢?还是再来复习一下 JS

预备

又是构造函数

构造函数直接调用时,没有新的对象被创建,而this会被指向global

1
2
3
4
5
function Foo() {
this.bla = 1; // 获取设置全局参数
}
Foo(); // undefined
bla; // 1

闭包

看下面的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

var fn = (function() {
var closure = {};
function f(name) {
return function() {
closure.pk = name;
}
}
return f;
})();

var a = fn('a');
a();
var b = fn('b');
b();

变量ab有共同的闭包closure,def.js 就是利用这一点传递属性。

closure

Object.prototype.valueOf()

在需要原生值(primitive value)的时候却遇到了对象,JS 就会自动调用valueOf方法把对象转换为原生值。

1
2
3
4
5
6
7
8
9
10
11
12
13

function MyNumber(value) {
this.value = value;
}
MyNumber.prototype.valueOf = function() {
console.log(this.value);
return this.value;
}

var num1 = new MyNumber(1);
var num2 = new MyNumber(2);

num1 + num2; // 调用 valueOf,会打印 1 和 2 到 console

再看看 def.js def 部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
(function(global){

// 闭包用来传递属性,继承关系
var deferred;
// ...
function def(context, klassName) {
klassName || (klassName = context, context = global);
// 定义一个构造函数
var Klass = context[klassName] = function Klass(){
if(context != this){
// called as a constructor
// allow init to return a different class/object
return this.init && this.init.apply(this, arguments);
}
// called as a function - defer setup of superclass and properties
deferred._super = Klass; // 继承
deferred._props = arguments[0] || { }; // 传递属性
}
// ...
}
global.def = def;
}(this));

当把Person这个构造函数当做普通函数使用的时候,contextthis 都是 global,会利用PersonNinja共同的闭包deferred做属性传递。

当示例代码执行到 N << temp 的时候,JS 会自动调用 def.js 内定义的valueOf方法,实现继承。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
deferred.valueOf = function(){
var Superclass = deferred._super;

// inherit from superclass
Subclass.prototype = Superclass.prototype;
var proto = Klass.prototype = new Subclass;
// reference base and superclass
Klass._class = Klass;
Klass._super = Superclass;

// enforce the constructor to be what we expect
proto.constructor = Klass;
// to call original methods in the superclass
proto._super = base;
// set properties
deferred(deferred._props);
};

最后

也没什么别的,多读读代码总有好处。欢迎大家推荐类似短小精悍的代码库:)

一直心心念念要开的博客终于要开始了。

开博的想法一直像荒草在心里漫漫地长着,没人管,也没人锄。这就是拖延症吧。总有各种各样的理由不去开始,比如要选一个高大上的博客系统啦,或是要自己搭一个博客系统啦,或是先把一个项目写起来啦…… (不只开博,在做很多事情时我都会设置过于复杂、艰难的前置条件,比如开始一个项目,要先把相关的书 读一遍)一天天,一周周,一月月,手边总有事情忙,博客总开不了。

8月16日,我参加了 Rails Girls 上海站活动,当教练。学员夸我讲得好,蛮有成就感的。活动过后,刘虓震博士推荐了一本书《成功,动机与目标》,说的是“提升行动力,达成目标,并且享受过程”。活动结束第二天,我就买了一本。

一本被书名毁了的好书。但这并非作者之误,只因我们的时代给“成功”添加了太多原本不属于它的含义。“成功”在字典中的定义是:达成想要的结果。本书基于心理学研究,阐述如何提高行动力,达成目标,并且享受过程。

说起买书读书,也是泪。搁在我书架上的很多书,我都没有读完。这本200页出头的小书,我在三个礼拜后的今天,才看了一半。我原本是打算在地铁上读的,总有各种情况阻止我:人太挤拿不出书;看到的好内容手边没有笔记记录;手机打发了一半行程,这时候拿出书来看不了多久;别人会不会觉得我装逼……要不平时看吧?正襟危坐总可以了吧。可我要看的书很多啊,提高技能的专业书怎么也更急迫吧。我还要刷 leetcode 呢。可见成功(以下描述“成功”时均忽略其世俗意义,用其“达成目标”的意义)路上有多少阻碍啊。

不管怎样,我周六下午抽了2个小时把后半部分看完了。趁着这个劲头,我写个读后感,画个脑图,也算这本书没白读。不知道这是不是我又一个强迫的完美主义范例。

先给成功下个定义:成功就是个人或团体通过行动达成目标。这里有三个要素:

  • 人:成功是需要主语的,或者是个人,比如“我”成功减肥;或者是团体,比如“暴走漫画”成功上市。
  • 目标:我们需要一个成功的标的物,比如减肥或上市。
  • 行动:考虑到可操作性,躺着数钱不列入考虑。心酸。

《成功,动机与目标》对上面三个要素进行了分析,通过心理学实验得出了一些结论,我们可以运用这些结论,提高我们的成功概率。

人的因素包括:

  • 对自我的认知。要成功,首先要相信自己能够达成目标。相信能力是可以变化的,做“渐变论”而不是“实体论”者。小时候的我有一种认知,我是天生骨架大,这么胖是天生的,基因的,喝水都会胖的。我对自己运动能力的否定也是我越来越胖的一个原因。前两年我开始慢跑,相信我糟糕的心肺功能是可以改善的,这种可以改变的信念一次次把我推上跑步机,我也确实从3公里喘得不行进步到完成两次半程马拉松。(不要问我为什么还是那么胖)
  • 自制力的强弱。自制力是让减肥者远离蛋糕,学习者远离微博的盾牌。自制力和肌肉一样,是可以锻炼的;是会在使用过后疲乏的。了解到这一点,我们应该注意锻炼我们的自制力,不用它,它会萎缩,锻炼它,它会强健。我们还要注意放松休息,自制力耗尽的时候,我们容易掉入陷阱,被诱惑所魅。有趣的是,吃点甜食让血糖提高可以恢复自制力。
  • 潜意识。把行动变为习惯,用潜意识的力量。潜意识不会消耗我们稀缺的意志力。固定时间运动,背单词,看书刷题会慢慢养成习惯。
  • 榜样和环境。善于运用榜样和环境的力量。把美女帅哥的照片贴满屋会让触发潜意识对身材的追求。
  • 表现还是进步。你是注重表现,即和他人能力或成绩的比较,还是注重进步,即自身能力的增长。在目标简单的时候,表现型的心态会让你获得更好地成绩,而目标复杂困难的时候,注重表现会让人容易消沉而过早放弃。聚焦进步能让人更享受过程,更专注,更投入,更能抵抗失败的挫折感,这往往能带来更好的结果。
  • 乐观还是悲观。乐观是进取型的,他们更追求获得。悲观则强调防御,不愿意失去。根据目标调整心态是获得更好结果的有效策略。
  • 基本需求。胜任力,关联感和自主权。在确定目标和开展行动时要关注对人基本需求的满足。

目标

取乎其上,得乎其中;取乎其中,得乎其下;取乎其下,则无所得矣

目标的制定需要注意:

  • 目标的方向。不是所有的目标都能带来长久地快乐。目标是不是满足人的基本需求,关联感(帮助别人、建立人际,回报社会),胜任力(提高自身能力,经验)和自主权(自己的追求是动力的源泉)。- 为什么 or 是什么。 “为什么”是抽象的,有助于提高抗诱惑力,让人冲劲满满。“是什么”是具体的,有助于提高行动力,让人专注具体操作。
  • 前途是光明的,道路是曲折的。既要积极地相信目标能够实现,也不能忽略前进路上的拦路虎,过分乐观,或者说不基于事实的乐观往往导致失败。运用心理对照法则,写一条成功的情景,写一条路上的障碍,有助于启动自身的动力系统。
  • 具体而可衡量。减肥10斤比减一点肥要好。用 React 写一个网页,比掌握 React 要好。
  • 略困难而可达到。

行动

不积跬步无以至千里

  • 简单而有效地计划。有没有计划的差距很大。使用“如果……就……”的计划能让给潜意识上一个闹钟,提醒行动,避免错过当下地机会。计划要具体,包括时间和地点。对可能的诱惑和障碍也要制订计划:“如果感觉饿,就吃一片干果”。
  • 抓住当下。敏捷行动,有效地利用碎片时间。
  • 拿起盾牌。拒绝干扰和诱惑,拒绝相互矛盾的目标。这些会分散注意力,削弱自制力。
  • 养成习惯。利用潜意识的力量。
  • 了解现状。书读到几页了,有几本书在读。今天多少斤,离目标有多远。不了解现状就对不准方向,迈不开步子。
  • 对失败负责,对自身诚实。从自身找原因,从策略找原因。对过程评价而不是对能力评价。失败时不要质疑能力,要怪自己不努力。而成功了也不要表扬能力而是赞扬过程。对能力评价容易让人感到能力是固定的,要提醒能力是可以变化的,只要方向对,够努力。