面向对象的设计原则

面试的时候回头看到这些理论知识,结合自己之前的开发经验感慨万千。原来前辈们列出了这么多优秀的条条框框,之前也看过,但是没啥感觉。当你遇到问题的时候,你会发现你的解决方式和这些理论不谋而合。虽然是一些简单的理论知识,但是项目组中的很多人真的不一定会知道怎么用。万丈高楼平地起, 细节化聊聊面向对象的设计准则.

1. 单一职责原则(Single Responsibility Principle )

一个类应该只有一个引起它变化的原因,即一个类应该只负责一项职责。

单一职责不是“只有一个功能”,而是“只对一个变化负责”。你可以做很多事,只要这些事都属于“同一个责任人”负责的范围。(你可以理解单一职责是做一类事情)

例子

比如 UserService 聚焦用户注册、登录、权限等“用户管理”逻辑;而日志记录、邮件发送这种职责,我会抽到 LogServiceNotificationService。 这样改某一块功能,不会牵动整个类,系统的可维护性更高。

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,就会导致程序错误。

合理继承的做法

  1. 子类方法行为不能违反父类方法的语义约定
  2. 子类可以返回 父类的兼容结果
  3. 子类可以抛出 更少/更窄的异常,但不能更多
  4. 子类应该 不增加使用限制(比如“只接受正数参数”)

我理解里氏替换原则是子类必须可以无缝替换父类,否则就不该继承。比如我们在业务中抽象了某个通用的服务接口,如果某个子类实现不兼容调用方的逻辑(比如改了返回值语义、增加参数限制),那其实就是在破坏继承关系。扩展不同行为要采用了组合模式,而不是继承。

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();

我理解最少知识原则就是“一个模块尽量只了解跟它直接相关的对象,尽量少接触不该接触的对象”。封装好暴露接口,不让外部直接访问多层结构。这样做可以降低耦合,提升可维护性,也符合面向对象的封装思想。