1. 数据抽象
下面代码1和代码2都表示笛卡尔平面上的一个点。
代码1:
public class Point { public double x; public double y; }
代码2:
public interface Point { double getX(); double getY(); void setCartesian(double x, double y); double getR(); double getTheta(); void setPolar(double r, double theta); }
代码1暴露了实现,而代码2隐藏了实现,在代码2中你不知道实现是在矩形坐标系中还是极坐标系中。
类并不简单的通过get/set方法将其变量推向外面,而是暴露抽象接口,以便用户无需了解数据的实现就能操作数据本体。
下面的代码3暴露具体变量给外界,而代码4采用百分比抽象。
代码3:
public interface Vehicle { double getFuelTankCapacityInGallons(); double getGallonsOfGasoline(); }
代码4:
public interface Vehicle { double getPercentFuelRemaining(); }
代码4比代码3好,我们不愿意暴露数据细节,更愿意以抽象的形态表述数据。要以最好的方式呈现某个对象包含的数据,需要做严肃的思考。傻乐着乱加get/set方法,是最坏的选择。
2. 数据和对象的反对称性
数据结构暴露其数据,没有提供有意义的函数;对象则是吧数据隐藏于抽象之后,暴露操作数据的函数。
代码5是面向数据编程,所有的行为都在Geometry类中。
代码5:
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类添加一个计算周长的方法,形状类不会受影响。但是如果想要添加一个新形状,就得修改Geometry中的所有方法来处理它。
下面是面向对象的方案。area()方法是多态的,不需要有Geometry类。所以添加一个新的形状,现有类一个不会受影响;但添加新的函数时,所有形状都得做修改。
public interface Shape { double area(); } public class Square implements Shape { private Point topLeft; private double side; @Override public double area() { return side * side; } } public class Rectangle implements Shape { private Point topLeft; private double height; private double width; @Override public double area() { return width * height; } } public class Circle implements Shape { private Point center; private double radius; public final double PI = 3.1415926; @Override public double area() { return PI * radius * radius; } }
再次对比上面的两种实现:他们是截然对立的。
过程式代码便于在不改动机油数据结构的前提下添加新函数。面向对象代码便于在不改动既有函数的前提下添加新类。所以,对面向对象较难的事,对于过程式代码却较容易,反之亦然。
在任何一个复杂系统中,都会有需要添加新数据类型而不是新函数的时候,这时面向对象比较合适。当需要新函数而不是新数据类型的时候,过程式代码和数据结构更合适。老练的程序员知道,一切都是对象只是一个传说。