格式与对象:垂直格式、水平格式、数据抽象、数据、对象的反对称性
说实话,代码格式这个话题,很多程序员觉得是「小事」。不就是缩进几个空格、换行对不对齐嘛?
但我见过太多因为格式混乱导致的线上事故了。有一次我接手一个老项目,一个文件三千行,垂直方向上各种逻辑混在一起,水平方向上有的行超过两百个字符。我花了整整两天才理清楚一个if分支的逻辑。嗯,从那以后,我对格式这件事就特别较真。
今天咱们聊聊格式与对象。说白了,就是两件事:代码怎么摆,以及数据怎么藏。
垂直格式:让代码像报纸一样阅读
我个人习惯是,一个文件尽量控制在200行以内。超过这个数,我就会问自己:这个类是不是承担了太多职责?
垂直格式的核心原则其实很简单:相关的东西放一起,不相关的东西用空行隔开。
核心原则:
- 变量声明靠近使用位置
- 函数调用者在上,被调用者在下
- 概念相关的代码块之间用空行分隔
- 每个函数只做一件事,垂直方向上自然就短了
你想想看,读报纸的时候,标题、导语、正文是层层递进的。代码也应该这样。我见过最糟糕的写法是把所有成员变量堆在文件顶部,所有方法堆在底部,中间没有任何逻辑分组。这种代码读起来就像在翻一本没有目录的书。
// 不好的垂直格式
public class OrderService {
private double taxRate;
private String currency;
private List<OrderItem> items;
private Customer customer;
private DiscountCalculator calculator;
public double calculateTotal() {
// ... 50行逻辑
}
private double applyTax(double subtotal) {
// ... 20行逻辑
}
private double applyDiscount(double amount) {
// ... 30行逻辑
}
public void printInvoice() {
// ... 40行逻辑
}
}
// 好的垂直格式
public class OrderService {
// 配置相关
private double taxRate;
private String currency;
// 订单数据
private List<OrderItem> items;
private Customer customer;
// 计算器
private DiscountCalculator calculator;
public double calculateTotal() {
double subtotal = calculateSubtotal();
double afterTax = applyTax(subtotal);
return applyDiscount(afterTax);
}
private double calculateSubtotal() {
// 相关逻辑
}
private double applyTax(double subtotal) {
// 相关逻辑
}
private double applyDiscount(double amount) {
// 相关逻辑
}
public void printInvoice() {
// 相关逻辑
}
}
我的习惯:每个函数之间留一个空行。不同逻辑组之间留两个空行。这样视觉上就能快速区分「这是新的一段逻辑」。
水平格式:别让代码跑出屏幕
我记得有一次在Code Review时,看到同事写了一行300多个字符的代码。我问他:「你写这行的时候,眼睛不累吗?」他说:「我用的宽屏显示器,能显示完。」
但问题是,不是所有人都用宽屏。而且,一行代码太长,本质上说明它做了太多事。
水平格式的黄金法则:
- 每行不超过120个字符(我个人偏好80-100)
- 运算符前后加空格
- 逗号后面加空格
- 括号内不加空格(除非为了对齐)
// 不好的水平格式
public double calculateDiscountedPrice(double basePrice,double discountRate,int quantity,boolean isVIP){
return basePrice*quantity*(1-discountRate)*(isVIP?0.95:1.0);
}
// 好的水平格式
public double calculateDiscountedPrice(
double basePrice,
double discountRate,
int quantity,
boolean isVIP) {
double vipFactor = isVIP ? 0.95 : 1.0;
double subtotal = basePrice * quantity;
double discount = subtotal * discountRate;
return subtotal * (1 - discount) * vipFactor;
}
注意:不要为了缩短行数而牺牲可读性。比如把多个if条件挤在一行,或者把三元运算符嵌套三层。我曾经在一个项目里看到这样的代码:return a ? b ? c ? d : e : f : g;——这简直是代码界的「俄罗斯套娃」。
数据抽象:藏得越深,改得越爽
数据抽象,说白了就是「不要让别人知道你的数据长什么样」。你暴露的是行为,不是数据结构。
我遇到过最典型的反例:一个类里全是public的getter和setter,外部代码直接操作内部字段。后来需求变了,需要把存储方式从List改成Map,结果改了二十多个文件。
// 不好的数据暴露
public class Point {
public double x;
public double y;
}
// 好的数据抽象
public class Point {
private double x;
private double y;
public double distanceTo(Point other) {
double dx = this.x - other.x;
double dy = this.y - other.y;
return Math.sqrt(dx * dx + dy * dy);
}
public Point translate(double dx, double dy) {
return new Point(this.x + dx, this.y + dy);
}
}
我的经验:如果一个类只有getter/setter,那它本质上就是个数据结构,不是对象。真正的对象应该封装行为,而不是暴露数据。
数据与对象的反对称性
这个标题有点绕,我换个说法:面向对象和面向过程,其实是互补的。
你想想看:
- 面向对象:把数据和操作绑在一起。新增一个操作很难,因为要改所有类。但新增一个数据类型很容易。
- 面向过程:把数据和操作分开。新增一个操作很容易,但新增一个数据类型很难,因为要改所有函数。
这就是所谓的「反对称性」。说白了,没有银弹。你得根据场景选。
什么时候用对象?
- 数据类型相对稳定,但操作经常变化 → 用面向过程(函数式)
- 操作相对稳定,但数据类型经常变化 → 用面向对象
举个例子。如果你在做一个图形编辑器,图形类型(圆形、方形、三角形)基本不变,但经常要加新操作(旋转、缩放、镜像),那用面向对象就很合适。每个图形自己实现这些操作。
反过来,如果你在做编译器,语法树节点类型经常变,但遍历操作基本固定(打印、类型检查、代码生成),那用访问者模式(面向过程思路)就更合适。
// 面向对象风格:新增操作难,新增类型易
interface Shape {
double area();
double perimeter();
}
class Circle implements Shape {
private double radius;
public double area() { return Math.PI * radius * radius; }
public double perimeter() { return 2 * Math.PI * radius; }
}
class Square implements Shape {
private double side;
public double area() { return side * side; }
public double perimeter() { return 4 * side; }
}
// 面向过程风格:新增类型难,新增操作易
class ShapeOperations {
static double area(Object shape) {
if (shape instanceof Circle) {
Circle c = (Circle) shape;
return Math.PI * c.radius * c.radius;
} else if (shape instanceof Square) {
Square s = (Square) shape;
return s.side * s.side;
}
throw new IllegalArgumentException();
}
}
避坑指南:我曾经在一个项目中,强行把所有东西都做成对象。结果一个简单的数据转换逻辑,硬是拆了五个类,每个类只有两三个方法。后来重构时全部改成了纯函数,代码量直接减半。记住:不要为了用对象而用对象。
知识体系总览
下面这张图总结了本章的核心逻辑。你可以把它当作一个快速参考。
好了,这一章的内容就到这里。格式和对象,看似是两个独立的话题,但本质上都指向同一个目标:让代码更容易被理解、更容易被修改。下次你写代码的时候,不妨多问自己一句:「这段代码,三个月后的我还看得懂吗?」