设计原则讲解与业务实践

引言

在程序员的日常学习中,设计模式 几乎是"必修课"。但相比之下,设计原则往往被忽视,很多人甚至听都没听过。

其实,设计原则才是设计模式的根基。如果不理解原则,只会照搬模式,往往就会出现"为了模式而模式"的情况,代码看起来用了模式,但本质依旧混乱。

因此,本文将重点介绍六大设计原则,并结合真实业务场景,说明如果违反这些原则可能带来的后果。对于设计模式还不太熟悉的同学,可以先阅读《设计模式》专栏,再回头理解原则,效果会更好。


1、单一职责原则(SRP)

定义: 一个类只负责一项职责,避免出现"万能类"。

常见反例(业务场景) : 在实际项目中,经常能看到这样的写法:

  • 用户相关业务全部写在 UserService 里;
  • 订单相关业务全部写在 OrderService 里;
  • 商品相关业务全部写在 GoodsService 里。

时间一长,就会带来以下问题:

  • 类体积庞大:单个类动辄几千行,IDE 打开卡顿,多人开发时冲突频发;
  • 逻辑高度耦合:功能之间互相牵扯,一个小改动可能引发连锁问题;
  • 循环依赖风险:不同"万能类"之间互相引用,Spring 虽能解决部分场景,但复杂时依然可能导致项目无法启动;
  • 扩展性差:新增功能只能继续往这个大类里堆代码,维护和测试成本急剧上升。

优化方案 : 以用户服务为例,登录、资料更新、积分计算本质上是三种不同的职责。如果都堆在 UserService 中,会让逻辑混乱。更合理的做法是:

  • LoginService:专门负责用户登录;
  • UserProfileService:专门负责用户资料维护;
  • UserPointService:专门负责用户积分逻辑。

其他模块同理,订单、商品业务也应按职责拆分。 一个实用的判断标准是:如果某个功能会被其他类频繁依赖,就应该单独抽取出来。这样类之间的依赖关系会更加清晰,尽量保持线性依赖,避免循环依赖。

小结 单一职责原则的核心价值是 解耦与清晰。把"万能类"拆分成小而专的类,既能提升代码可读性和可维护性,也能避免扩展时陷入"牵一发动全身"的困境。


2、开闭原则(OCP)

定义: 软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。换句话说,需求变化时尽量通过"增加代码"来实现,而不是"修改已有代码"。

常见反例(业务场景) : 在支付业务中,很多系统一开始写成这样:

csharp 复制代码
if (payType.equals("WECHAT")) {
    // 微信支付逻辑
} else if (payType.equals("ALIPAY")) {
    // 支付宝支付逻辑
}

这种写法的隐患:

  • 违背开闭原则 :每增加一种支付方式,就要修改 PayService
  • 风险高:修改老代码容易引入回归 bug;
  • 可维护性差:if/else 会越来越多,代码臃肿难读。

优化方案 : 通过 策略模式 + 多态 进行解耦:

  1. 定义统一的 PayStrategy 接口;
  2. 为微信、支付宝、Apple Pay、银联支付分别实现对应的策略类;
  3. 使用工厂或 Spring 的依赖注入,根据 payType 自动选择对应策略。

这样新增支付方式时,只需增加一个类,不需要改动已有的 PayService,既符合开闭原则,又降低了风险。

推荐阅读《设计模式专栏(五):支付系统扩展与回调处理案例》,里面有完整实现示例。

小结 : 开闭原则的核心价值在于:通过扩展而非修改来适应变化。它让系统在需求不断变化时仍能保持稳定,减少回归风险,提升扩展性。


3、里氏替换原则(LSP)

定义: 子类对象必须能够替换父类对象,且程序逻辑不会因此出错。继承关系要符合"is-a"的逻辑,子类不能削弱或违背父类的约定。

常见反例(业务场景) : 假设我们在权限系统里定义了一个用户基类:

typescript 复制代码
public class User {
    public void viewPage(Page page) {
        // 用户查看页面
    }
}

现在新增一个 NormalUser(普通用户),继承了 User,但我们在实现时却这样写:

java 复制代码
public class NormalUser extends User {
    @Override
    public void viewPage(Page page) {
        throw new UnsupportedOperationException("普通用户不允许查看该页面");
    }
}

问题来了:

  • 按照语义,普通用户是用户(NormalUser is a User),应该具备用户的所有基本行为;
  • 但在子类里却禁止了 viewPage 的行为,导致替换成 NormalUser 时业务逻辑崩溃;
  • 这就是典型的违反 里氏替换原则

优化方案: 调整抽象设计:

  • 把用户抽象为 AbstractUser,定义最小公共能力;
  • 将"查看页面"权限交给 权限控制组件 判断,而不是写死在子类;
  • 这样,NormalUser 替换 User 时不会破坏原有逻辑,而权限差异通过策略或配置解决。

例如:

java 复制代码
public abstract class AbstractUser {
    public abstract void access(Page page);
}

public class NormalUser extends AbstractUser {
    @Override
    public void access(Page page) {
        // 普通用户查看页面
    }
}

public class AdminUser extends AbstractUser {
    @Override
    public void access(Page page) {
        // 管理员查看页面 + 管理员特权操作
    }
}

权限差异通过 PermissionService 来做限制,而不是在继承关系里硬编码。

小结: 里氏替换原则提醒我们:

  • 继承要合理,子类不能削弱父类功能;
  • 语义必须一致NormalUserUser,就必须至少能做用户能做的事;
  • 权限、条件限制应交由 组合/策略 去实现,而不是破坏继承关系。

4、接口隔离原则(ISP)

定义: 不要让一个接口承担过多职责,接口应该小而精,按需设计,避免强迫实现无关方法。

常见反例(业务场景) : 在一个通知系统里,我们定义了一个大接口:

arduino 复制代码
public interface NotificationService {
    void sendEmail(String to, String content);
    void sendSms(String to, String content);
    void sendPush(String to, String content);
}

问题:

  • 如果一个类只需要实现短信通知,就被迫同时实现 sendEmailsendPush 方法,哪怕方法体是空的,也破坏了接口的纯洁性;
  • 当新增一种通知方式(如站内信)时,所有实现类都要跟着修改。虽然 Java 8 之后接口可以增加默认方法,避免了强制实现,但问题依旧存在:接口方法越来越多,职责越来越混乱,接口本身变得臃肿;
  • 接口变成了"胖接口",违背了接口隔离原则

优化方案: 将接口拆分为更小的接口,按需实现:

arduino 复制代码
public interface EmailNotification {
    void sendEmail(String to, String content);
}

public interface SmsNotification {
    void sendSms(String to, String content);
}

public interface PushNotification {
    void sendPush(String to, String content);
}

这样:

  • 只发短信的类只实现 SmsNotification
  • 只发推送的类只实现 PushNotification
  • 如果要同时支持多渠道,可以实现多个接口。

小结 : 接口隔离原则强调 精简接口,职责单一

  • 开发者只需实现与自身业务相关的接口,避免冗余方法;
  • 当业务扩展时,不会牵连无关实现类;
  • 从架构角度看,接口越小,系统的灵活性和可维护性就越高。

5、依赖倒置原则(DIP)

定义: 高层模块不应该依赖底层模块,二者都应该依赖于抽象。

常见反例(业务场景) : 假设多个业务类(如 UserServiceOrderServiceCouponService)都直接依赖 SmsNotification 发送短信通知:

typescript 复制代码
public class UserService {
    private SmsNotification smsNotification = new SmsNotification();

    public void registerUser(String userId) {
        // 业务逻辑
        smsNotification.send(userId, "注册成功");
    }
}

问题:

  • 如果后续改成 邮箱通知站内信通知,这些服务类都要修改,耦合度极高。
  • 系统缺乏灵活性,扩展新通知方式时改动面巨大。

优化方案: 引入抽象

  1. 定义 Notification 接口
  2. 各业务类依赖 Notification 接口,而不是具体实现
  3. 通过依赖注入(DI)传入不同实现类(短信、邮箱、站内信等)
typescript 复制代码
public interface Notification {
    void send(String userId, String message);
}

public class SmsNotification implements Notification {
    @Override
    public void send(String userId, String message) {
        System.out.println("短信通知: " + message);
    }
}

public class EmailNotification implements Notification {
    @Override
    public void send(String userId, String message) {
        System.out.println("邮箱通知: " + message);
    }
}

public class UserService {
    private final Notification notification;

    // 构造注入
    public UserService(Notification notification) {
        this.notification = notification;
    }

    public void registerUser(String userId) {
        // 业务逻辑
        notification.send(userId, "注册成功");
    }
}

这样,如果未来新增 PushNotification ,只需要新增实现类,并在 Spring 配置里替换注入方式即可,UserService 无需修改。

小结 : 依赖倒置的核心价值是 面向接口编程,高层模块只关心抽象能力,不关心底层实现。这样系统更灵活、可扩展。


6、迪米特法则(LOD,最少知道原则)

定义: 一个对象应当尽量少了解其他对象的内部细节,只与"直接朋友"交互。

常见反例(业务场景) : 假设业务层直接操作通知发送的底层对象:

typescript 复制代码
public class UserService {
    private SmsNotification smsNotification;

    public void registerUser(String userId) {
        // 短信通知业务封装过于开放,为了"自由和扩展",调用方必须直接操作底层 Sender 和 Connection,
        // 相当于让业务层自己实现了发短信的功能,耦合度高,扩展性差
        smsNotification.getSender().getConnection().sendMessage(userId, "注册成功");
    }
}

问题:

  • UserService 依赖了 SenderConnection 等一堆内部对象。
  • 一旦底层 SmsNotification 内部结构发生变化(比如换成第三方 API),业务层也要跟着改。
  • 调用链条过长,耦合严重。

优化方案 : 让 Notification 对外只暴露必要方法,隐藏内部实现细节:

typescript 复制代码
public interface Notification {
    void send(String userId, String message);
}

public class SmsNotification implements Notification {
    private Sender sender;

    @Override
    public void send(String userId, String message) {
        sender.getConnection().sendMessage(userId, message);
    }
}

public class UserService {
    private final Notification notification;

    public UserService(Notification notification) {
        this.notification = notification;
    }

    public void registerUser(String userId) {
        // 业务逻辑
        notification.send(userId, "注册成功");
    }
}

这样:

  • UserService 只需要知道 Notification 提供的 send 方法。
  • 内部 SenderConnection 的变更对业务层透明。
  • 系统耦合度降低,扩展性提升。

小结 : 迪米特法则的核心是 隐藏不必要的复杂性,让调用方只和直接的抽象交互,避免依赖过多的中间细节。


设计原则 vs 设计模式

1. 定义层面

  • 设计原则(Design Principles) 是一种 思想和指导方针 ,告诉你"写代码应该遵循什么样的规则",更偏理念层,比如"一个类只做一件事"(单一职责)。
  • 设计模式(Design Patterns) 是一种 具体的可复用解决方案 ,告诉你"遇到某类问题可以用什么样的代码结构去解决",更偏实现层,比如"用策略模式来扩展支付方式"。

2. 关注点不同

  • 设计原则 → 注重架构质量和代码规范

    • 更关注系统的可维护性、扩展性、解耦性。
    • 偏"高层次",像指导思想。
    • 举例:开闭原则告诉我们"要通过扩展而不是修改来应对需求变化"。
  • 设计模式 → 注重具体问题的解决办法

    • 更关注"怎么写代码"来实现这些原则。
    • 偏"实践层",是原则的落地实现。
    • 举例:策略模式就是开闭原则的落地方案之一。

3. 是否需要代码实现

  • 设计原则 :本身不需要代码实现,它是代码背后的思想。比如 SRP(单一职责),你即使没有用设计模式,也能写出符合原则的代码。
  • 设计模式 :一定需要代码结构的组织,比如类图、接口、继承、组合等,才能体现出模式。

4. 抽象 vs 具体

  • 原则 → 偏抽象,更通用,不局限于某种语言或框架。
  • 模式 → 偏具体,往往结合语言特性、编程习惯(比如 Java 常见的工厂模式、观察者模式)。

总结

本文介绍了六大设计原则(SRP、OCP、LSP、ISP、DIP、LOD),并结合实际业务场景说明了违反原则可能带来的问题以及优化方案。总结核心要点如下:

  1. 设计原则是代码背后的思想 它们指导我们如何组织代码、设计架构、降低耦合、提升可维护性和扩展性。

  2. 设计模式是原则的落地实现 模式提供了可复用的具体方案,让原则在代码中得以体现。理解原则可以让你更灵活地运用模式,而不是照搬。

  3. 遵循原则可以带来显著价值

    • 代码更清晰、职责更单一
    • 系统更易扩展,修改风险降低
    • 团队协作更高效,减少冲突和误解
    • 系统耦合度低,后期维护成本更可控
  4. 原则和模式相辅相成 在实际开发中,先理解设计原则,再选择合适的设计模式去实现,会让你的系统架构更加稳健、灵活、可持续。

最终目标不是死板地记住规则,而是养成以原则思考、用模式实现的能力。这样,你写出的每一行代码都更有价值,系统也更容易演进。

相关推荐
七夜zippoe2 小时前
微服务配置中心高可用设计:从踩坑到落地的实战指南(二)
微服务·架构·php
青草地溪水旁3 小时前
设计模式(C++)详解——迭代器模式(1)
c++·设计模式·迭代器模式
hello_2503 小时前
GitOps:一种实现云原生的持续交付模型
架构·argocd
青草地溪水旁3 小时前
设计模式(C++)详解——迭代器模式(2)
java·c++·设计模式·迭代器模式
苍老流年3 小时前
1. 设计模式--工厂方法模式
设计模式·工厂方法模式
PaoloBanchero3 小时前
Unity 虚拟仿真实验中设计模式的使用 ——策略模式(Strategy Pattern)
unity·设计模式·策略模式
PaoloBanchero3 小时前
Unity 虚拟仿真实验中设计模式的使用 —— 观察者模式(Observer Pattern)
观察者模式·unity·设计模式
Mr_WangAndy3 小时前
C++设计模式_创建型模式_单件模式
c++·单例模式·设计模式
笨手笨脚の3 小时前
设计模式-享元模式
设计模式·享元模式·结构型设计模式·设计模式之美