MENU

今日必玩-每日活动精选推送

Java 设计模式中的六大设计原则详解(带详细代码示例):掌握面向对象的金科玉律

在日常的 Java 面向对象开发中,我们经常会用到各种设计模式,如工厂模式、策略模式、代理模式等等。但你是否深入思考过:这些设计模式的底层逻辑、设计理念究竟是什么?是否存在一套指导标准,来衡量“一个设计是好的”还是“不够合理”?答案是肯定的。在软件工程中,有一套被广泛认可并遵循的面向对象设计的六大原则(Design Principles),它们被誉为设计模式背后的“金科玉律”。

本篇文章将详细讲解这六大原则:开闭原则、单一职责原则、里氏替换原则、迪米特法则、接口隔离原则、依赖倒置原则,通过通俗的语言、清晰的实例来理解它们的内涵和实际意义。

一、OCP 开闭原则(Open Closed Principle)

定义:一个软件实体(类、模块、函数)应该对扩展开放,对修改关闭。

通俗解释:

“对扩展开放”:可以添加新的功能;“对修改关闭”:不应修改已有的代码。

一、开闭原则(OCP)

错误示例:

public class UserDaoImpl {

public void saveUser() {

// 原始 JDBC 实现

System.out.println("保存用户使用 JDBC 实现");

}

}

后续修改为 JNDI 实现:

public class UserDaoImpl {

public void saveUser() {

// 修改为 JNDI 实现

System.out.println("保存用户使用 JNDI 实现");

}

}

问题:修改了已有类,可能影响原有调用和测试,违反开闭原则。

正确示例:

public interface UserDao {

void saveUser();

}

public class JdbcUserDaoImpl implements UserDao {

public void saveUser() {

System.out.println("保存用户使用 JDBC 实现");

}

}

public class JndiUserDaoImpl implements UserDao {

public void saveUser() {

System.out.println("保存用户使用 JNDI 实现");

}

}

好处:扩展新功能只需新增类,而不必修改已有代码,符合 OCP。

错误示例:

假设我们有一个 UserDao 接口及其实现类 UserDaoImpl,采用 JDBC 实现插入操作。后来需求变了,要改用 JNDI。很多初学者会直接修改 UserDaoImpl 类,删除原来的 JDBC 代码,写上 JNDI 的实现。

这种做法违背了开闭原则,因为你修改了已有代码,可能会引发新旧逻辑的不兼容和测试风险。

正确做法:

应该新增一个类 UserDaoJndiImpl,实现同样的接口 UserDao,而不是修改原有的 UserDaoImpl 类。这样可以在使用时通过配置或者工厂类来选择使用哪个实现。

拓展建议:

虽然继承也是一种扩展方式,但不推荐通过继承原有实现类来改写方法逻辑,因为这可能导致 里氏替换原则被破坏。更推荐的方式是新增实现类。

二、SRP 单一职责原则(Single Responsibility Principle)

定义:一个类应该只有一个引起它变化的原因。

通俗解释:

一个类只做一件事,聚焦于单一功能。

二、单一职责原则(SRP)

错误示例:

public class VideoUserService {

public void playVideo(String userType) {

if ("guest".equals(userType)) {

System.out.println("游客播放视频:展示广告");

} else if ("vip".equals(userType)) {

System.out.println("VIP 播放视频:免广告高清");

}

// 更多角色...

}

}

问题:一个类承担多个角色逻辑,难以维护和扩展。

正确示例:

public interface VideoUserService {

void playVideo();

}

public class GuestUserService implements VideoUserService {

public void playVideo() {

System.out.println("游客播放视频:展示广告");

}

}

public class VipUserService implements VideoUserService {

public void playVideo() {

System.out.println("VIP 播放视频:免广告高清");

}

}

好处:每个类专注一个职责,新增角色只需添加实现类。

错误示例:

视频网站中的服务类,里面通过 if-else 判断用户是访客、普通用户、会员或 VIP,然后执行不同的播放逻辑。这样的设计耦合了所有角色的处理逻辑,后续每加一个角色,都要修改这个类。

正确做法:

定义一个接口 VideoUserService,并为每种用户角色创建独立的实现类:如 GuestUserService、NormalUserService、VipUserService,每个类只负责处理对应角色的业务逻辑。

这不仅符合 SRP,也方便扩展新角色(如 SVIP),而无需修改现有逻辑,间接也遵循了开闭原则。

衍生模式:

这种策略根据对象类型进行不同行为处理的方式,本质上是“策略模式”的一个典型实现。

三、LSP 里氏替换原则(Liskov Substitution Principle)

定义:子类对象可以替代父类对象,程序行为不发生改变。

通俗解释:

子类能出现的地方,父类也能正常使用;子类扩展父类功能,但不能违背父类原有行为的预期。

错误示例:

public class CashCard {

public void withdraw(double amount) {

System.out.println("账户扣除:" + amount);

}

}

public class CreditCard extends CashCard {

@Override

public void withdraw(double amount) {

System.out.println("增加信用额度负债:" + amount);

}

}

问题:虽然是继承关系,但语义不一致,导致调用方混淆。

正确示例:

public abstract class BankCard {

public abstract void deposit(double amount);

public abstract void withdraw(double amount);

}

public class CashCard extends BankCard {

public void deposit(double amount) {

System.out.println("存入现金:" + amount);

}

public void withdraw(double amount) {

System.out.println("取出现金:" + amount);

}

}

public class CreditCard extends BankCard {

public void deposit(double amount) {

System.out.println("还款金额:" + amount);

}

public void withdraw(double amount) {

System.out.println("信用透支:" + amount);

}

}

好处:语义清晰,子类替代父类时行为一致。

错误示例:

信用卡继承储蓄卡,看起来合理。但储蓄卡的提现是从账户扣款,而信用卡的提现是增加负债(贷款);储蓄是存款,而信用卡的“存款”其实是还款。这两者在行为语义上完全不同。

直接继承会导致代码语义混乱,违背 LSP。

正确做法:

抽象出一个父类 BankCard,定义通用的行为如 deposit() 和 withdraw()。然后:

CashCard 实现账户真实资金操作;CreditCard 实现透支和还款逻辑(如 loan() 和 repayment());

各自行为清晰,互不干扰。

总结 LSP 四条原则:

子类可以实现父类的抽象方法,但不能重写非抽象方法;子类可以增加自己的独有方法;子类重写方法时,输入参数范围应更宽松;子类输出/返回值应与父类相等或更严格。

作用:

保证行为稳定性;避免继承导致行为冲突;增强程序健壮性;降低未来维护成本。

四、LOD 迪米特法则(Law of Demeter)——最少知道原则

定义:一个对象应当对其他对象有尽可能少的了解。

通俗解释:

类之间低耦合;尽量只和“直接朋友”通信,不要过多依赖其他类的内部结构。

错误示例:

public class Student {

public int getScore() {

return 90;

}

}

public class Principal {

public void report(Student student) {

int score = student.getScore(); // 直接访问细节

System.out.println("学生成绩为:" + score);

}

}

问题:校长类直接依赖学生,耦合过多。

正确示例:

public class Student {

private int score = 90;

public int getScore() {

return score;

}

}

public class Teacher {

private Student student = new Student();

public int getAverageScore() {

return student.getScore();

}

}

public class Principal {

public void report(Teacher teacher) {

int avg = teacher.getAverageScore();

System.out.println("学生平均成绩为:" + avg);

}

}

好处:校长只和老师通信,不依赖学生内部细节,符合最少知道原则。

错误示例:

校长类直接访问学生列表,遍历并统计每个学生成绩,职责太重,知道太多。

正确做法:

由老师类封装学生成绩的总分/平均分等处理逻辑,校长类只调用老师提供的接口,如 getAverageScore()。这样实现:

校长不需知道学生详情;老师负责处理内部细节;耦合关系大幅降低。

五、ISP 接口隔离原则(Interface Segregation Principle)

定义:一个类不应该依赖它不需要的接口。

通俗解释:

与其让类实现一个大接口,不如将接口细化成多个更小、更专一的接口;实现类按需选择实现哪个接口。

错误示例:

public interface Animal {

void fly();

void run();

void swim();

}

public class Dog implements Animal {

public void fly() {} // 鸭子实现,不需要

public void run() {

System.out.println("狗在跑");

}

public void swim() {}

}

问题:Dog 不会飞,却被强制实现 fly(),造成接口污染。

正确示例:

public interface RunnableAnimal {

void run();

}

public interface SwimmableAnimal {

void swim();

}

public class Dog implements RunnableAnimal, SwimmableAnimal {

public void run() {

System.out.println("狗在跑");

}

public void swim() {

System.out.println("狗会游泳");

}

}

好处:实现类只依赖所需接口,避免臃肿。

示例解析:

Java Servlet 中的监听器设计就体现了这一原则:

ServletContextListener 只负责上下文生命周期;HttpSessionListener 只负责 Session;ServletRequestListener 只负责请求。

这样,每个类只实现自己关心的接口,避免实现冗余方法。

实际好处:

避免“臃肿接口”;提高代码的灵活性与可读性;避免修改一个接口影响大量实现类。

六、DIP 依赖倒置原则(Dependency Inversion Principle)

定义:

高层模块不应该依赖底层模块,二者都应该依赖抽象;抽象不应该依赖细节,细节应该依赖抽象。

通俗解释:

面向接口编程;高层业务逻辑不依赖具体实现,而是依赖抽象接口。

错误示例:

public class LotteryService {

public void draw(String type) {

if ("random".equals(type)) {

System.out.println("随机抽奖");

} else if ("weight".equals(type)) {

System.out.println("权重抽奖");

}

}

}

问题:高层模块依赖具体实现,耦合严重。

正确示例:

public interface ILotteryStrategy {

void draw();

}

public class RandomLotteryStrategy implements ILotteryStrategy {

public void draw() {

System.out.println("随机抽奖");

}

}

public class WeightLotteryStrategy implements ILotteryStrategy {

public void draw() {

System.out.println("权重抽奖");

}

}

public class LotteryService {

private ILotteryStrategy strategy;

public LotteryService(ILotteryStrategy strategy) {

this.strategy = strategy;

}

public void doDraw() {

strategy.draw();

}

}

使用:

LotteryService service = new LotteryService(new RandomLotteryStrategy());

service.doDraw();

好处:面向接口,解耦实现;方便扩展和测试。

错误示例:

抽奖系统直接在逻辑类中写了“随机抽奖”和“权重抽奖”的代码,并通过 if-else 控制行为。这种做法紧耦合,不便于扩展。

正确做法:

抽象一个接口 ILotteryStrategy;具体实现类 RandomLotteryStrategy 和 WeightLotteryStrategy 实现该接口;抽奖服务类接收 ILotteryStrategy 作为参数;外部根据策略选择具体实现传入服务类。

优势:

解耦业务逻辑与策略实现;新增策略无需改动现有逻辑;灵活、易扩展、方便测试。

总结:六大设计原则速记

原则名称英文缩写核心思想开闭原则OCP对扩展开放,对修改关闭单一职责SRP一个类只做一件事里氏替换LSP子类可替换父类且行为一致迪米特法则LOD / Demeter只和“直接朋友”通信,最少知道接口隔离ISP接口应小而专一依赖倒置DIP高层依赖抽象,面向接口编程

写在最后

六大设计原则并不是某个工具框架强制规定的,而是在长期的面向对象设计与工程实践中,逐渐被开发者总结出来的一套“通用开发指导思想”。它们是架构的基础、设计模式的根、也是提升代码质量的核心路径。

在日常编码过程中,可能你没有刻意遵循,但只要代码结构合理、模块清晰、逻辑独立,那很可能你已经在“潜移默化”中遵循了这些原则。

真正写出优雅、可维护、可扩展的代码,绝不是多用几个设计模式,而是深入理解并践行这些设计原则。这才是掌握设计模式的本质。

Copyright © 2022 今日必玩-每日活动精选推送 All Rights Reserved.