美文网首页代码的艺术架构设计与重构互联网科技
并不是一切皆对象(clean code阅读笔记之五)

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

作者: TheAlchemist | 来源:发表于2016-05-15 09:07 被阅读496次
    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一样都属于数据结构而非对象,所以不要在其中添加复杂的逻辑。

    结论

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

    相关文章

      网友评论

      本文标题:并不是一切皆对象(clean code阅读笔记之五)

      本文链接:https://www.haomeiwen.com/subject/msbprttx.html