《对象健身操详解》
很久之前看的一篇文章,其中几条特别实用,分享给大家看看。
优秀设计背后的核心概念并不高深,七条评判代码质量原则就基本上能够涵盖,它们具体是:
-
内聚性
-
松耦合
-
零重复
-
封装
-
可测试性
-
可读性
-
单一职责
道理我都懂,用起来就不熟练了,很正常。 实际应用时都需要熟能生巧。
-
首先涉及到你是否能够严肃的对待它们,还是差不多就行。
-
其次,你是否具备足够的经验或技术来实践它们,使之成为可能。
由Jeff Bay提出的对象健身操就是这样的具体方法来帮助你轻松实现上述原则,称之为“九诫”:
-
方法只使用一级缩进(One level of indentation per method)—— 每个方法长度不超过5。
-
只需要在方法内没有嵌套的if/switch/for/while等关键字,使用重构中的extract method手法完全可以做到。
-
目的1:实现函数的单一职责
-
目的2:函数变得更加简明,定位错误更加容易。
-
-
拒绝使用else关键字(Don’t use the ELSE keyword)
-
方式一:卫语句或提前返回
-
方式二:使用三元操作符
-
其他方法:
- 使用多态
- 空对象模式
- 策略模式
- 状态模式
-
-
封装所有的原生类型和字符串(Wrap all primitives and Strings)
-
通过包装类来封装原生类型和字符串,比较常见的有:Hour、Money等类。
使得类型的使用上更具可读性和安全性。
-
-
一行代码只有一个“.”运算符(One dot per line)
- 违反该诫条的代码形式为:obj.m1().m2().m3(),对象需要同时与另外多个对象交互。也叫“消息链条”。
- 该行为暴露了细节,破坏了封装性,让类的边界跨入了其不应知道的类中,违反了“迪米特法则”(只和身边的朋友交流)。
迪米特法则的通俗解释:你可以玩自己的玩具,可以玩你制造的玩具,还要别人送给你的玩具,但是永远不要碰别人的玩具。
-
不要使用缩写( Don’t abbreviate)
- 所有实体对象的名称只包含一到两个单词,不能使用缩写。好处是避免名字中重复上下文信息。
- 使用缩写的原因:
- 不停地方法调用—意味着有必要消除重复。
- 方法名太长—意味着职责没有放在正确的位置或有缺失的类。
-
保持实体对象简单清晰(Keep all entities small)
-
类的行数不超过50行,每个包不超过10个文件。
-
超过50行一般职责大概率不单一
-
超过50行一个屏幕就放不下了。 低于不用滚屏,使得代码更易于阅读者理解。
-
由于包内文件数量的限制,包会更加内聚,且会有一个明确的意图。
-
-
任何类中的实例变量都不要超过两个(No classes with more than two instance variables)
-
使用一流的集合(First class collections)
- 任何包含集合的类中,不应包含其他成员变量。
-
不使用任何Getter/Setter/Property(No getters/setters/properties)
戒条详解
诫条三:封装所有的原生类型和字符串
public interface Account {
void credit(int amount);
void debit(int amount);
}
应用诫条后的代码:
public interface Account {
void credit(Money amount);
void debit(Money amount);
}
重构前任意的int型数值都可以参与账户转账业务,重构后只能是Money类型才合法。
如果原生类型变量拥有行为时,有必要对其进行封装。
诫条四:一行代码只有一个“.”运算符
class Board {
...
class Piece {
...
String representation;
}
class Location {
...
Piece current;
}
String boardRepresentation() {
StringBuffer buf = new StringBuffer();
for (Location l : squares())
buf.append(l.current.representation.substring(0, 1));
return buf.toString();
}
}
应用诫条后的代码:
class Board {
...
class Piece {
...
private String representation;
String character() {
return representation.substring(0, 1);
}
void addTo(StringBuffer buf){
buf.append(character());
}
}
class Location {
...
private Piece current;
void addTo(StringBuffer buf){
current.addTo(buf);
}
}
String boardRepresentation() {
StringBuffer buf = new StringBuffer();
for (Location l : squares())
l.addTo(buf);
return buf.toString();
}
}
在流式编程及内部DSL中也常有,但这些代码一般称之为“流畅接口(Fluent Interface)”:
-
二者的区别在于观察形成链条的每个方法返回的是别的对象,还是自身。
-
如果返回的是别的对象,就属于消息链条,就是不可取的。 但是stream,builder这些都是返回自身。
public class GraphDslSample {
public static void main(String[] args) {
Graph()
.edge()
.from("a")
.to("b")
.weight(40.0)
.edge()
.from("b")
.to("c")
.weight(20.0)
.edge()
.from("d")
.to("e")
.weight(50.5)
.printGraph();
}
}
诫条七:任何类中的实例变量都不要超过两个
-
将一个对象从拥有大量属性状态,解构成分层次的、相互关联的多个对象,直接产生一个更实用的对象模型。
-
这可能是最难做到的诫条了,但会促进代码的高内聚性和更好的封装性。它依赖于诫条三(封装所有的原生类型和字符串)。
实际操作时可沿两个方向进行:
- 将对象实例变量按照相关性分离在两个部分中
- 创建一个新的对象来封装两个已有变量
- 不断重复。
诫条八:使用一流的集合
任何包含集合的类中,不应包含其他成员变量。这样集合的各种行为就有了明确的依附物,这些行为包含各种过滤器、针对每个元素的特殊规则、多个集合的处理(拼接、交集等等)。
诫条九:不使用任何Getter/Setter/Property
通过该诫条迫使程序员在完成编码后,一定要为这段代码的行为找到一个合适的位置,确保它在对象模型中的唯一性。
其好处如下:
- 提升代码封装性
- 减少重复性错误
- 实现新特性时,有一个更合适的位置去引入变化。
// Game
private int score;
public void setScore(int score) {
this.score = score;
}
public int getScore() {
return score;
}
// Usage
game.setScore(game.getScore() + ENEMY_DESTROYED_SCORE);
应用戒条后:
// Game
public void addScore(int delta) {
score += delta;
}
// Usage
game.addScore(ENEMY_DESTROYED_SCORE);