图片来源:Objects and Data Structures
一,数据抽象
将变量设置为私有(Private),主要是不想让其他人依赖这些变量。
所以,不要随便给变量添加赋值方法和取值方法(set/get方法),这样其实是把私有变量公之于众。
隐藏变量和实现,并不是在变量与外界之间放一个函数层那么简单。隐藏关乎抽象。
类并不简单地用赋值方法和取值方法将其变量推向外间,而是暴露抽象接口,以便用户无需了解数据的实现而能操作数据本体。
要以什么方式呈现对象所包含的数据,需要做严肃的思考。随便加赋值方法和取值方法,是最坏的选择。
二,数据、对象的反对称性
有两种表示和操作数据的方式,分别是:
面向对象方式,把数据隐藏在抽象之后,曝露其操作数据的函数。
面向过程方式,只曝露其数据,没有提供有意义的函数(数据结构方面,请类比于使用C语言的struct类)。
这两种定义,它们是对立的,差异虽微小,但却有深远的含义。
比如以下代码,就是面向过程式的:
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(); } }
以上的面向对象的代码,如果给Geometry类添加一个primeter()函数,其他的形状类都不受影响!
如果添加一个形状类,则需要修改Geometry类里所有的函数来处理它。
以下代码,是面向对象式的,
面向对象式的代码,如果添加一个新的形状类,则现有的函数一个也不会受到影响,但是添加一个新函数时(在接口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; } }
这里我们看到了这两种定义的本质,它们是截然对立的,这说明了面向对象和面向过程的本质区别:
面向过程的代码,便于不改动既有数据结构(struct)的前提下,添加新函数。
面向对象的代码,便于不改动既有函数的前提下,添加新类。
反过来讲也说得通:
过程式的代码难以添加新的数据结构,因为必须修改所有的函数。
面向对象的代码难以添加新函数,因为必须修改所有类。
所以,大体上,对面向对象较难的事情,对于面向过程却较容易,反之亦然。
在任何一个复杂系统种,都会有添加新数据类型而不是添加新函数的时候,这时,面向对象就很适合。
另一方面,也有添加新函数而不是数据类型的时候,此时面向过程代码和数据结构更合适。
三,得墨忒耳定律
得墨忒耳定律(The Law of Demeter)认为,模块不应了解它所操作对象的内部情形。
对象隐藏数据,曝露操作,这意味着对象不应通过存取器曝露其内部数据结构,因这更象曝露而非隐藏其内部结构。
更准确地说,得墨忒耳定律认为,类C的方法f,只能调用以下的方法:
C内部的方法
由f创建的对象的方法
作为参数传递给f的对象的方法
由C的实体变量持有的对象的方法
f不得调用由任何函数返回的对象的方法。
换言之,只跟朋友谈话,不与陌生人谈话。
以下代码违反了得墨忒耳定律,因为它调用了getOption()返回的对象里的getScratchDir()函数,又调用了getScratchDir()返回值的getAbsolutePath()方法。
final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();
这类代码通常被称作“火车失事”,因为它看起来就象一列火车。
这种串联调用通常被认为是肮脏的风格并应该避免。
改成以下代码,效果会好些:
Options opts = ctxt.getOptions(); File scratchDir = opts.getScratchDir(); final String outputDir = scratchDir.getAbsolutePath();
但对于一个函数来说,里面调用的方法包括这么多对象的引用,它“知道”的太多了。
如果是这样的代码,就不会有对得墨忒耳定律的违反,
final String outputDir = ctxt.options.scratchDir.absolutePath;
另外,也可以封装一个方法,象这样:
ctxt.getAbsolutePathOfScratchDirectoryOption();
另外我们还要看,需要这个绝对路径的目的是什么,比如,如果该代码的目的,是创建一个文件:
String outFile = outputDir + "/" + className.replace('.', '/') + ".class"; FileOutputStream fout = new FileOutputStream(outFile); BufferedOutputStream bos = new BufferedOutputStream(fout);
那么我们干脆让ctxt对象来做这件事情:
BufferedOutputStream bos = ctxt.createScratchFileStream(classFileName);
看起来就好多了。
四,混杂风格
有时候看类的定义,又曝露属性读写方法,又有方法定义,这是一半面向对象,一半面向过程的结构。
无论出于什么样的初衷,公共的属性读写方法,都会诱导外部函数修改属性,并以面向过程的方式使用该类。
这样的类,增加了添加新函数的难度,也增加了添加新数据结构的难度,两面不讨好。
五,数据传送对象
面向过程的数据结构,是只有属性和存取方法,没有函数的类。这种数据结构有时被称之为“数据传送对象”,简称DTO(Data Transfer Objects)。
在与数据库通信,或解析Socket信息时,经常使用它。
public class Address { private String street; private String streetExtra; private String city; private String state; private String zip; public Address(String street, String streetExtra, String city, String state, String zip) { this.street = street; this.streetExtra = streetExtra; this.city = city; this.state = state; this.zip = zip; } public String getStreet() { return street; } public String getStreetExtra() { return streetExtra; } public String getCity() { return city; } public String getState() { return state; } public String getZip() { return zip; } }
讨论问题:
1, 你同意本文关于面向对象和面向过程的代码的看法吗?如果有分歧,你的看法是什么?
如何加入Root Cause读书会:请加我的微信brainstudio,我建立了一个群,方便通知各位写读后感及讨论。
面向对象:当需要再接口中增加函数时,说明之前的设计出了问题;我基本同意文中的观点,但是还想强调另外一点,即面向对象更重要的是“信息隐藏”,将变化影响的范围尽可能缩小。
文中的例子提到,如果给Shape接口里加函数很难,但是另一方面接口的特性会强制(编译器会提示)我们改掉所有实现了这个接口的类,但是如果是面向过程的写法,在Geometry类里少写个方法编译器也不会报错。总的来说,这可能是语言特性的区别,好的语言会引导和监督使用者写出优秀的代码。