24、面向对象设计原则:高内聚低耦合,组合优于继承,迪米特法则,接口隔离

面向对象设计原则,说白了就是一套「写代码的规矩」。我刚开始写程序那会儿,觉得能跑就行。后来项目越做越大,改一个功能要动十几个文件,改完还出 bug……嗯,那时候我才明白,规矩不是用来限制你的,是用来救你的。

今天聊四个核心原则。它们不是孤立的,你想想看,它们其实都在回答同一个问题:怎么让代码好改、好测、好复用?

一、高内聚与低耦合

这两个词经常一起出现。我个人的理解很简单:高内聚,就是把相关的东西放一起;低耦合,就是让不相关的东西少说话。

1. 高内聚

一个类只做一件事,并且把它做好。比如一个 OrderService 只处理订单逻辑,别把发送邮件、生成报表也塞进去。

高内聚的好处:
  • 代码容易理解——打开一个类就知道它干嘛的
  • 修改影响范围小——改订单逻辑不会炸了邮件模块
  • 复用性高——拿出去就能用
我的习惯:写类之前先问自己一句「这个类到底负责什么?」如果答案里出现「和」字,比如「处理订单和发送通知」,那就拆。

2. 低耦合

模块之间依赖越少越好。我在项目中遇到过最头疼的事:改了一个工具类的参数名,结果十几个模块报错。这就是耦合太紧的代价。

// 高耦合(不推荐)
public class OrderService {
    private EmailService email = new EmailService();
    private SmsService sms = new SmsService();

    public void createOrder(Order order) {
        // 创建订单...
        email.send("订单创建成功");
        sms.send("订单创建成功");
    }
}

// 低耦合(推荐)
public class OrderService {
    private List<NotificationService> notifiers;

    public OrderService(List<NotificationService> notifiers) {
        this.notifiers = notifiers;
    }

    public void createOrder(Order order) {
        // 创建订单...
        for (var n : notifiers) {
            n.notify("订单创建成功");
        }
    }
}

看到了吗?低耦合版本里,OrderService 根本不关心具体用什么方式通知。加个微信通知?加个钉钉通知?完全不用改 OrderService 的代码。

二、组合优于继承

这个原则我年轻时吃过亏。那时候觉得继承很「面向对象」,恨不得所有类都继承来继承去。结果呢?三层继承之后,父类改一个方法,子类全崩了。

组合的核心思想:用「有什么」代替「是什么」。

// 继承方式(不灵活)
public class Bird {
    public void fly() { System.out.println("飞"); }
}
public class Penguin extends Bird {
    @Override
    public void fly() {
        throw new UnsupportedOperationException("企鹅不会飞");
    }
}

// 组合方式(灵活)
public class Bird {
    private FlyBehavior flyBehavior;

    public Bird(FlyBehavior flyBehavior) {
        this.flyBehavior = flyBehavior;
    }

    public void performFly() {
        flyBehavior.fly();
    }
}

public class Penguin extends Bird {
    public Penguin() {
        super(new NoFly());  // 不会飞的行为
    }
}
避坑指南:我曾经在一个项目中用继承实现了「动物-狗-拉布拉多」三层结构。后来要加一个「会飞的狗」?继承体系直接炸了。改成组合后,给狗加个「飞行能力」对象就搞定了。

组合的优势很明显:

  • 运行时可以动态改变行为
  • 不会破坏封装
  • 类层次扁平,容易理解

三、迪米特法则(最少知识原则)

这个原则的名字听着挺唬人,其实意思很简单:一个对象应该尽量少地知道其他对象。

说白了就是:别跟陌生人说话。你只跟你的直接朋友打交道,别去调用朋友的朋友的方法。

// 违反迪米特法则
public class Teacher {
    public void checkHomework(Student student) {
        // 老师直接调用学生的书包里的作业本
        student.getBag().getHomework().check();
    }
}

// 符合迪米特法则
public class Teacher {
    public void checkHomework(Student student) {
        // 老师只跟学生说话
        student.submitHomework();
    }
}

public class Student {
    public void submitHomework() {
        // 学生自己处理书包和作业本
        bag.getHomework().check();
    }
}
核心要点:
  • 一个对象的方法只能调用:自身的方法、参数对象的方法、成员对象的方法、创建的对象的方法
  • 不要通过 getter 拿到内部对象后再调用它的方法
  • 如果发现代码里出现连续的点号(a.getB().getC().doSomething()),大概率违反了这个原则

我记得有一次重构一个老项目,看到一个方法里写了 order.getUser().getAddress().getCity()。我当时就想:这个方法的调用者到底需要知道多少东西?后来改成 order.getCity(),清爽多了。

四、接口隔离原则

这个原则说的是:客户端不应该被迫依赖它不使用的接口。

你想想看,如果一个接口里塞了十几个方法,实现类里有一半方法都只能抛出 UnsupportedOperationException,那这个接口设计就有问题。

// 胖接口(不推荐)
public interface Worker {
    void work();
    void eat();
    void sleep();
    void code();
    void design();
    void test();
}

// 接口隔离(推荐)
public interface Workable {
    void work();
}
public interface Eatable {
    void eat();
}
public interface Sleepable {
    void sleep();
}
public interface Coder {
    void code();
}
public interface Designer {
    void design();
}
public interface Tester {
    void test();
}

// 程序员只需要实现需要的接口
public class Programmer implements Workable, Eatable, Sleepable, Coder {
    // 只实现自己需要的
}
我的经验:接口设计时,先问「这个接口的使用者是谁?他们需要什么?」而不是「把所有可能的方法都列出来」。接口越小,越容易复用,越容易测试。

知识体系总览

下面这张图把这四个原则的关系梳理了一下。你会发现它们其实都在解决同一个问题:让代码更灵活、更稳定。

面向对象设计四大原则 高内聚低耦合 相关功能放一起(高内聚) 模块间少依赖(低耦合) → 改一处,不影响别处 组合优于继承 用「有什么」代替「是什么」 运行时动态组合行为 → 避免继承链爆炸 迪米特法则 只跟直接朋友说话 别调用朋友的朋友的方法 → 减少对象间耦合 接口隔离 接口小而专一 不强迫实现不需要的方法 → 接口更易复用 目标:代码灵活、稳定、易维护、易测试

总结一下

这四个原则,我个人的使用频率排序是这样的:

原则 核心思想 我常用的场景
高内聚低耦合 相关放一起,不相关少联系 模块拆分、类设计
组合优于继承 用组合代替继承层次 策略模式、装饰器模式
迪米特法则 减少对象间的知识 接口设计、方法参数设计
接口隔离 接口小而专一 微服务接口、类库设计
最后说一句:这些原则不是教条。我见过有人为了「低耦合」把代码拆得稀碎,反而更难维护。原则是工具,不是枷锁。用的时候多想想:这样改,以后改起来方便吗?

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