Fork me on GitHub

对象健身操详解

the details of object calisthenics

Posted by Kaelzhang on June 10, 2018

对象健身操详解

优秀设计背后的核心概念并不高深,七条评判代码质量原则就基本上能够涵盖,它们具体是:

  1. 内聚性
  2. 松耦合
  3. 零重复
  4. 封装
  5. 可测试性
  6. 可读性
  7. 单一职责

对于上述原则,大家可能都耳熟能详,但在实际应用时可能就没那么容易去做了,这首先涉及到你是否能够严肃的对待它们,还是差不多就行。其次,你是否具备足够的经验或技术来具化它们,使之成为可能。由Jeff Bay提出的对象健身操就是这样的具体方法来帮助你轻松实现上述原则,称之为“九诫”:

  1. 方法只使用一级缩进(One level of indentation per method)
  2. 拒绝使用else关键字(Don’t use the ELSE keyword)
  3. 封装所有的原生类型和字符串(Wrap all primitives and Strings)
  4. 一行代码只有一个“.”运算符(One dot per line)
  5. 不要使用缩写( Don’t abbreviate)
  6. 保持实体对象简单清晰(Keep all entities small)
  7. 任何类中的实例变量都不要超过两个(No classes with more than two instance variables)
  8. 使用一流的集合(First class collections)
  9. 不使用任何Getter/Setter/Property(No getters/setters/properties)

戒条一:方法只使用一级缩进

该戒条理解起来很简单,只需要在方法内没有嵌套的if/switch/for/while等关键字,使用重构中的extract method手法完全可以做到。其目的有2个:

  1. 实现函数的单一职责
  2. 函数变得更加简明,定位错误更加容易。
class Board {
	...
	String board() {
		StringBuffer buf = new StringBuffer();
		for(int i = 0; i < 10; i++){
			for(int j = 0; j < 10; j++)
				buf.append(data[i][j]);
			buf.append("\n");
		}
		return buf.toString();
	}
}

应用诫条重构后的代码:

class Board {
	...
	String board(){
		StringBuffer buf = new StringBuffer();
		collectRows(buf);
		Return buf.toString();
	}

	Void collectRows(StringBuffer buf){
		for(int i = 0; i < 10; i++)
			collectRow(buf, i);
	}

	Void collectRow(StringBuffer buf, int row){
		for(int i = 0; i < 10; i++)
			buf.append(data[row][i]);
		buf.append("\n");
	}
}

该诫条的另外一种常见形式是每个方法长度不超过5。

诫条二:拒绝使用else关键字

方式一:卫语句或提前返回

public static void endMe(){
	if(status == DONE){
		doSomething();
	}else{
		doSomethingElse();	
	}
}
public static void endMe(){
	if(status == DONE){
		doSomething();
		return;
	}
	doSomethingElse();
}

方式二:使用三元操作符

public static Node head(){
	if(isAdvancing()){
		return first;
	}else{
		return last;
	}
}
public static Node head(){
	return isAdvancing() ? first : last;
}

其他方法:

  1. 使用多态
  2. 空对象模式
  3. 策略模式
  4. 状态模式

具体可参见各种设计模式的书籍,在此就不复述了。

注:使用该诫条需要关注代码清晰度的变化。

诫条三:封装所有的原生类型和字符串

该诫条对应反模式:Primitive Obsession 通过包装类来封装原生类型和字符串,比较常见的有:Hour、Money等类。使得类型的使用上更具可读性和安全性。 但这并不意味着使用诸如Java语言提供的类似对象包装器,使用Integer类并不会在表达意图上带来额外的优势,而使用表达意图含义的包装器既能澄清其用法,又能让意图变得明显。

public interface Account {
    void credit(int amount);
    void debit(int amount);
}

应用诫条后的代码:

public interface Account {
    void credit(Money amount);
    void debit(Money amount);
}

重构前任意的int型数值都可以参与账户转账业务,重构后只能是Money类型才合法。

注:如果原生类型变量拥有行为时,有必要对其进行封装。

诫条四:一行代码只有一个“.”运算符

违反该诫条的代码形式为:obj.m1().m2().m3(),对象需要同时与另外多个对象交互。在Martin Fowler《重构》中,将其命名为“消息链条(Message Chain)”,别名“火车残骸”。该行为暴露了细节,破坏了封装性,让类的边界跨入了其不应知道的类中,违反了“迪米特法则”(只和身边的朋友交流)。

迪米特法则的通俗解释:你可以玩自己的玩具,可以玩你制造的玩具,还要别人送给你的玩具,但是永远不要碰别人的玩具。

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)”:

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();
 
  }
}

二者的区别在于观察形成链条的每个方法返回的是别的对象,还是自身。如果返回的是别的对象,就属于消息链条。

注:附带好处可读性进一步提升。

诫条五:不要使用缩写

所有实体对象的名称只包含一到两个单词,不能使用缩写。好处是避免名字中重复上下文信息。

使用缩写的一般原因:

  1. 不停地方法调用—意味着有必要消除重复
  2. 方法名太长—意味着职责没有放在正确的位置或有缺失的类
class EO{
   ...
   void shipOrder();
}

// 方法调用上存在冗余
EO order = new EO();
order.shipOrder();

应用诫条后的代码:

class EntityOrder{
   ...
   void ship();
}

// 方法调用更自然
EntityOrder order = new EntityOrder();
order.ship();

诫条六:保持实体对象简单清晰

类的行数不超过50行,每个包不超过10个文件。

超过50行的类通常做不止一件事,这使得它们更难理解,更难以重用。 另外一个好处就是可在一个屏幕上显示,不用滚屏,使得代码更易于阅读者理解。

挑战是将会出现很多成组的行为,它们的逻辑应该在一起的。这就需要包机制来平衡。由于包内文件数量的限制,包会更加内聚,且会有一个明确的意图。

class SomeClass
{
    // 300 lines of code
    
    // 20 properties
    
    // 20 methods
    
    public function simpleLogic()
    {
        // 30 lines of code
    }
}
class SomeClass
{
    // 50 lines of code
    
    // 5 properties
    
    // 5 methods
    
    public function simpleLogic()
    {
        // 10 lines of code
    }
}

诫条七:任何类中的实例变量都不要超过两个

将一个对象从拥有大量属性状态,解构成分层次的、相互关联的多个对象,直接产生一个更实用的对象模型。

这可能是最难做到的诫条了,但会促进代码的高内聚性和更好的封装性。它依赖于诫条三(封装所有的原生类型和字符串)。

一图胜千言:

代码示例:

class Name{
   String first;
   String middle;
   String last;
}

应用诫条后的代码:

class Name{
   Surname family;
   GivenNames given;
}

class Surname{
   String family;
}

class GivenNames{
   List<String> names;
}

实际操作时可沿两个方向进行:

  1. 将对象实例变量按照相关性分离在两个部分中
  2. 创建一个新的对象来封装两个已有变量

诫条八:使用一流的集合

任何包含集合的类中,不应包含其他成员变量。这样集合的各种行为就有了明确的依附物,这些行为包含各种过滤器、针对每个元素的特殊规则、多个集合的处理(拼接、交集等等)。

public class BlogPost
{
    public readonly string[] ContentBlocks;
    //...//
}
public class ContentBlocks
{
    public readonly string[] Blocks;
}

public class BlogPost
{
    public readonly ContentBlocks Content;
    public readonly bool AddHeadline;
    public readonly string Category;
}

应用诫条后的代码:

public void Publish(BlogPost post)
{
    if(post.Category == "News")
    {
      return;
    }
    
    contentBlocks.Except(post.Content.Blocks[0])
                 .Foreach(block => writer.WriteBlock(block);
}

诫条九:不使用任何Getter/Setter/Property

别名: 告诉而不要询问(Tell, don’t ask)原则。

通过该诫条迫使程序员在完成编码后,一定要为这段代码的行为找到一个合适的位置,确保它在对象模型中的唯一性。

其好处如下:

  1. 提升代码封装性
  2. 减少重复性错误
  3. 实现新特性时,有一个更合适的位置去引入变化。
// 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);

应用

对象健身操由于过于严苛,一般会用于codekata之类的编码练习,以便参与者能够加深其对面向对象编程的理解程度。

那么它是否在真实业务场景下是没有价值的?我的建议是首先掌握其精髓,然后尽量尝试。何况Jeff Bay就在一个规模超过10W行的系统中严格应用全部的9条规则,并取得不错效果。

最后别忘了,为你的代码编写测试用例。

练习

写一个新的Job Application domain,要求如下:

domain中包含5个实体:

  1. Jobs
  2. Jobseekers
  3. Employers
  4. Resumes
  5. Job Applications

实体交互如下:

  • Employers能够发布jobs
  • Employers也应能看见他们所发布的jobs清单
  • Jobseekers能够保存jobs以便以后查看
  • Jobseekers可以申请employers发布的jobs
  • employers可以发布两类Jobs: JReq and ATS
  • JReq类型的jobs需要resume才能申请它们
  • ATS类型的jobs无需resume
  • Jobseekers不能以他人的resume申请job
  • Jobseekers能够以不同的resumes申请不同的jobs
  • Jobseekers能够保存jobs清单以便后续查看
  • Jobseekers能够查看已申请的jobs清单
  • Employers能够通过job或者day查看申请job的jobseekers。并且可以联合job和day来查看申请job的jobseekers
  • 能够获取任一jobseekers在给定day的jobs申请情况
  • 能够以csv或html格式获取job申请报告
  • 能够从job申请报告中确定jobseeker,job,employer和job申请日期
  • 通过job和employer应该能够看到有总job申请数量
  • 通过job和employer应该能够看到有多少job申请失败,以及有多少job累计成功
  • Jobseekers在显示时应以他们的名字
  • Employers在显示时应以他们的名字
  • Jobs显示时应显示一个title和发布它的employer的名字
  • 系统能够处理具有相同title的多个jobs
  • 系统能够处理同名的多个Jobseekers
  • 系统能够处理同名的多个employer

案例

  1. C#:https://gist.github.com/onlytiancai/1738383
  2. https://github.com/dennisdoomen/objectcalisthenics
  3. JS:https://github.com/bennadel/Object-Calisthenics
  4. Golang:https://github.com/rafos/object-calisthenics-in-go
  5. Java:https://github.com/apaxmai/ObjectCalisthenics
  6. Python:https://github.com/halucinka/object_calisthenics

工具

对象健身操分析器: https://plugins.jetbrains.com/plugin/8080-object-calisthenics-analyzer

参考文献

  1. 《软件开发沉思录》
  2. http://www.infoq.com/cn/minibooks/thoughtworks-anthology
  3. https://github.com/TheLadders/object-calisthenics

  • 版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 3.0 许可协议。转载请注明出处!