【拒绝屎山】让自己的代码更好看!

702 阅读5分钟

《对象健身操详解》

很久之前看的一篇文章,其中几条特别实用,分享给大家看看。

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

  1. 内聚性

  2. 松耦合

  3. 零重复

  4. 封装

  5. 可测试性

  6. 可读性

  7. 单一职责

道理我都懂,用起来就不熟练了,很正常。 实际应用时都需要熟能生巧。

  • 首先涉及到你是否能够严肃的对待它们,还是差不多就行。

  • 其次,你是否具备足够的经验或技术来实践它们,使之成为可能。

由Jeff Bay提出的对象健身操就是这样的具体方法来帮助你轻松实现上述原则,称之为“九诫”:

  1. 方法只使用一级缩进(One level of indentation per method)—— 每个方法长度不超过5

    • 只需要在方法内没有嵌套的if/switch/for/while等关键字,使用重构中的extract method手法完全可以做到。

    • 目的1:实现函数的单一职责

    • 目的2:函数变得更加简明,定位错误更加容易。

  2. 拒绝使用else关键字(Don’t use the ELSE keyword)

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

    • 方式二:使用三元操作符

    • 其他方法:

      • 使用多态
      • 空对象模式
      • 策略模式
      • 状态模式
  3. 封装所有的原生类型和字符串(Wrap all primitives and Strings)

    • 通过包装类来封装原生类型和字符串,比较常见的有:Hour、Money等类。

    使得类型的使用上更具可读性和安全性。

  4. 一行代码只有一个“.”运算符(One dot per line)

    • 违反该诫条的代码形式为:obj.m1().m2().m3(),对象需要同时与另外多个对象交互。也叫“消息链条”。
    • 该行为暴露了细节,破坏了封装性,让类的边界跨入了其不应知道的类中,违反了“迪米特法则”(只和身边的朋友交流)。

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

  1. 不要使用缩写( Don’t abbreviate)

    • 所有实体对象的名称只包含一到两个单词,不能使用缩写。好处是避免名字中重复上下文信息。
    • 使用缩写的原因:
      1. 不停地方法调用—意味着有必要消除重复。
      2. 方法名太长—意味着职责没有放在正确的位置或有缺失的类。
  2. 保持实体对象简单清晰(Keep all entities small)

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

    • 超过50行一般职责大概率不单一

    • 超过50行一个屏幕就放不下了。 低于不用滚屏,使得代码更易于阅读者理解。

    • 由于包内文件数量的限制,包会更加内聚,且会有一个明确的意图。

  3. 任何类中的实例变量都不要超过两个(No classes with more than two instance variables)

  4. 使用一流的集合(First class collections)

    • 任何包含集合的类中,不应包含其他成员变量。
  5. 不使用任何Getter/Setter/Property(No getters/setters/properties)

image.png

戒条详解

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

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

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

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

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

image.png

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

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

诫条八:使用一流的集合

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

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

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

其好处如下:

  1. 提升代码封装性
  1. 减少重复性错误
  1. 实现新特性时,有一个更合适的位置去引入变化。

    // 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);