并不是一切皆对象(clean code阅读笔记之五)

Star Trek中的机器人Data

注:正文中的引用是直接引用作者Bob大叔的话,两条横线中间的段落的是我自己的观点,其他大约都可以算是笔记了。

本文中的函数方法是一个概念

本文读起来可能比较晦涩,其实通篇只是讲了一件事情:在面向对象的环境里有两种方法去定义一个类,面向对象(本文中一直谈到的对象)和面向过程(本文中谈到的数据结构),它们各有优劣,在开发的时候要合适地做出选择。

由于「Clean Code」整本书都有很浓厚的Java的色彩,所以大部分代码和概念都是Java中比较常见的,不过在面向对象的语言中大致都能找到相应的东西


数据抽象

我们在设计对象的结构时,应该尽可能地使用数据抽象。如代码5-1中所示的两种对于笛卡尔平面中的一个点的数据结构定义,从这里可以看到,使用抽象的类定义,这个类就不仅仅是一个数据结构了。通过暴露出来的方法强制了对于数据的设置必须x轴和y轴同时设置,它可以代表一个平面坐标系中的一个点,也可以代表极坐标系中的一个点。

//代码5-1
//具象类定义
public class Point { 
    public double x; 
    public double y;
}

//抽象类定义
public interface Point {
    double getX();
    double getY();
    void setCartesian(double x, double y); 
    double getR();
    double getTheta();
    void setPolar(double r, double theta); 
}

面向对象概念中的「隐藏实现」的真实意义不应该只是在变量中增加了一层函数,而是「数据抽象」。数据抽象也不仅仅是使用一些interfacegettersetter就可以的,它需要你认真仔细去思考「如何才能更好地表示一个对象所包含的数据」。

「数据结构」和「对象」的反对称性


Bob大叔在这一小节讲得很玄乎,阐述了一个道理:过程式编程和面向对象编程是互补的两个概念。「一切皆对象」只是一个神话,我们有时候不可避免的要用到过程式的代码(也就是本文一直提到的数据结构)来补充。


数据结构对象是两个相反的概念,对象隐藏了数据并暴露了对数据操作的函数,而数据结构暴露了它的数据但并没有有意义的函数。这两个概念互为相反,但又相辅相成的。

过程式代码(使用数据结构的代码)的优势在于「当你想要添加新的函数时,不需要修改已存在的数据结构」,而面向对象的代码的优势在于「当你添加新的类时,不需要修改已存在的函数」。

下面通过一个例子解释它们的反对称性。

//代码5-2
//过程式的Shape类
public class Square { 
    public Point topLeft; 
    public double side;
}
public class Rectangle { 
    public Point topLeft; 
    public double height; 
    public double width;
}
public class Circle { 
    public Point center; 
    public double radius;
}
public class Geometry {
    public final double PI = 3.141592653589793;
    
    public double area(Object shape) throws NoSuchShapeException {
        if (shape instanceof Square) { 
            Square s = (Square)shape; 
            return s.side * s.side;
        }
        else if (shape instanceof Rectangle) { 
            Rectangle r = (Rectangle)shape; 
            return r.height * r.width;
        }
        else if (shape instanceof Circle) {
            Circle c = (Circle)shape;
            return PI * c.radius * c.radius; 
        }
        throw new NoSuchShapeException(); 

    }
}

//多态版本的Shape类
public class Square implements Shape { 
    private Point topLeft;
    private double side;
    public double area() { 
        return side*side;
    } 
}

public class Rectangle implements Shape { 
    private Point topLeft;
    private double height;
    private double width;
    public double area() { 
        return height * width;
    } 
}

public class Circle implements Shape { 
    private Point center;
    private double radius;
    public final double PI = 3.141592653589793;
    
    public double area() {
        return PI * radius * radius;
    } 
}

代码5-2中展示了两种Shape类的实现方法:

  • 第一种是传统的过程式的编程方法(也就是数据结构)。如果要在Geometry类中添加一个计算周长的方法,是不需要修改具体实现类(Square、Rectangle、Circle)中的任何代码,但是如果你要添加一个新的形状的实现类,那么就要在Geometry类中的每一个方法都作出修改。

  • 第二种是面向对象的多态方式的编程方法(也就是对象)。与第一种刚好相反(反对称性),如果要在Shape类中添加一个计算周长的方法,那么每一个实现类中的代码都需要修改,但是如果你要添加一个新的形状的实现类,那么现有的其他代码都不需要修改。

迪米特法则


迪米特法则可以概括为「不要和陌生人说话」。


精确的讲,迪米特法则规定一个类C中的方法f应该只调用以下几种方法:

  • C中的方法
  • f中生成的对象的方法
  • f的参数对象的方法
  • C持有其引用的对象中的方法
1. 火车事故

如代码5-3中代码段1这样的链式的耦合性极强的调用方法,我们通常称之为火车事故「train wrecks」,这类链式的调用应该禁止,应该将其重构为代码2这种形式。


Build模式和JQuery中的链式调用和并不适用这条规则,因为他们从头到尾所有方法的调用者都是同一个对象。


代码5-3中ctxt这个对象操作了多个层级的函数,违反了迪米特原则。但是,如果ctxtOptionsScratchDir这三个类都是简单的数据结构(如代码5-2中过程式的类定义),并不包含任何的行为的话,这样的调用并不违反迪米特法则。

我们平常使用的很多编程框架中要求所有的「实体类」都要添加getter和setter,这使得判断一个调用是否违反迪米特法则变得很困惑。作者认为作为简单的数据结构,比如实体类Pojo,就应该只包含public属性。

//代码5-3
//代码段1   这种强耦合的链式调用应该被禁止使用
final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();

//代码段2
Options opts = ctxt.getOptions();
File scratchDir = opts.getScratchDir();
final String outputDir = scratchDir.getAbsolutePath();

2. 数据结构和对象的杂交

有时候我们会创建这样的杂交类,其中有包含复杂逻辑的方法,同时又包含public属性或者public的getter和setter,应该避免创建这样的类。

3. 隐藏结构

对象应该隐藏自己的内部结构。

具体到代码5-3中所说的例子,作者认为从ctxt这个对象获取一个文件的绝对路径的本身就是不对的,绝对路径可能是ctxt的内部结构,我们可能是想使用这个绝对路径来构造一个对象,那么如果代码是这样子的:

String outFile = outputDir + "/" + className.replace('.', '/') + ".class";
FileOutputStream fout = new FileOutputStream(outFile);
BufferedOutputStream bos = new BufferedOutputStream(fout);

我们可以把它封装成函数,这样来调用:

BufferedOutputStream bos = ctxt.createScratchFileStream(classFileName);

传输数据的对象(Data Transfer Objects)

DTO是指那些只包含公共变量且没有函数的类,这是一类很有用的数据结构。但是现在更广为使用的则是Bean(类的变量为私有,但是含有公共的getter和setter),它起到的作用其实和DTO相同。

Active Record

这是DTO的一种特殊形式,在上边DTO讨论的基础之上,还包括一些save或find这样一些浏览方法。
它和DTO一样都属于数据结构而非对象,所以不要在其中添加复杂的逻辑。

结论

如果未来很有可能需要不断地往一个类中添加新的对象,那么就选择对象结构;如果未来可能需要不断的改变类的行为,就选择数据结构

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容