格式与对象:垂直格式、水平格式、数据抽象、数据、对象的反对称性

说实话,代码格式这个话题,很多程序员觉得是「小事」。不就是缩进几个空格、换行对不对齐嘛?

但我见过太多因为格式混乱导致的线上事故了。有一次我接手一个老项目,一个文件三千行,垂直方向上各种逻辑混在一起,水平方向上有的行超过两百个字符。我花了整整两天才理清楚一个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();
    }
}

避坑指南:我曾经在一个项目中,强行把所有东西都做成对象。结果一个简单的数据转换逻辑,硬是拆了五个类,每个类只有两三个方法。后来重构时全部改成了纯函数,代码量直接减半。记住:不要为了用对象而用对象

知识体系总览

下面这张图总结了本章的核心逻辑。你可以把它当作一个快速参考。

格式与对象 · 知识体系 代码格式 对象与数据 垂直格式 水平格式 · 相关放一起 · 空行隔开 · 调用者在上 · 不超过120字符 · 运算符空格 · 逗号后空格 数据抽象 反对称性 · 隐藏数据结构 · 暴露行为 · 避免纯getter/setter · 对象:类型易增 · 过程:操作易增 · 根据场景选择 核心:格式服务于可读性,抽象服务于可维护性

好了,这一章的内容就到这里。格式和对象,看似是两个独立的话题,但本质上都指向同一个目标:让代码更容易被理解、更容易被修改。下次你写代码的时候,不妨多问自己一句:「这段代码,三个月后的我还看得懂吗?」


公众号:蓝海资料掘金营,微信deep3321