Clean Code:对象和数据结构

By | 2019年4月14日

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

再次对比上面的两种实现:他们是截然对立的。

过程式代码便于在不改动机油数据结构的前提下添加新函数。面向对象代码便于在不改动既有函数的前提下添加新类。所以,对面向对象较难的事,对于过程式代码却较容易,反之亦然。

在任何一个复杂系统中,都会有需要添加新数据类型而不是新函数的时候,这时面向对象比较合适。当需要新函数而不是新数据类型的时候,过程式代码和数据结构更合适。老练的程序员知道,一切都是对象只是一个传说。