笔记:Ruby设计模式

摘要

1.2 模式的模式

四人组设计模式可以总结为以下四点:

  1. 把变和不变的事物分开
  2. 针对接口编程,而不对实现编程
  3. 组合(Composition)优于继承(Inheritance)
  4. 委托、委托、委托

1.2.5 YAGNI(你不会用到它)

补充原则 YAGNI(你不会用到它)原则

简单的说:就是你不应该实现目前不需要的功能。

一个出色的系统是一个问题修正、设计需求变更、不断地采用新技术和最终不可避免被重写的朱磊情况下都能变化自如的系统。

如果你不确定你是否需要一个功能,那你就推迟该功能的而实现直到你需要他的时候。

你应该把经理华仔实现那些你绝对马上需要的东西上。

注:设计模式不应该成为死板的模仿

设计模式说的全是如何让你的系统更灵活,能顺利的被修改。但是对设计模式的何故地和一种致命的过度开发风格挂上了勾 ……

评论:要灵活的运用设计模式。而非生拉硬套,死板照搬。甚至牺牲代码本身的可读性,就为了实现模式不可取。 模式是为了灵活性服而非目的。

1.3 23种模式的14种概述

查看书 Page10

2 Ruby语法概述

2.1 交互ruby

  • 可以直接换行亦或是使用反斜杠换行
x = 10 +
20 + 30

x = 10 \
+ 10

2.4 Fixnums Bignums

长度合理总数是 Fixnums(31个字节)大数Bignum可以存储赵级数字。 他们直接可以无缝切换和计算

2 # Fixnum

2**437 # Bignum

2.8 true、false和nil

true是 TruleClass唯一实例 false是FalseClass唯一实例

&& 和 and 看起来一样可以替换。实际上 && (表达式粘合剂)比 and 优先级更高

2.10 循环

while 是 条件一开始为真,一直运行到 假 结束

until 是 条件一开始为假,一直运行到 真 结束

2.12 符号

表示一样东西,它相对数据来说更像是程序内部识别符的时候——使用符号

函数名、类名内部都是符号

2.15 正则

正则被包裹在 / / 之间

=~ 来测试一个表达式是否和给定字符配对,要么返回nil(不匹配),要么返回匹配第一个字符的索引

/old/ =~ 'the old man'  # 返回5 索引

2.18 我是谁

self 一个始终指向当前对象的引用。

2.19 继承、子类、超类

super

评论: super的行为比较特别

当一个方法调用 super 的时候,那好比说 “在我的父级类中找到跟我同名的方法,并且调用这个方法”。

所以在子类中 initialize中调用super实际上就是调用了父类的initialize方法。如果在超类中没有找到同名方法,Ruby会继续向上追溯这个继承树直到它找到同名方法,或者遍历继承树中所有的类然后报一个错误。

和许多面向对象语言不同的是,Ruby不会自动保证所有的父类中initialize都会被调用。 这就是说,Ruby把initialize方法当做普通方法来对待。比如你没有在子类的initialize调用super,他便不会初始化父类的 initialize,那么父类的initialize的逻辑也不会作用在继承树下的子类中。

2.21 模组(module)

注:超类就是父类,模组就是module模块

注:重点,查找方法的过程

当你在类中引入一个模组时,这个模块变成了类的一种特殊而秘密的超类。 虽然一个类只能有一个超类,但他可以引入人一多的模组。

当外部调用类的实例的一个方法时,Ruby会首先判定该方法是否直接定义在类中。如果是的话,那么这个方法就会被执行。

如果调用方法并非直接定义在类中,那么Ruby随后会查看所有被这个类引用的模组。Ruby会从最后引入的那个模组开始向上查询,如果Ruby没有在模组中发现所要调用的方法那么他会继续查找父类和父类引入的模组。

2.23 线程

线程使用Thread#join让线程加入工作

多线程带来的竞争问题,可以使用标准库 Monitor类来确保线程安全

@monitor = Monitor.new

@monitor.synchronize do
  // 单位时间内只运行一个线程
end

2.24 管理分散文件

require语句用来加载文件

  1. 自动追加.rb
  2. 识别文件是否已经被加载过

习惯上大家会在文件头部一次性加载文件。确保先加载再使用。

加载会运行模块代码。

3 模板方法变换算法

最简单的模板方法,如下图所示,约定行为接口。行为接口就是模板,可以被继承复用。 子类实现父类接口。

希望被子类实现的,可以在接口中 raise异常,子类不实现就会报错。这样模板类表现得像一个抽象类在工作。

接口里面也可以提供一些默认实现。这种也叫钩子方法。

class Report

  def do_a
  end

  def do_b
  end

end

模板方法的滥用:就是把很多细碎的操作化为N个接口,你必须实现N个接口才能正常工作,增加了复杂性。

模板方法的例子:WEBrick 预留了一个 run 方法给第三方实现,内部处理代码。

require 'webrick'

class HelloServer < WEBrick::GenericServer
  def run(sock)
    // do sth
  end
end

我们只需要继承类,实现一个方法,就可以获得自己的server

4 策略替换算法

模板方法的特点,建立在类继承之上,类继承意味着限制灵活性。你的子类总要包含父类,这是继承关系的本质。

一旦选定了一个父类,想要更改和替换就很麻烦。

弥补这种情况就是——委托。

4.1 委托

我们不为每种变换创建一个类,而是把整个变换部分从他自己的类中拆分出来并将其孤立。 把“算法提取出来放到一个独立的对象中”,的技巧成为策略模式。

所有的策略对象都做相同的目标,比如文中的例子是输出报告(可能有N个格式),所有的策略对象都精确地支持相同的接口(output_report方法)。

策略的使用者(也叫 环境类)可以将策略对象当做它内部的替换部分。使用哪个策略对象看起来都一样。


# 这里使用了模板模式,设计了公共类
class Formatter
  def output_report(title, text)
    raise 'Abstract method need implement'
  end
end

class HTMLFormatter < Formatter
  def output_report(title, text)
    puts "..."
    # 做一些具体渲染
  end
end

class PlainTextFormatter < Formatter
  def output_report(title, text)
    puts "..."
    # 做一些具体渲染
  end
end



# Report 是调用者,也就是上下文说的环境类
class Report
  def initialize(formatter)
    ....
    @formatter = formatter
  end

  def output_report
    @formatter.output_report()
  end
end

# HTMLFormatter、PlainTextFormatter 是具体的策略类
report1 = Report.new(HTMLFormatter.new)
report1.output_report

report2 = Report.new(PlainTextFormatter.new)
report2.output_report

4.2 策略和环境(使用策略类)中共享数据

使用策略对象,传参有两种风格。

如果是具体参数,弱点是可能存在大量的数据在环境对象、策略对象之间传递,你无法保证每个都会被利用到。

第二种方法是,让环境对象把自己的应用作为参数传递给策略对象。策略对象可以通过环境对象的方法来获得所需要的数据。但是这个增加了耦合性。

@formatter.output_report(title, subtitle, content ....)

@formatter.output_report(self)

4.3 再说鸭子类型

以上面的例子为例,我们的策略包含了:抽象类 Formatter 基类和他的两个子类 HTMLFormatter、PlainTextFormatter。

然而这是一个非Ruby的实现方案,因为Formatter实际上不做任何事情,他的存在仅仅为了定义两个格式化子类的接口。

如果从是否工作正常的角度来看,这个实现没有问题。然而这类的代码违背了鸭子类型精神。

根据鸭子类型的精神,HTMLFormatter、PlainTextFormatter已经共享了相同的接口,因为他们都实现了output_report 方法。所以没有必要人为的创建一个实际上什么都不做的类。

所以可以在上面的实现上,直接去掉 抽象类。


class HTMLFormatter
  def output_report(title, text)
    puts "..."
    # 做一些具体渲染
  end
end

class PlainTextFormatter
  def output_report(title, text)
    puts "..."
    # 做一些具体渲染
  end
end



# Report 是调用者,也就是上下文说的环境类
class Report
  def initialize(formatter)
    ....
    @formatter = formatter
  end

  def output_report
    @formatter.output_report()
  end
end

# HTMLFormatter、PlainTextFormatter 是具体的策略类
report1 = Report.new(HTMLFormatter.new)
report1.output_report

report2 = Report.new(PlainTextFormatter.new)
report2.output_report

两个版本都可以工作,Ruby世界会毫无疑问的支持去掉 Formatter抽象类。

4.4 Proc和代码块

其他用来描述Proc的术语是 closure和lambda

Proc对象和方法具有许多相同的点。例如Proc和方法都绑定代码,并且都返回一个值。

Proc返回的是最后一个表达式的值,不论Proc返回什么都在 .call被调用时候返回。

Proc对象有个极其有用的特性就是会获得被创建时候的环境状态。

name = 'John'

proc = Proc.new do
  name = 'Mary'
end

proc.call
puts name # Mary

一般用 do~end创建多行 Proc,大括号创建单行。

实际上在Ruby表达式中,{} 的优先级比 do~end更高,本质上他是表达式粘合剂。 Page64注脚

4.5 快速而随性的策略对象

前面谈了很多Proc,那么它和策略模式有什么关系呢?

简单说,你可以将策略对象看做是一群被包裹在对象中并知道自己要干什么(比如格式化文字)的可执行代码。

这听上去很耳熟,因为这就是一个很好地对于Proc的描述。

使用Proc重铸策略模式。


class Report
  def initialize(&formatter)
    ....
    @formatter = formatter
  end

  def output_report
    @formatter.call(self)
  end
end

report1 = Report.new do |context|
  puts "..."
  # do HTML Formatter Job
end
report1.output_report

这种方式,我们不需要为处理格式建立新的类。

通过代码块,我们讲一个策略模式需要一个环境、一个策略基类、各种具体策略类和相关实例等缩减到仅仅需要一些代码块。

那么所有这些是不是意味着我们要忘记基于类的策略对象呢?并非有如此,基于代码块的策略对象仅适用于简单的——单方法接口。

毕竟我们能够调用Proc对象的唯一方法就是call方法,如果你需要策略对象中调用多个方法,你不得不去创建一些类。如果你只需要调用简单的策略,代码块则是毫无疑问的选择。

4.6 使用和滥用策略模式

策略模式最容易滥用的情况是将环境对象、策略对象的接口弄错。

你要做的是将完整一致的、可独立的工作转移出环境对象,然后用策略对象来代理他。还要关注他们的耦合。如果你的环境对象和第一个策略对象过于耦合,而无法在设计中推出第二个、第三个策略对象,那么你的策略模式显然是误用了。

4.7 策略模式的实际使用

rdoc中可以吧文档生成不同的格式 HTML、CHM、XML、Text

sort方法通过传入排序策略来排序

4.8 总结

策略模式是一个用于解决跟 模板方法模式处理问题相似但是基于代理的方法。


1~7 章有感

编程的解耦的方式是通过接口之间的合作。设计的艺术性侧重点在接口这边。

由此来看针对接口编程是一个贯穿始终的事情。

面向对象是接口代表了自己的行为,和其他对象共同合作的行为,是模型之间能够通信的基础。

Ruby的接口丰富,JavaScript的DOM等也是通过丰富接口控制等……这种体会更深了。

8 命令模式

Proc就是匿名函数,任何简单的东西都可以额外添加一个匿名函数的block提供支持。 Proc能应付的事情就是他非常的简单。

接口编程 接口编程的思想就是一种以接口为稳定媒介,提供一种控制的反转。他可以说是一切模式的基础了。 (内在的逻辑是程序必须提供一种调用,而接口就是维持这种稳定的调用。)

实现的思想就是,把不变的的东西当做基类,把变化的东西独立成工作类,把变化的逻辑封装在接口里。基类和工作类通过约定的接口名来进行互相调用。(基类初始化时候传入 工作类作为子实例对象,接口方法内调用工作对象的约定方法)。这里Proc可以锦上添花。

迭代器就是提供了一个迭代接口。 命令模式是提供命令接口。 发布订阅是,依赖发布接口、订阅接口来混合工作。 组合模式是 树节点、叶子结点提供相同接口实现一种递归一致性处理。

一切都为围绕着约定的接口来调用。这是一种编码解耦基础思想。

命令模式和观察者模式的区别:
命令对象确切知道如何做一些事情,但是对执行它的那个对象号不感兴趣。

观察器却对调用它的那个对象的状态感兴趣。

这是两个模式的根本区别。

9 适配器模式

9.4 修改能力摘要

Ruby最大的特点是,你几乎可以在任何时间修改类。修改类的能力是隐藏在Ruby强大的灵活性背后的秘密。使用这种强大的能力要对它负责。

9.5 单例类出现

大多数Ruby对象不但具有一个常规的类,而且还有第二个或多或少隐秘的类。 单例类实际上会被 常规类先执行到,所以单例类定义方法会重载标准类定义中的同名方法。

Ruby的打开类、单例类提供的能力可以重载方法,可以完成适配器模式。 这里选择打开类还是适配器,取决于你做事的复杂度。对于不熟悉不清楚原理,安全起见请使用适配器模式。

修改类要谨慎。

工程设计的目的就是公平交易。提供了封装的方便性,就会增加复杂性。直接修改类可能让工作变得简单,不过产生的情况需要自行判断和思考。

9.7 实际例子

ActiveRecord通过适配器模式匹配不同的数据库驱动。

9.8 总结

设计模式并不只是代码,意图才是关键。 代理模式、装饰器模式其实和适配器模式很像,都是一个对象在另一个对象中充当替身。但是不重要,意图才是关键。当你在接口不匹配中挣扎的时候,那个适配器才是真正的适配器。


10 代理模式

控制对象访问权限、提供和位置无关的对象访问路径和延迟对象的创建等这些情况,都有一个共通的解决方案:使用代理模式。

10.1 心理模型

代理模式其实是一个善意的谎言。放对方向你请求对象,返回的其实是一个替身。四人组将这种冒充对象称为代理。

代理的内部隐藏着一个指向真实的对象的引用

10.5 代理模式的苦差事

代理模式的苦差事就是要重复去实现一个已经存在的方法。如果有100多个方法,朴素的就是去实现100多个方法的代理。

10.5.1 消息传递和方法

不同于静态语言的调用方法,Ruby里面的方法更适合看做是 “消息”。

10.5.2 method_missing

method_missing 第一个参数是 符号,那个不存在的方法的名字。随后的参数是原始参数。


class Test
  #...
  def method_missing(name, *args)
    # do sth
  end
end

行为上这个和Smalltalk一致,只是smalltalk的名字叫做 doesNotUnderstand

10.5.4 无痛的代理


class Proxy
  def initialize(subject)
    # ...
    @subject = subject
  end

  def method_missing(name, *args)
    @subject.send(name, *args)
  end
end

10.6 使用和滥用

创建代理,需要注意的是当你使用了 method_missing 别忘了那些从 Object 继承下来的方法。他们可能会被优先调用。

随笔:这边 《Ruby元编程(2rd)》 里面提到过使用白板类类避免这种情况。

同时这样做也会慢很多。

11 装饰器模式

个人觉得 完整角度,Ruby的装饰器无法实现的很优雅。

主观的概括下这个关键点

装饰器要做的事情,其实存在很多替代行为。

假设我们的任务是想给 A函数(比如打印一行记录)增加行为,增加 log、统计字符、增加前缀……

我可以选择继承。

使用继承意味着每个单独的行为会产生很多父类,而Ruby是单继承。继承关系更应该留给那些稳定的、更本质的存在。

一个ruby朴素的实现 Dectorator 这里考虑了 装饰器复用。使用继承实现。

 class WriterDecorator
  def initialize(real_writer)
    @real_writer = real_writer
  end

  def write_line(line)
    @real_writer.write_line(line)
  end
 end

 class NumberingWirter < WriterDecorator

  def initialize(real_writer)
    super(real_writer)
    @line_number = 1
  end

  def write_line(line)
    @real_writer.write_line("#{@line_number} : #{line}") # 额外做的事情
    @line_number += 1
  end
 end


writer = NumberingWriter.new(SimpleWriter.new('final.txt'))
writer.write_line('hello')

上述的方法不够Ruby,因为充满了大量的没什么实际作用的样板代码。

优化的方式,method_missing 是一种方式,可以简化一些。

forwardable 模块可以帮我们自动生成代理方法。

require 'forwardable'

class WriterDecorator
  extend Forwardable

  def_delegators :@real_writer, :write_line

  def initialize(real_writer)
    @real_writer = real_writer
  end

end

def_delegators 接受两个或者更多参数,第一个是实例属性的名字,后面是一个或者多个方法的名字,每个方法都会被传递到代理对象上去。

forwardable 是比 method_missing 更精确的武器。

11.4 实现装饰器的另一个方法 alias


w = SimpleWriter.new('out')

# 要注意 w 拥有 write_line 原始方法
# 下面要打开实例,开始对他的方法进行编辑
class << w
  alias old_write_line  write_line

  def write_line(line)
    old_write_line("#{Time.new}: #{line}")
  end

end

alias 关键字为现有的方法添加一个新名字。我们给原始的方法创建了一个新名字。可以通过 old_write_line 或者 write_line 来调用这个方法。然后我们重写这个方法。但是 old_write_line 依然指向旧方法。

这种冒充、并且包裹,也完成了装饰器模式。

缺点,冒充的缺点就是你冒着名字冲突的危险。确保你的代码规模,可以不冲突的进行声明。 这种方式,你也无法简单的 un-include 装饰器。

还有extend方法,觉得不太有代表性,不记录了。详细查看 Page155~156

11.5 使用和滥用

装饰器必须要简单清晰,而且不要出现冲突。

潜在的缺点是长装饰器链路带来的性能问题。N个装饰器意味着N次交接。

使用别名技巧来装饰对象,会让代码调试困难。并且 un-include 需要改代码。

11.6 实际应用

ActiveSupport 提供了一个别名装饰器的工具集合。 alias_method_chain 方法。

12 单例模式

Ruby类方法/变量:许多编程语言(C++ 和 Java)将类级别的方法和变量成为 static方法和变量。虽然陈虎不同,但是很相似。

12.2.1 类变量

ruby的类变量是 @@

12.2.2 类方法

类中,一个普通函数将会是 实例方法。

执行类的代码中 self 是类本身

class SomeClass
  def self.class_level_method
    # ...
  end

end

也可以通过类名定义

class SomeClass
  def SomeClass.class_level_method
    # ...
  end

end

self 的写法更有优势,尤其在移植的时候。

12.3 单例应用

使用类变量唯一性,让类变量生成自己的实例。

备注 如果有疑问为什么Sample被完整扫描之前,可以新建实例。这里有我的笔记讨论

这里把 new 换成私有方法的原因是,防止他再次可以new一个实例。禁止这个行为来保持全局单例。


class Sample
  @@instance = SimpleLogger.new

  def self.instance
    return @@instance
  end

  private_class_method :new

end

12.6.1 其他的方式实现

$sample = Sample.new 全局变量的问题是,谁都可以修改它。

SampleInstance = Sample.new 可以发出警告提示,改善一点点。

12.6.2 使用类作为单例

每一个类是唯一的,既然可以在类上定义方法,那么可以类本身作为单例。

class Sample
  @@info = "xxx"
  def self.info
    @@info
  end

end

Sample.info

优点是,可以确保没有人可以创建第二个实例。 缺点是,惰性初始化这个缺乏控制。还有一些人对编写类方法不适应。

12.6.3 使用module作为单例

module Sample
  @@info = "xxx"
  def self.info
    @@info
  end

end

Sample.info

不能被实例化是他的有优点

12.7 singleton module

singleton class 可以帮助我们快速的实现单例模式。原理和我们前面手写的差不多。

require "singleton"

class Manger
  include Singleton

  def run
    puts "run"
  end
end


Manger.instance.run

12.8 使用和滥用

单例模式不应该变成全局变量。单例模式就是为了发生一次而建模的。

12.8.4 单例模式的测试

require "singleton"

class SimpleLogger
  # 在这个类中包含所有的日志功能
end


class SingletonLogger < SimpleLogger
  include Singleton
end

实际的代码使用SingletonLogger,而测试的时候使用SimpleLogger

12.9 实际应用

ActiveSupport的全局规则列表(单复数规则)

13 工厂模式

这里关注点在于,工厂模式是帮助区分和选择出类来工作。

14 生成器模式

这里和 工厂模式的区别是 工厂模式目的更加的关注——帮你选择正确的类。

生成器模式的关注点在,把大量的new工作放在一起,提供复用。

14.1 魔法生成器

使用 Rails的语言约定 —— 比如 find_name_and_age 可以处理字符串为 find _ name and age 提取出 find,name,age 配合 method_missing 来动态 send 方法。

动态性从而实现一种“大量”的生成工作。

15 解释器模式

有几种方式实现解析操作

  1. 正则表达式来实现解析
  2. UNIX YACC 来实现
  3. Ruby里面有 UNIX YACC的Ruby实现Racc
  4. 使用 yaml 解析库以YAML的形式来配置

解释器模式的核心是抽象语法树,将你的语言设计成一种表达式,把表达式分解为抽象语法树。得到AST就可以进行计算。

解析方式根据上面可以选择很多。

解释器程序在效率方面是一个问题,几乎不可能让 自建语言和原生语言执行效率一样快。但是解释器模式为我们提供了强大的功能和灵活性。他并不适用与程序中百分之二的性能密集型代码,但是对于另外百分十九十八的代码而言可能是最好的选择。

解释器模式特别适用于问题范围被详细界定的情况,比如数据库查询或者配置语言。这个模式同样也适用于合并现成的功能代码块。

解释器模式提供了强大的灵活性和拓展新,通过构建不同的AST,你可以让同一个解释器执行不同的事情。你通常可以通过向AST添加节点的方法直接扩展你的语言。解释器要比直接代码模式执行来的慢,要提高他们的执行速度是很困难的,所以应该避免需要高性能情况下使用解释器模式。

Parser拓展

首先来科普一下。所谓 parser,一般是指把某种格式的文本(字符串)转换成某种数据结构的过程。最常见的 parser,是把程序文本转换成编译器内部的一种叫做“抽象语法树”(AST)的数据结构。也有简单一些的 parser,用于处理 CSV,JSON,XML 之类的格式。

—— 对 Parser 的误解


第三部分 Ruby的设计模式

16 DSL模式

可以采用的方式:

  1. 正则表达式
  2. 分析器生成工具 (称为 外部DSL)
  3. 手动的编写一个传统的分析器,逐个读单词
  4. 使用Ruby的方法调用

16.2 ~ 16.5

单个运行的DSL将方法绑定到 全局(Object),然后


def do_sth
  # 绑定到全局
end

# 主要就是把文本当做 ruby代码读取执行
# 这里的 eval是在全局作用域中生效
eval(File.read('your_script.pr')) # 也可以用load?我没找到

# 这里也可以用工作实例  XXX.instance.run

上面的例子仅仅记录的是一种方法。 仅仅适合单个实例。

也就是你的代码是在有一个上下文中执行。举个例子,这里是复制目录,这里的行为只能指定一个目录,如果我想创建2套工作实例?


class Backup
  #....

  def initialize
    @data_source = []
    # ....
    yield(self) if block_given?
    XXX.instance.run

  end

end

然后我们的 DSL可以这样写


Backup.new do |b|
  b.backup '/aaa/nbb'
end

Backup.new do |b|
  b.backup '/aaa/nbb'
end

# ... 这样可以创建多个实例

这里主要就是 给 block 传递了 self 指针。


#....
  def run
    threads = []
    @backups.each do |backup|
      threads << Thread.new (backup.run)
    end

    threads.each { |t| t.join}
  end

#....

eval(File.read('your_script.pr')
XXX.instance.run

也可以配合多线程执行IO密集型

16.8 使用和滥用

DSL存在报错信息问题,本质上对编程一无所知的用户使用的是Ruby本身,而来自于 用户层面上编写错误会报错一个过程中的错误。 这对于用户将会是摸不着头脑。所以在执行处捕获友好报错十分重要。

当安全性是必要因素的时候,请远离内部DSL。整个DSL的概念在你的程序中是读取并执行他人编写的代码,这就需要你对用户有足够的信任。

注: 写到这里,其实Ruby的 DSL是借助 Ruby内部语法的优势,让人们书写代码。他和 解释器模式的自造 Parser其实有本质区别。并且Ruby的DSL形式可以认为是固定的了。从经验上也是如此 Sinatra、Rails、Rake的DSL写法都类似。 而DSL的精髓就在于使用 eval,eval还有很多兄弟元素,使用他们就是在 合适的作用域内,使用合适的eval。eval会把你的外部代码,放在当前的作用域内部执行。

17 元编程

注: TODO 我的困扰 元编程,灵活的思考元编程。这里主要困扰是,搜索常量,变量的顺序,需要再次确认。看到一个变量,既可以作为变量也可以作为方法。那么如何搜寻。self这种会绑定搜寻么

元编程其实是一种能力,在Ruby中类、方法、对象都是可以在执行的过程中被修改的。

元编程侧重在,我们可以根据一些规则(后面的“约定大于配置”),我们根据一个名字的关键字,并以此为推导,生成周围符合约定的类、方法等。比如


# 关键字是 book

def book_model

end

def books_controller

end

# .....

而这些工作,可以通过动态的去生成。生成的方式有很多种。比如

17.1 给对象单独定制方法

# 动态定义类方法

plant = Object.new

if stem_type == :fleshy
  def plant.stem
    'fleshy'
  end
end

17.2 通过模块自定义对象对象和模块

module Food
  def diet
    'food'
  end
end

module Fruit
  def diet
    'fruit'
  end
end

#....


myfood = Object.new

if xx_type == :food
  myfood.extend(Food)
end

通过 extend在运行时方便修改一个对象

17.3 完全创建新方法



class CompositeBase
  
  def self.member_of(composite_name)
    # %Q 的写法  https://ruby-china.org/topics/18512
    code = %Q(
      attr_accessor :parent_#{composite_name}
    )

    class_eval(code)
  end

  def self.composite_of(composite_name)

    code = %Q(
      def sub_#{composite_name}s
        @sub_#{composite_name}s = [] unless @sub_#{composite_name}s
        @sub_#{composite_name}s
      end

      #.....
    )

  end

end


CompositeBase的子类不会自动继承任何组合模式的行为(mixin)但是他们会继承 member_of,composite_of。

17.4 自省


# 公开方法列表
pub_m = object.public_methdos

pub_m.include?("method_name")

# 也可以用

object.response_to?("method_name")

这种功能叫反省(自省)。在任何时候都有用。当你越来越深入使用元编程后,你的对象会越来越多的依赖他们的历史而不是他们的类。从而你会发现这些自行功能的真正价值。

17.5 使用和滥用

元编程像是一把锋利的刀,只有在必要的时候才将他掏出来。

元编程的关键是,你编写的程序在他们运行时调整和修改他们自身。元编程被使用的越多,你的运行程序就越不像你所编写的源代码。这是这个编程模式的意义所在,也是危险所在。

调试普通代码已经不是一件容易的事情,调试元编程所产生的短暂的对象则更困难。

完整的单元测试是程序能正常工作的关键,如果使用了大量的元编程,那么单元测试绝对必不可少。

这个设计模式的主要危险在于功能之间未预测的互动。

必要的在元编程中显式的抛出异常,虽然不完美,但是总要好过无法发现或者被遗忘。

17.6 实际例子

ActiveRecord 大量使用元编程。

17.7 总结

所谓元编程是指有时要得到你所需要的功能代码最便捷的方法不是肢解编码,而是以程序的方式在运行时自动生成。

通过Ruby的动态功能,我们可以以一个简单的对象开始,逐渐向它加入独立的方法或者甚至是充满许多方法的整个module,不仅如此,通过使用 class_eval 我们可以在运行时生成整个module,最后我们可以用Ruby的自省工具提供的优越的功能来让程序改动之前,先检测他自己的结构。

18 约定大于配置

配置是有害的,有Java的前车之鉴。大量的配置会让一切变得非常麻烦。

实际开发中,很少会使用全部的配置。大多数情况下,名字管理的URL非常稳定、handler也很稳定……约定大于配置的宗旨就是减少配置文件的负担,保留应用程序和框架的基本扩展性同事去掉无休止的细节的配置信息。

(在配合上元编程)

约定大于配置,定义一个普通工程师可能会应用的惯例,然后就一直遵循这个惯例(存放目录、名字约定、鉴权的方式等等)。

实际例子:

Rails中 Restful API 是一中约定, MVC的文件名称是一种约定。

Gem中目录结构也是一种约定。

18.10 总会

约定优于配置认为。有时候你能通过使用基于类名、方法名、文件名和一个标准目录的解构管理来将代码组合起来,从而构成一个易于使用的系统。如此一来,你的程序更容易被拓展。你可以通过简单的添加正确命名的文件、类、或者方法来拓展系统。

约定优于配置、DSL、元编程 这三个Ruby特定的设计模式,采用了Ruby语言的灵活性。很大程序上依赖于运行时的代码计算。它和元编程一样要求相当高层面的程序自省。

这三个模式还有一个共同点,都是解决编程问题。

他们的共同宗旨:你不应该局限于所看到的编程语言,而是应当将它塑造成更直接地帮助你解决问题的工具。


设计模式一览

  • 01.模板方法模式:提供样板接口
  • 02.策略模式:委托出去,实例化不同子类,对接口有不同实现从而满足策略
  • 03.观察器模式:关心发布接口、订阅接口。
  • 04.组合模式:树节点、叶子结点提供相当接口,实现一种递归一致性处理。
  • 05.迭代器模式:提供了一个迭代接口、block
  • 06.命令模式:依赖命令接口来工作。
  • 07.适配器模式:作为中间修改和调整接口。
  • 08.代理模式:使用代理对象,做传递,做一些控制性的事情。method_missing可以提供帮助
  • 09.装饰器模式:分离任务,类似 f(g(z(x))), forwardable, alias ,朴素手写各有千秋
  • 10.单例模式:永远只有一个实例生成,朴素手写,禁用new、使用天然的class、还有 module singleton
  • 11.工厂模式:倾向于在class内部帮你组合,选择合适的子类
  • 12.生成器模式:在这里组装复杂的过程
  • 13.解释器模式:自建parser构建ast来实现一个解决问题的新语言

Ruby设计模式

  • 14.DSL模式:使用Ruby的语言特性,配合 eval家族、extend、动态定义类方法 实现执行外部书写代码
  • 15.元编程模式:根据名字生成方法,涉及到 eval家族、extend、动态定义 还有配合上 自省、单测,过程历史很重要。
  • 16.约定大于配置:使用约定简化配置,类名、方法名、文件名、目录配置 等,配合元编程。形成 逻辑+架构级别的拓展。

Callback前后呼应

四人组设计模式可以总结为以下四点:

  1. 把变和不变的事物分开
  2. 针对接口编程,而不对实现编程
  3. 组合(Composition)优于继承(Inheritance)
  4. 委托、委托、委托

作者新增

  1. YAGNI(你不会用到它)

一句话

一切的基础

  • 编程的解耦方式是通过接口之间的合作
  • 以接口作为稳定的媒介,提供一种控制反转,是一切模式的基础。
  • 实现的思想是,把不变的和变化的分离,不变的作为基类,变化的封装在接口里。基类和工作类通过接口名来调用。Proc可以对轻量级任务锦上添花。

模式不是一个编码形态,而是意图。


我的大总结

RUBY其实是很”脏”的语言。动态性是一把双刃剑,一面面向问题,另一面就对着自己。必须用自测保持正确性。用自省确认它当下的状态。要不然完全就是一个作死的状态。所有设计模式都在聊很细节的优化,唯有”约定大于配置” 这个RAILS带来的模式,既在聊代码也在聊架构。这是一个宏观的模式思想。

Mark24

Everything can Mix.