笔记:面向对象设计实践指南Ruby语言描述

1.2.1 设计原则

RobertMartin 提出 SOLID 原则:

  • Sing Responsibility Principle, SRB (单一职责原则)
  • Open-Closed Principle, OCP (开闭原则)
  • Liskov Substitution Principle , LSP (里氏替换原则)
  • Interface Segregation Principle,ISP (接口隔离原则)
  • Dependency Inversion Principle,DIP (依赖倒置原则)

Andy Hunt 和 Dave Thomas 提出:

  • Don’t Repeat Yourself, DRY (不重复原则)

美国东北大学(Northeastern University)迪米特项目:

  • Law of Demeter, LoD (迪米特法则)

1.2.1 设计模式

Gang of Four, Gof(四人组):

Erich Gamma、Richard Helm、Ralph Johnson、Jov Vlissides 与1995年 一起编写了 《设计模式》

摘要

当设计行为与编程行为分开时,所开发的那个面向对象软件也注定失败。设计是一个逐步发现的过错,他依赖往复不断的反馈。这种反馈应该是适时和递增的。

2.1.1 设计具有单一职责的类

设计是保留可变性的艺术,而非达到完美性的行为。

2.1.2 组织代码以便于更改

代码“易于更改”定义成下面几条

  • 更改不会产生意想不到的副作用
  • 需求的轻微变化对代码的更改要求也相应较小
  • 现有的代码易于重用
  • 最简单的更改方式是添加其自身也易于更改的代码

编写的代码应该具有如下几个特点

  1. 透明性(Transparent): 在所更改的代码,以及在远处依赖它的代码里,更改所产生的后果应该显而易见
  2. 合理性(Reasonable): 任何更改所产生的成本都应该与更改所要带来的效益成正比
  3. 可用性(Usable): 现有代码在新的环境和意想不到的环境里都可以使用
  4. 典范性(Exemplary): 代码本身应该鼓励那些为延续这些特点而对它进行的更改

简写为 TRUE 特点代码

TRUE 特点代码的第一步是要保证每个类都只有一种单一的、定义明确的职责。

2.2.1 示例程序: 自行车和齿轮

个人评论

2.2.1 的末尾那段我非常的赞同了。就是组织代码如何确认是最好的?

像 initialize 需要四个参数值这种,如果他未来不变化了,形式完全ok。如果未来这部分要发生变化,那么我们就要做点事情,让未来的变化易于更改。 固定参数的方式可能就不好了。因为所有引用 初始化的地方,都要同步更改。

2.2.2 为何单一职责原则很重要

易于更改的程序,应该有易于重用的类构成。 拥有多重职责的类难以重用,内部纠缠不清。 单一职责的类像积木。 依赖过多的职责类,会增加程序被意外破坏的概率。

2.2.3 确定一个类是否具有单一职责

有一种方法是假设它存在意识,然后质询它,将它的每个方法都改述为一个问题,那么提问应该会行之有效。

另一种彻底弄清楚某类实际上做什么的方法是:尝试一句话描述它。这件事描述起来应该很简单,如果你能想出最简单的描述使用了 “和” 这样一个字,那么这个类可能有多重职责。 如果使用了 “或” 那么这个类便具有了多重职责,只是他们的关系没那么紧密。

OO设计者使用了 “内聚”(cohesion) 一次来描述这个概念。当某个类的所有内容都与其中心目标相关联时,就可以说这个类属于 “高内聚”(highly cohesive)或者具有单一职责。

单一职责并不是要求一个类非常狭窄的只做一件事情,也不是要求这个类只为某种吹毛求疵的原因而进行修改。与之相反,当一个类是内聚的——这个类做的所有事情都与其目标高度相关。

2.3 编写拥抱变化的代码

有时候,你会写出很奇怪的类,他作为A不适合作为B也不适合,它还具有多重职责。除非给出更加明确的信息(未来需求),否则我们很难下决定。

尝试挖掘更多的信息,正面修改;或者衡量今日和未来的成本,决定是否修改。

还有一种就是,把他编写的可以拥抱变化。

2.3.1 要依赖行为,不依赖数据

1.隐藏实例变量

ruby里面 数据是永远在 实例内部的,只有通过方法才能获得。实际上 reader、wrtter 构造的方法,会把 变量转换为行为。 存在这个中间层,当数据发生变化,我们只需要重新的定义 方法 内部数据处理,从而避免了四处修改引用他的逻辑。

评论 这是ruby刻意的设计。相比来说,Python和JavaScript都可以直接引用,这点确实差了一点思考。

2.隐藏数据结构

最糟糕的事情是依赖一个复杂的数据结构。比如 一个类中 实例变量 @data 他的值是一个嵌套数组,你需要知道 [0] [1] 具体的位置才能使用它。并且所有内部方法,凡是使用到 @data 都必须要关心这个。一旦 @data 发生变化,对于其他引用方法是灾难性的。

评论

参考 Page 24 页。

主要原理是 使用自定义类,或者Ruby的 Struct 的解构把数据转化为具有接口属性的语义化解构。

引用的地方只要关心这组数据是 可迭代的,响应 2种方法即可。

这是一种把数据本身给接口语义化的例子

这也不是说,Struct 之类的就是解药了。

主要的目标是,我们通过某种方式隐藏这种混乱的数据结构。这种方式根据情况而定。

2.3.2 全面推行单一职责原则

1.将额外的责任从方法里提取出来

与类一样,方法也应该具有单一职责

2.将类里的额外职责隔离起来

可以单独设置一个类。或者通过 Struct的方式简单的 创建一个类。

隔离可以让后面的修改变得安全和可具有复用。

这个类里面可以有必要的方法,当未来的场景出现,这个类可以单独声明,方法增加。

2.5 (单一职责)小结

将负责一件事的类,与程序的其余部分 隔离起来。这种隔离可支持无不良后果的更改和不进行复制的重用。

3 管理依赖关系

一个对象必须要知道其他对象的某种情况,这种 “知道” 便创建了一种以来关系。如果不仔细加以管理,那么这种依赖关系将会毁掉整个程序。

3.1.2 对象间的耦合

依赖关系就是耦合。每一个耦合都会创建一个依赖关系,两个类彼此“知道”的越多,他们耦合的就越紧密,他们的行为就越想一个单一实体。

A、B如果耦合(彼此以来),那么你要更改A,那么你就会发现也要更改B;如果你要复用A,B也会来凑热闹;如果你要测试A,你就得测试B。

程序往往都是第一次写完的时候,一切都很好。问题会一直被掩盖到另一个环境尝试使用或者更改B、或者A的时候。

3.2 编写松耦合的代码

3.2.1 注入依赖关系 —— 依赖注入(dependency injection)

Page 35

通过名字引入另一个类会创建一个主要的依赖点。这个依赖所产生的直接后果是,如果名字改变,那么两处的代码都要更改。

Page37

依赖注入,初始化的时候传入目标类/对象,保存成自身实例变量之一。自身的方法,访问这个变量,并且通过鸭子类型的方式工作,只认方法/接口。

依赖注入名气很大,实际上很简单,只需要调整下参数,不需要改动内部就可以实现解耦。

3.2.2 隔离依赖关系

不能移除的依赖关系,都把他们隔离在你的类里。

把所有的依赖关系都当成一种细菌,把他们隔离起来

  1. 隔离实例创建

1)初始initialize 中创建实例

可以创建,并且显示的注册了一种依赖关系。

2)延时创建

调用具体方法的时候   = 的方式创建,更加lazy一点

分析,然后把最脆弱的部分,用自己的方法创建,方法内部可能包含着外部依赖实例创建。

  1. 移除参数顺序依赖

顺序参数定义,意味着引用他的函数,也要关注参数顺序,参数顺序变化也会带来问题。

1) 使用散列表初始化参数

2) 显示定义默认值

A   default 但是如果想要显示获得 false、nil

fetch方法是可取的。

也可以内部创建一个方法提供默认值,内部对参数进行默认值 merge

  1. 隔离多参数初始化操作

顺序参数、字典参数创建过成无法避免,极其复杂。可以把他放在一个单独的方法、类中。

其实这是一种工厂模式的实践。

把复杂创建过程给隐藏掉。

3.3.2 选择依赖方向

直接依赖、依赖反转

评论 直接以来就是直接引用了名字、方法; 依赖反转就是通过传参的形式传入,内部通过约定接口来调用。 这是两个方向的选择。 到底如何选择控制依赖的方向。这里的关键在于,我们需要区分那些类更容易变化,然后排个序。做出选择。这里是一个平衡和评估的过程。

小结

书里为了描述清晰,引入了UML、图标、象限。就像数学过程一样,希望他变得严谨,给出了一些比较方法。

但是实际上这个过程,没那么复杂甚至相当简单,而且有限。我个人觉得是如此。

就单一职责而言,使用的东西也是极其有限的。

计算机的底层就是函数的执行栈,函数执行比如被明确的引用。引用就要使用方法。

而写出松耦合的代码,技巧就是:

  1. 隔离(用方法隐藏操作)
  2. 控制方向,直接引用、控制反转(传参依赖接口)

说了很多,但是实际上最关键的其实是——程序未来将会如何变化

实际上我们的程序也无法预知未来如何变化,所以我们要做的就是嗅探或者警觉的增加隔离。受到变化的冲击其实也无法避免,这也是工作的日常。

这只能说是个努力的方向。 而上面的思想方法和技巧,等于提前劳动,为未来减少劳动。

4 创建灵活接口

4.1 理解接口

评论 讨论的图例,给我一个灵感就是,减少一种无须的相互引用,即便可以如此,也要尽量让一切存在一种秩序。 这种灵感来自于 Redux框架理论,对前端混乱的数据流,定义了一种方向。 编程依然是一种对现实的建模活动,现实中,水管、电线都会实现一种一致性的排线,观感上可以寻找出秩序,而从预测,也是方便下次检查使用。这种思想其实都是一致的。 程序需要在完成任务的同时,尽可能的维护一种内在的秩序性。 而这种秩序性,其实就是我们追求的、大师拥有的、本书一直在聊的 关键思想。

4.2 定义接口

区分私有接口、公共接口。

公共接口就是对外的承诺,稳定的存在,希望外界访问,避免变化和联动修改。

私有接口,内部使用,可以自己更改实现。不会传染性的联动修改。

这里有个问题就是,区分出这两种接口,设计出来。

4.3 找出公共接口

草稿构建,实际上要先于测试、编码。想清楚再做。

时序图可以帮上忙。时序图可以描述 类之间调用关系。

(UML时序图)

4.3.4 请询问“要什么”,别告知“如何做”

8.5.3 选择关系

  1. 将继承用于“是什么”关系
  2. 将鸭子类型用于“表现的想什么”关系
  3. 将组合用于“有什么”关系

设计最划算的测试

编写可更改的代码是一门艺术,其实实践依赖于三种不同的技能。

第一种,你必须理解面向对象设计。从实用角度可变性是唯一重要的设计指标,易于更改的代码是需要精心设计的。

第二种,你必须要擅长代码的重构。重构指的是以这样一种方式更改软件系统的过程:他不会改变代码的外在行为,而只是改善其内部结构。

评论:重构其实是和外部设计是相辅相成的。

第三种,编写高价值测试的能力。测试可以不断给你重构的信心。

评论:这三种是互为支撑的,在敏捷的道路上。 让代码更加容易修改,重构就是让你的代码从一种设计变成另一种,而测试可以让你重构的时候不会受到“惩罚”。 敏捷,可以让这种开发周期围逼近真实的需求情况去交付。更快的完成目标。

9.1.2

测试如果事无巨细,甚至编写的成本巨高,那么也没意义了。

评论 所以编写测试的方法论,和上面的技巧并不冲突

测试本身也要注意:

  1. 测试关键的部分
  2. 依赖抽象
  3. 松耦合,低深度 的编写测试

评论

什么较艺术性呢?就是这些技巧也罢,理论也罢,都无法推向一个极端。比如全部测试、全部无差别极端解耦……

艺术性的特点就是要靠人根据实际情况进行平衡。

这些技巧终归是技巧罢了。看情况使用罢了。任何不合适的情况,极端的使用都会进入一个陷阱。

按照我以前的经验,很多的东西难以发挥出作用,就是因为按照一种信条无差别极端的推进,导致很快进入了一种滥用和恶化。

本质因为代码不论是业务还是测试他们都要产生容易改变的代码。而部分情况一味的强推,马上就会进入难以自处的漩涡。

所以设计的关键的不是知道了某种技巧,其实这些所谓的技巧都是完全可以自己发现的。

9.1.4 了解测试的方法

BDD(行为驱动)开发,侧重于有外向内

TDD (测试驱动)开发,侧重于由内向外

测试鸭子类型就是 测试中 response 抽象方法

对于父类测试,就是测试公共方法,测试继承父类的子类,对于mini_test 这种框架,也可以 include 父类的 Module

进行继承测试。

总结

总之,技巧方法需要灵活使用,自己不断的探索。

这类的书看多了逐渐发现不是一个固定的模式,而是灵活使用。 也不应该辩才一个固定的公式机械化的使用,而是灵活取舍,平衡优先。

他也不是非黑即白,贯彻一种绝对,或者一元论。而是要很辨证的根据具体的情况使用,善用其特点,避免极端化扩大缺点。


随笔

其实也就一句话,考虑实现,考虑容易更改,考虑容易测试容易更改。把他们都纳入考虑,然后写出来的代码都带有设计性。

具体的技巧无非就是:单一,隔离,控制反转,继承,组合,鸭子类型; 对应的测试也有 (测试鸭子类型,测试可以继承,测试可以组合)

Mark24

Everything can Mix.