Ruby 面向对象语言描述
March 12, 2016
面向对象设计实践指南Ruby语言描述
问题比答案更重要, 下面的章节中会关注与问题的提问,然后才是问题的解决方案
面向对象设计
- 设计的目的(要解决的问题)
- 应对变化
- 变化困难的原因(建立了太多的依赖关系,对周围的环境期望太多)
- 设计良好的大型程序必须由设计良好的小程序演变而来, 不存在设计错误小程序演变为设计良好的大型程序(当然大量的重构和重新设计也不无可能,然而存在的可能性太小)
- 实用的设计不回去预测未来将要发生什么
- 设计的目的时为了将来进行设计(应对变化), 首要目的: 降低变化所来成本
- 工程师的目的:权衡 现在设计、 现在不设计+将来改动成本,之间的权重比较
- 设计的工具
- solid原则(Single Responsibility、Open-Closed Principle、Liskov Substitution、Interface Segregation、 Dependency Inversion、 DRY)
- 设计模式
- 设计实践
- 设计失败的原因:没有足够的经验和设计反思(不懂设计、懂设计但是讲过多的设计套用)
- 设计的时机: 反复的应用,不进行大规模的预先设计,设计应该发生在项目的过程中,不断的迭代的一个过程
- 设计评判:1. 设计需要代价 2. 设计的盈亏点依赖于工程师(时间表、能力)
实践是检验真理的唯一标准, 实践会弄脏你的双手、是充满选择的。
面向对象设计问题是----------需求的变化。 面向对象设计本质是----------依赖关系的管理 警示:小的坏的程序不太可能形成好的大的程序(当然可以通过重构来实现, 但是不应该期待谁能重构它)
设计单一职责
- 如何确定一个类是否具有单一职责
将类的每个方法都改述为一个问题(例如:齿轮请问你的直径多大) 2. 尝试使用一句话描述方法做的事情,这件事情的描述应该很简单。而不是需要使用 “和” 这样的字眼。
- 解决问题的方法
- 依赖行为而不是数据(变量) // ruby中对这样的支持非常好, att_accessor 等元编程
- 隐藏数据结构
# 代码示例 class Example attr_reader :data # data 的数据结构应该为[[one, two], [three, four]] def initialize(data) @data = data end def sum data.inject(0) do |sum, item| sum += item[0] * item[1] end end end # 重构之后的代码 (这里使用了额外的类,来隔离具体的数据结构) class Example attr_read :data def initialize(data) @data = data.map do |item| Infer.new(item[0], item[1]) end end def sum @data.inject(0) &:product end end class Infer attr_reader :one, :two def initialize(one, two) @one, @two = one, two end def product one * two end end
- 将外在的依赖关系尽量的隔离开来
将经常重复的代码封装,将对外在的依赖尽量隔离在一个地方(建立单独的方法,统一隔离对外在的依赖关系,思想类似与 依赖方法而不是数据)
管理依赖关系
消息的三中类型, 1. 自身实现 2. 继承 3. 依赖(指代发送消息)
- 问题: 什么是依赖关系(个人认为3. 4 没有太多用处,虽然ruby中可以广泛的应用hash传递参数,但是依然没有避免参数的依赖)
- 另一个类的名字。 代表自身期待另一个类的存在
- 消息的名字。
- 消息所需要的参数。
- 参数的顺序
- 解决依赖关系的方法
- 注入依赖关系(依赖注入)
class Gear attr_reader :chainring, :cog, :rim, :tire def initialize(chainring, cog, rim, trie) @chaining = chainring @cog = cog @rim = rim @tire = tire end def gear_inches ratio * Wheel.new(rim, tire).diameter end end Gear.new(10, 10, 10, 10).gear_inches # 重构之后代码 class Gear attr_reader :chainring, :cog, :wheel def initialize(chainring, cog, wheel) @chainring = chanring @cog = cog @wheel = wheel end def get_inches ratio * wheel.diameter end end Gear.new(10, 10, Wheel.new(12, 10)).gear_inches
- 隔离脆弱的外部信息
def gear_inches ratio * wheel.diameter end def gear_inches // ...... wheel.diameter //...... end # 现在对wheel.diameter的引用嵌入在一个复杂的应用过程中, 这样做会变得更加脆弱 def gear_inches //..... diameter //..... end def diameter wheel.diameter end #移除依赖关系,并将其封装在自己的某个方法中
当一个类包含了对某个可能发生变化的消息的嵌入引用时,这样的技术变得非常有用。另一种方法为:将依赖关系反转(后面)
- 注入依赖关系(依赖注入)
- 为什么需要管理依赖关系:
- 依赖关系是可以被改变的(通过函数的参数改变)
- 依赖关系的方向的选择会对未来的变化产生影响
- 选择依赖方向:
- 有些类比其他类更容易发生变化
- 具体类比抽象类更容易发生变化(例如ruby中方法参数建立在抽象上,java建立在具体类上)
- 更改拥有多的依赖关系的类会造成广泛的影响
依赖于那些变化情况比你所做的更改还要少的事情
-
总结:
对依赖关系的管理时创建面向未来的应用程序的核心。
- 依赖注入 可以建立松耦合的依赖关系
- 依赖于抽象 可以降低面对变化的可能行(建立在更高层次的抽象上)
- 管理依赖的关键时管理依赖的方向(依赖于更不容易变化的对象)
创建灵活的接口(类里面的接口)
- 什么是接口
- 暴露了其主要职责
- 期望被其他对象调用
- 不容易改变
- 对其他依赖它的对象来说是安全的
- 在测试里面被详尽描述的
找出并定义公共接口是一种艺术,它呈现出一种设计挑战,因为这里没有现成的规则可以使用。并且很难从错误中学习
- 更好的找出接口
关注消息,而非领域对象。绘制时序图来明确消息的传递,并提问做什么,而不是如何做!
使用时序图,描述对象之间的消息,来确定类之间需要暴露的接口,从而创建更小的接口,更最小上下文的依赖关系。松耦合的设计。
总结:
应用程序有消息来定义而成, 而非类。而设计时的提问方式为 “我需要什么,谁来做” 而不是 “告诉接收者如何做”,
使用时序图来暴露消息。
使用鸭子类型技术降低成本
- 使用ruby的鸭子类型设计,可以创建出灵活、更结构化的设计,也可能创建出混乱不堪的设计
- 具体化与抽象化之间的取舍,是面向对象设计的基础内容
- 识别鸭子类型,kind_of? is_a? responds_to?
- 记录鸭子类型(做好测试)
通过继承获得行为
- 继承的核心为:自动的消息委托
- 建立继承的时机以及方法: 在需要第三种同种类型的时候(在拥有更多的抽象信息的时候), 应当尽力推迟继承的抽象。
- 继承抽象的方法: 从下往上, 先将方法下降至具体类型,抽象出更高层次的继承体系,不断的重构,将方法提升(将方法下降到具体层次的代价更高)
- 设计的决策(或者任何事情的): 1. 错误成本, 2: 实现成本。 抽象继承的方法采用 从下向上的原因在于从上往下的成本过高。
- 使用模板方法有助于解耦合父子类之间的关系,但是也仅仅限于父子两个层次之间的关系。(这样的解决子类的初始化问题,在于,Ruby的设计问题,不应该使用这样的方法给语言打补丁)