面试的时候回头看到这些理论知识,结合自己之前的开发经验感慨万千。原来前辈们列出了这么多优秀的条条框框,之前也看过,但是没啥感觉。当你遇到问题的时候,你会发现你的解决方式和这些理论不谋而合。虽然是一些简单的理论知识,但是项目组中的很多人真的不一定会知道怎么用。万丈高楼平地起, 细节化聊聊面向对象的设计准则.
1. 单一职责原则(Single Responsibility Principle )
一个类应该只有一个引起它变化的原因,即一个类应该只负责一项职责。
单一职责不是“只有一个功能”,而是“只对一个变化负责”。你可以做很多事,只要这些事都属于“同一个责任人”负责的范围。(你可以理解单一职责是做一类事情)
例子:
比如 UserService
聚焦用户注册、登录、权限等“用户管理”逻辑;而日志记录、邮件发送这种职责,我会抽到 LogService
或 NotificationService
。
这样改某一块功能,不会牵动整个类,系统的可维护性更高。
2. 开放封闭原则(Open/Closed Principle)
对扩展开放,对修改封闭
示例(错误示范):
public class OrderProcessor {
public void process(Order order) {
if (order.getType().equals("NORMAL")) {
// 普通订单处理
} else if (order.getType().equals("FLASH_SALE")) {
// 秒杀订单处理
} else if (order.getType().equals("GROUP_BUY")) {
// 拼团订单处理
}
}
如果新增一个需求,比如预售订单处理,又得加一个 else-if
,这就违反了开闭原则。其实这在业务里还可以去修改,如果你在写底层的依赖包的时候,由于这种if-else逻辑写死的话会导致依赖包的变动,很麻烦,扩展性太差。
解决方式(策略模式)去实现开闭原则:
// 1. 抽象策略接口
public interface OrderHandler {
boolean supports(String orderType);
void process(Order order);
}
// 2. 具体策略类
public class NormalOrderHandler implements OrderHandler {
public boolean supports(String orderType) {
return "NORMAL".equals(orderType);
}
public void process(Order order) {
// 普通订单处理逻辑
}
}
public class FlashSaleOrderHandler implements OrderHandler {
public boolean supports(String orderType) {
return "FLASH_SALE".equals(orderType);
}
public void process(Order order) {
// 秒杀订单处理逻辑
}
}
//3.客户端调用
public class OrderProcessor {
private List<OrderHandler> handlers; //这里在SpringBoot项目中,通常使用Autowired自动注入一个list;
public OrderProcessor(List<OrderHandler> handlers) {
this.handlers = handlers;
}
public void process(Order order) {
for (OrderHandler handler : handlers) {
if (handler.supports(order.getType())) {
handler.process(order);
return;
}
}
throw new RuntimeException("不支持的订单类型: " + order.getType());
}
}
如果这时候新增一个业务需求, 只需要实现OrderHandler
接口好了, 不用改 OrderProcessor,很简单的完成了扩展,这就是对扩展开放.
3. 里氏替换原则(Liskov Substitution Principle)
子类对象必须能替换父类对象,且行为保持一致,逻辑不变,预期一致。
子类对象应该能够替换掉所有父类对象, 凡是父类能胜任的地方,换成子类也必须一样能跑得通,逻辑不变、预期一致。例子:一个正方形是一个矩形,但如果修改一个矩形的高度和宽度时,正方形的行为应该如何改变就是一个违反里氏替换原则的例子。[继承不是为了重写,而是为了“完全兼容地扩展”。]
反例:
class FileReader {
public void readFile(String path) throws IOException {
// 读文件
}
}
class DatabaseReader extends FileReader {
@Override
public void readFile(String path) throws SQLException {
// ❌ 改为读数据库,抛出了不同的异常
}
}
FileReader reader = new DatabaseReader();
reader.readFile("data.db");
如果调用方只 try-catch IOException
,而 DatabaseReader
抛了 SQLException
,就会导致程序错误。
合理继承的做法
- 子类方法行为不能违反父类方法的语义约定
- 子类可以返回 父类的兼容结果
- 子类可以抛出 更少/更窄的异常,但不能更多
- 子类应该 不增加使用限制(比如“只接受正数参数”)
我理解里氏替换原则是子类必须可以无缝替换父类,否则就不该继承。比如我们在业务中抽象了某个通用的服务接口,如果某个子类实现不兼容调用方的逻辑(比如改了返回值语义、增加参数限制),那其实就是在破坏继承关系。扩展不同行为要采用了组合模式,而不是继承。
4. 接口隔离原则(Interface Segregation Principle)
客户端不应该依赖它不需要的接口,即接口应该小而专。一个类对另一个类的依赖,应该建立在最小的接口上。
胖接口不可取,职责专一才优雅。 用多个小接口替代大接口,能让系统更解耦、更灵活。
虽然接口隔离原则强调“小而专”的接口设计,但我认为在实际项目中不能机械拆分,关键是找到合适的粒度,如果拆的太细也不好维护。[基于较好的方法论去实践,会让落地效果更好,但是得具体问题具体分析,要灵活应用]
5. 依赖倒置原则(Dependency Inversion Principle)
高层模块不应该依赖低层模块,二者都应该依赖于抽象,抽象不应该依赖于细节,细节应该依赖于抽象。
//定义接口抽象
public interface PaymentGateway {
void process(Order order);
}
//由具体实现类实现接口
public class AlipayService implements PaymentGateway {
public void process(Order order) {
// 支付逻辑
}
}
//高层模块依赖接口 + 注入实现
public class OrderService {
private final PaymentGateway paymentGateway;
public OrderService(PaymentGateway paymentGateway) {
this.paymentGateway = paymentGateway;
}
public void pay(Order order) {
paymentGateway.process(order);
}
}
依赖倒置原则是“解耦之王”,让上层只关心“做什么”,不关心“怎么做”。
Note: 虽然有时候在开发是注入实现类代码也能正常使用,但是会有隐患。因为之前项目中就发生了问题,因为有同事加了一个注解,导致这个实现类没有按照代理对象的逻辑执行。
6. 最少知识原则(Law of Demeter)
一个对象应当只与直接朋友交互,减少对外部细节的依赖。比如: 如果某对象不是当前对象的直接成员变量 / 方法参数 / 返回值,就不要直接调用它的方法。
反例:
// 直接链式调用,跨了两层结构,耦合严重
//一旦 Address 或 Customer 改结构,Order 调用方就得跟着大改,出问题的概率高。
order.getCustomer().getAddress().getCity();
改进:
public class Order {
private Customer customer;
//这里最好不要用get方法,因为get方法在序列化时会生成一个字段的。
public String retrieveCustomerCity() {
if(cutomer == null){
return null;
}
return customer.getCity(); // 抽象封装
}
}
//一层调用,屏蔽内部结构
order.getCustomerCity();
我理解最少知识原则就是“一个模块尽量只了解跟它直接相关的对象,尽量少接触不该接触的对象”。封装好暴露接口,不让外部直接访问多层结构。这样做可以降低耦合,提升可维护性,也符合面向对象的封装思想。