设计模式七大原则详解(小白也能懂)

501 阅读6分钟

哈喽,大家好,我是Xwaiy,关注公众号:小码农岛 可查看更多文章,所有文章都会优先发布在公众号上。

话不多说,我们回到正题。

开闭原则(OCP)

简单来说

能方便地加新功能,但别改老代码
就像玩乐高积木,你可以随时加新积木块(扩展),但不用拆已经搭好的部分(不改动原有代码)。

为啥这么重要?

  • 改老代码容易带出bug
  • 加新功能时不用重新测试老代码

代码

错误案例

// 这个绘图工具每次加图形都得改代码
class GraphicEditor {
    public void drawShape(Shape s) {
        if (s.type == 1) drawRectangle();  // 加个三角形又得新增if判断
        else if (s.type == 2) drawCircle();
        // 以后每加新图形都要改这里
    }
}

问题:每次新增图形类型都得修改核心方法,迟早要出问题。

正确姿势

// 先定个规矩:所有图形都得会自己画自己
abstract class Shape {
    public abstract void draw();  // 强制子类自己实现
}

// 圆形自己管怎么画
class Circle extends Shape {
    @Override
    public void draw() {  
        System.out.println("画个圆");
    }
}

// 工具类根本不用改
class GraphicEditor {
    public void drawShape(Shape shape) {
        shape.draw();  // 甭管什么图形,调就完事了
    }
}

好处:要加三角形?直接新建个Triangle类就行,其他代码不用动。

单一职责原则(SRP)

说人话

一个类只干一件事
就像餐厅里炒菜归厨师,端盘子归服务员,各司其职。

为什么要拆开?

  • 改一个功能不影响其他
  • 更容易重复使用

代码

错误案例

class UserService {
    // 又管数据库又管业务逻辑
    public UserDTO getUser(String name) {
        Connection conn = DriverManager.getConnection(...); // 直接连数据库
        // 写SQL...
        // 转DTO...
    }
}

问题:哪天要换数据库,整个业务逻辑都得跟着改。

正确拆分

// 专门管数据库的
class UserDao {
    public User queryFromDB(String name) { 
        // 专业处理数据库操作
    }
}

// 专门管业务的
class UserService {
    private UserDao dao;  // 用接口不直接依赖
    
    public UserDTO getUser(String name) {
        User user = dao.queryFromDB(name);
        // 专心处理业务转换
        return new UserDTO(user);
    }
}

好处:换数据库只要改Dao层,业务代码稳如老狗。

里氏替换原则(LSP)

核心要点

子类要能当父类用
就像手机充电口,type-C可以替换USB,但功能不受影响。

关键注意

  • 不能缩小方法权限(父类public的方法子类不能变private)
  • 不能改变核心功能(父类算减法,子类不能改成加法)

经典案例

// 基础版:鸟会飞
class Bird {
    public void fly() { 
        System.out.println("扑棱翅膀飞"); 
    }
}

// 反面教材:企鹅继承鸟
class Penguin extends Bird {  // 要闯祸!
    @Override
    public void fly() {
        throw new UnsupportedOperationException(); // 企鹅不会飞啊
    }
}

// 正确打开方式
interface Flyable { void fly(); }
interface Swimmable { void swim(); }

class Sparrow implements Flyable { ... }  // 麻雀会飞
class Penguin implements Swimmable { ... } // 企鹅会游泳

启示:别让子类做不可能完成的任务,用接口更灵活。

依赖倒置原则(DIP)

说人话

对着接口编程,别死磕具体实现
就像用插座充电,管你是手机还是台灯,符合插头标准就能用。

实现套路

  1. 业务层定接口标准
  2. 具体实现类按标准来
  3. 用的时候把实现类"注射"进去

代码示例

// 先定义发消息的标准
interface MessageSender {
    void send(String message);
}

// 短信发送实现
class SmsSender implements MessageSender {
    public void send(String msg) {
        System.out.println("发短信:" + msg);
    }
}

// 邮件发送实现
class EmailSender implements MessageSender {
    public void send(String msg) {
        System.out.println("发邮件:" + msg);
    }
}

// 业务类只管标准不管实现
class NotificationService {
    private MessageSender sender;
    
    // 要什么发送方式就从外面传
    public NotificationService(MessageSender sender) {
        this.sender = sender;  
    }
    
    public void alert(String message) {
        sender.send(message);  // 按标准调用
    }
}

优势:要换发送方式?换个实现类就行,业务代码纹丝不动。

接口隔离原则(ISP)

核心思想

宁要多个小接口,不要一个大而全
就像工具箱,螺丝刀归螺丝刀,锤子归锤子,比瑞士军刀好用。

什么时候拆?

  • 接口里有些方法总用不上
  • 不同客户端只用部分方法

代码示例

臃肿接口

interface Animal {
    void eat();
    void fly();  // 企鹅:我招谁惹谁了?
    void swim(); // 麻雀:关我啥事?
}

问题:实现类被迫写没用的方法。

优化方案

interface Eatable { void eat(); }
interface Flyable { void fly(); }
interface Swimmable { void swim(); }

class Sparrow implements Eatable, Flyable {...}
class Penguin implements Eatable, Swimmable {...}

好处:需要什么功能就实现什么接口,清爽!

合成复用原则(CARP)

说人话

多用组合,少用继承
就像组装电脑,买显卡插主板(组合)比改造主板电路(继承)靠谱多了。

组合 vs 继承

继承组合
关系是父类的一种(汽车是交通工具)包含组件(汽车有发动机)
灵活度改父类影响所有子类随时换零件
耦合度高(绑死父类)低(接口对接)

代码示例

// 用组合实现功能
class Car {
    private Engine engine;  // 发动机作为零件
    
    public Car(Engine engine) {
        this.engine = engine;  // 想换引擎?传进来就行
    }
    
    public void start() {
        engine.ignite();  // 调用引擎接口
    }
}

interface Engine {
    void ignite();
}

// 各种引擎实现
class GasEngine implements Engine {...}
class ElectricEngine implements Engine {...}

优势:油车改电车?换个电动引擎实现类就行。

迪米特法则(LOD)

简单理解

知道的越少越好
就像租房找中介,不用直接联系房东、物业、装修队。

注意事项

  1. 只和直接朋友打交道(自己的属性、方法参数、返回值)
  2. 不暴露复杂内部结构(返回List而不是ArrayList)
  3. 避免长链式调用(别写a.getB().getC().doSomething())

代码示例

// 违反法则的写法
class Teacher {
    public void command(Monitor monitor) {
        List<Student> students = monitor.getStudents();  // 拿到学生名单
        for(Student s : students) {  // 直接指挥每个学生
            s.clean();
        }
    }
}

// 符合法则的写法
class Teacher {
    public void command(Monitor monitor) {
        monitor.cleanClassroom();  // 让班长负责具体安排
    }
}

class Monitor {
    public void cleanClassroom() {
        // 内部怎么分配我不管
    }
}

好处:老师不用知道学生怎么打扫,只管下命令就行。

总结对比表

原则名称核心要点常用场景容易踩的坑
开闭原则加功能不改老代码功能扩展、插件开发过度设计搞出复杂抽象
单一职责一个类只干一件事代码分层、工具类拆分拆太细导致类太多
里氏替换子类能当父类用接口实现、算法替换子类乱改父类功能
依赖倒置面向接口编程模块解耦、依赖注入直接new具体实现类
接口隔离按需拆分小接口权限管理、功能拆分设计万能接口
合成复用多用组合少继承组件开发、功能扩展继承层级太深
迪米特法则减少不必要的交互模块通信、系统封装链式调用暴露内部结构

这些原则就像做饭的调料,用好了能提升代码质量,但别死板硬套。新手建议多练习代码重构,慢慢体会什么时候该用什么原则。记住:适合的才是最好的!

最后想说

如果还在为面试八股文头疼?关注公众号 小码农岛,后台发送 66 免费领取《面试高频题宝典》,涵盖Java/MySQL/Redis/分布式等热门考点,助你轻松斩获Offer!

OK,今天的分享就到这里。欢迎「点赞+关注」支持!你对此有什么看法?欢迎在评论区分享您的观点。最后非常感谢大家的支持!你们的支持是我写作路上最大的动力。我们下期再见!