一、单一职责原则:一个类只做一件事
定义:一个类应该仅有一个引起它变化的原因——换句话说,一个类只负责一个功能领域的职责。
新手常踩坑:把用户管理、订单处理、支付逻辑全塞到一个BusinessService
类里,结果修改用户手机号校验规则时,不小心改坏了订单状态更新逻辑。

反例:职责混杂的“万能类”
// 反例:一个类同时处理用户和订单
public class BadBusinessService {
// 处理用户注册
public void registerUser(String username) {
System.out.println("用户" + username + "注册成功");
}
// 处理订单创建
public void createOrder(String orderId) {
System.out.println("订单" + orderId + "创建成功");
}
}
问题:修改registerUser
的逻辑(比如加验证码),需要重新测试整个类——包括完全不相关的createOrder
,风险极高。
正例:拆分后的单一职责类
// 正例:拆分用户服务
public class UserService {
public void registerUser(String username) {
System.out.println("用户" + username + "注册成功");
}
}
// 拆分订单服务
public class OrderService {
public void createOrder(String orderId) {
System.out.println("订单" + orderId + "创建成功");
}
}
好处:修改用户逻辑只影响UserService
,订单逻辑独立,维护成本直接减半。
二、开闭原则:对扩展开放,对修改关闭
定义:软件实体(类、模块、函数等)应该可以扩展(新增功能),但不可修改(不改变原有代码)。
新手痛点:为了加新功能,直接改原有类——比如原本只有微信支付,要加支付宝时,把WeChatPay
类改成PayService
,结果改崩了微信支付的逻辑。
反例:直接修改原有类
// 反例:原有微信支付类
public class WeChatPay {
public void pay(double amount) {
System.out.println("微信支付:" + amount + "元");
}
}
// 加支付宝时直接修改——风险!
public class PayService {
public void payWeChat(double amount) { ... }
public void payAlipay(double amount) { ... } // 新增方法,修改了原有类
}
问题:修改原有类会引入 regression(回归)风险,比如不小心删了微信支付的签名逻辑。
正例:用抽象扩展新功能
// 正例:定义支付抽象接口
public interface Pay {
void pay(double amount);
}
// 微信支付实现
public class WeChatPay implements Pay {
@Override
public void pay(double amount) { ... }
}
// 支付宝支付实现(扩展,不修改原有代码)
public class Alipay implements Pay {
@Override
public void pay(double amount) {
System.out.println("支付宝支付:" + amount + "元");
}
}
// 使用时通过工厂或DI获取实例
public class PayFactory {
public static Pay getPayMethod(String type) {
if ("wechat".equals(type)) return new WeChatPay();
if ("alipay".equals(type)) return new Alipay();
throw new IllegalArgumentException("未知支付方式");
}
}
好处:新增支付方式(比如银联)只需加一个UnionPay implements Pay
,完全不碰原有代码——风险为0。
三、里氏替换原则:子类能无缝替换父类
定义:所有引用父类的地方,必须能透明地使用其子类的对象(子类不能破坏父类的契约)。
经典反例:正方形继承矩形——矩形有setWidth
和setHeight
,正方形的setWidth
会同时改height
,导致面积计算错误。
反例:违反契约的子类
// 父类:矩形
public class Rectangle {
protected int width;
protected int height;
public void setWidth(int width) { this.width = width; }
public void setHeight(int height) { this.height = height; }
public int getArea() { return width * height; }
}
// 子类:正方形(违反里氏替换)
public class Square extends Rectangle {
@Override
public void setWidth(int width) {
this.width = width;
this.height = width; // 同时改height,破坏父类逻辑
}
@Override
public void setHeight(int height) {
this.width = height;
this.height = height;
}
}
// 测试:用子类替换父类,结果出错
public class Test {
public static void main(String[] args) {
Rectangle rect = new Square(); // 子类替换父类
rect.setWidth(2);
rect.setHeight(3);
System.out.println(rect.getArea()); // 期望6,实际9!
}
}
问题:子类重写父类方法时,改变了父类的核心逻辑(矩形的宽高是独立的),导致替换后结果错误。
正例:用组合代替继承
// 正例:放弃继承,用组合实现正方形
public class Square {
private Rectangle rectangle; // 组合矩形
public Square() {
this.rectangle = new Rectangle();
}
public void setSide(int side) {
rectangle.setWidth(side);
rectangle.setHeight(side);
}
public int getArea() {
return rectangle.getArea();
}
}
好处:正方形不再继承矩形,而是使用矩形的功能,既保留了矩形的逻辑,又避免了契约破坏。
四、依赖倒置原则:依赖抽象,不依赖具体
定义:高层模块(业务逻辑)不应该依赖低层模块(具体实现),两者都应该依赖抽象;抽象不应该依赖细节,细节应该依赖抽象。
新手误区:直接在Service里new一个具体的Dao——比如UserService
直接newMySQLUserDao
,结果要换Oracle时,得改所有Service的代码。
反例:依赖具体实现
// 反例:Service依赖具体的MySQLDao
public class UserService {
private MySQLUserDao userDao = new MySQLUserDao(); // 直接依赖具体类
public User getUserById(int id) {
return userDao.findById(id);
}
}
// MySQL实现
public class MySQLUserDao {
public User findById(int id) { ... }
}
问题:换Oracle时,需要修改UserService
的代码——把MySQLUserDao
改成OracleUserDao
,违反开闭原则。
正例:依赖抽象(接口)
// 正例:定义抽象Dao接口
public interface UserDao {
User findById(int id);
}
// MySQL实现
public class MySQLUserDao implements UserDao { ... }
// Oracle实现
public class OracleUserDao implements UserDao { ... }
// Service依赖接口,通过注入获取实现
public class UserService {
private UserDao userDao;
// 构造器注入(或Setter/DI框架)
public UserService(UserDao userDao) {
this.userDao = userDao;
}
public User getUserById(int id) {
return userDao.findById(id);
}
}
// 使用时:
UserDao mysqlDao = new MySQLUserDao();
UserService service = new UserService(mysqlDao); // 换Oracle只需传OracleDao
好处:Service完全不关心Dao的具体实现,换数据库时只需换注入的Dao实例——不用改一行Service代码。
五、接口隔离原则:不强迫依赖不需要的接口
定义:客户端不应该依赖它不需要的接口(一个类对另一个类的依赖应该建立在最小的接口上)。
新手踩坑:定义一个大而全的接口,比如BigInterface
包含read()
、write()
、delete()
,结果只读的客户端被迫实现write()
和delete()
——哪怕它用不到。
反例:臃肿的大接口
// 反例:大接口包含所有操作
public interface BigInterface {
void read();
void write();
void delete();
}
// 只读客户端——被迫实现不需要的方法
public class ReadOnlyClient implements BigInterface {
@Override public void read() { ... }
@Override public void write() { throw new UnsupportedOperationException(); } // 无用
@Override public void delete() { throw new UnsupportedOperationException(); } // 无用
}
问题:客户端被迫实现不需要的方法,代码冗余且易出错(比如不小心调用了write()
)。
正例:拆分小接口
// 正例:拆分三个单一职责的小接口
public interface Readable { void read(); }
public interface Writable { void write(); }
public interface Deletable { void delete(); }
// 只读客户端——只依赖需要的接口
public class ReadOnlyClient implements Readable {
@Override public void read() { ... }
}
// 读写客户端——依赖两个接口
public class ReadWriteClient implements Readable, Writable { ... }
好处:客户端只依赖自己需要的接口,没有冗余代码,也避免了误调用不需要的方法。
六、迪米特法则:最少知道原则
定义:一个对象应该对其他对象保持最少的了解(只和直接的朋友通信)。
解释:“直接的朋友”指:当前对象本身、方法的参数、方法的返回值、当前对象的成员变量——除此之外的对象,都不是朋友,不要和它们通信。
新手问题:一个类直接操作另一个类的内部结构——比如UserService
直接访问Order
的orderItems
列表,导致Order
的修改影响UserService
。
反例:过度依赖朋友的朋友
// 反例:UserService直接访问Order的内部列表
public class UserService {
public void printUserOrders(User user) {
List<Order> orders = user.getOrders();
for (Order order : orders) {
// 直接访问order的orderItems(朋友的朋友)
List<OrderItem> items = order.getOrderItems();
for (OrderItem item : items) {
System.out.println(item.getProductName());
}
}
}
}
public class User {
private List<Order> orders; // 成员变量,是User的朋友
// getter
}
public class Order {
private List<OrderItem> orderItems; // 成员变量,是Order的朋友
// getter
}
问题:如果Order
改了orderItems
的存储方式(比如从List改成Set),UserService
的printUserOrders
就得跟着改——因为它直接访问了orderItems
。
正例:封装内部细节
// 正例:让Order自己负责打印明细
public class Order {
private List<OrderItem> orderItems;
// 封装内部细节,对外提供方法
public void printItems() {
for (OrderItem item : orderItems) {
System.out.println(item.getProductName());
}
}
}
// UserService只和Order通信,不碰它的内部
public class UserService {
public void printUserOrders(User user) {
List<Order> orders = user.getOrders();
for (Order order : orders) {
order.printItems(); // 调用Order的方法,不访问内部列表
}
}
}
好处:Order
的内部结构(比如orderItems
的类型)再怎么改,UserService
都不用变——因为它只调用printItems()
方法。
七、合成复用原则:优先用组合/聚合,不用继承
定义:尽量使用组合(has-a
)或聚合(contains-a
)关系来实现代码复用,而不是继承(is-a
)。
为什么?:继承会让子类依赖父类的实现,耦合度高;而组合是“使用”其他对象的功能,耦合度低。
反例:过度使用继承
// 反例:用继承实现“飞翔的鸭子”
public class Duck {
public void quack() { ... }
public void swim() { ... }
public void fly() { ... } // 鸭子会飞?
}
// 橡皮鸭——继承Duck,但不会飞
public class RubberDuck extends Duck {
@Override public void fly() { throw new UnsupportedOperationException(); } // 无用
}
问题:橡皮鸭继承了Duck的fly()
方法,但它根本不会飞——被迫重写并抛出异常,不符合里氏替换原则。
正例:用组合实现行为
// 正例:定义行为接口
public interface FlyBehavior { void fly(); }
public interface QuackBehavior { void quack(); }
// 具体行为实现
public class FlyWithWings implements FlyBehavior { @Override public void fly() { ... } }
public class NoFly implements FlyBehavior { @Override public void fly() { ... } }
public class Quack implements QuackBehavior { @Override public void quack() { ... } }
public class Squeak implements QuackBehavior { @Override public void quack() { ... } }
// 鸭子类——组合行为
public class Duck {
private FlyBehavior flyBehavior;
private QuackBehavior quackBehavior;
// 构造器注入行为
public Duck(FlyBehavior flyBehavior, QuackBehavior quackBehavior) {
this.flyBehavior = flyBehavior;
this.quackBehavior = quackBehavior;
}
public void performFly() { flyBehavior.fly(); }
public void performQuack() { quackBehavior.quack(); }
public void swim() { ... }
}
// 橡皮鸭——组合“不会飞”和“吱吱叫”
public class RubberDuck extends Duck {
public RubberDuck() {
super(new NoFly(), new Squeak());
}
}
// 野鸭——组合“会飞”和“嘎嘎叫”
public class WildDuck extends Duck {
public WildDuck() {
super(new FlyWithWings(), new Quack());
}
}
好处:鸭子的行为可以动态改变(比如给橡皮鸭加一个“装电池会飞”的功能,只需换FlyBehavior
),而且没有继承的耦合问题。
原则速查表:快速定位核心
原则 | 核心口诀 | 解决的问题 |
---|---|---|
单一职责 | 一个类,一件事 | 类职责混杂,修改风险高 |
开闭 | 扩功能,不改代码 | 加新功能时改原有代码引风险 |
里氏替换 | 子类能换父类,结果不变 | 子类破坏父类逻辑 |
依赖倒置 | 依赖抽象,不依赖具体 | 换实现时改大量代码 |
接口隔离 | 小接口,不臃肿 | 客户端被迫实现不需要的方法 |
迪米特 | 只和直接朋友说话 | 过度依赖其他类的内部结构 |
合成复用 | 组合优先,继承次之 | 继承导致的高耦合 |
最后想跟你说:原则不是教条,是权衡的工具。比如小项目不用过度拆分接口,否则会增加复杂度;但中大型项目,遵守这些原则能帮你避免“牵一发而动全身”的噩梦。最重要的是——写完代码后,问自己:“如果要加新功能,我需要改多少行?” 如果答案是“只加几行,不用改旧代码”,那你就掌握了面向对象的精髓。
原创文章,作者:,如若转载,请注明出处:https://zube.cn/archives/169