设计原则面试高频题+Java代码实践

设计原则面试高频题+Java代码实践(吃透面试+落地开发)

结合面试高频场景,我会先整理核心面试题+标准答案(覆盖90%面试考点),再用Java代码逐一演示七大设计原则的落地用法,兼顾“面试答题”和“实际开发”双重需求。

一、设计原则高频面试题+标准答案

基础必问(80%面试官会问)

1. 请说说SOLID五大设计原则分别是什么,各自的核心思想?

答案
SOLID是面向对象设计的核心原则,拆解为5个原则:

  • 单一职责(SRP):一个类只负责一个职责,变更原因唯一,核心是“高内聚”,避免一个类承担过多功能导致修改牵一发而动全身;
  • 开闭原则(OCP):对扩展开放、对修改关闭,核心是依赖抽象而非具体实现,新增功能通过扩展子类/实现类完成,不修改原有稳定代码;
  • 里氏替换(LSP):子类可无缝替换父类且程序行为不变,核心是保证继承的合理性,子类不能破坏父类的约定(如不能重写父类非抽象方法、不能缩小方法的访问权限);
  • 接口隔离(ISP):客户端不依赖不需要的接口,核心是将臃肿接口拆分为细粒度接口,避免接口变更影响无关客户端;
  • 依赖倒置(DIP):高层模块不依赖低层模块,二者都依赖抽象;抽象不依赖细节,细节依赖抽象,核心是“面向接口编程”,解除模块间的强耦合。

2. 开闭原则是设计原则的核心目标,如何在代码中落地开闭原则?

答案
开闭原则的核心是“抽象化”,落地关键有3点:

  1. 定义抽象层(接口/抽象类):将稳定的业务逻辑抽象为接口,封装不变的行为;
  2. 具体实现类扩展抽象层:新增功能时,编写新的实现类继承/实现抽象层,不修改原有实现;
  3. 依赖注入解耦:高层模块通过抽象层调用功能,而非直接依赖具体实现类。
    举例:支付场景中,定义PayService接口,原有AlipayPayService实现支付宝支付,新增微信支付时,只需新增WechatPayService实现PayService,无需修改原有支付逻辑。

3. 里氏替换原则的核心要求是什么?举一个违反里氏替换的例子。

答案
核心要求:所有引用父类的地方,替换为子类后程序的行为和逻辑不变,具体约束:

  • 子类不能重写父类的非抽象方法;
  • 子类不能扩大方法的前置条件(如父类方法参数是Object,子类不能改为String);
  • 子类不能缩小方法的后置条件(如父类返回List,子类不能返回ArrayList并限制元素类型);
  • 子类不能抛出父类未声明的异常(运行时异常除外)。

反例

// 父类:鸟类定义飞的行为
class Bird {
    public void fly() {
        System.out.println("鸟类飞行");
    }
}
// 子类:鸵鸟,违反里氏替换
class Ostrich extends Bird {
    @Override
    public void fly() {
        throw new UnsupportedOperationException("鸵鸟不会飞");
    }
}
// 调用方:依赖父类,但替换子类后报错
public class Test {
    public static void letBirdFly(Bird bird) {
        bird.fly();
    }
    public static void main(String[] args) {
        letBirdFly(new Ostrich()); // 抛出异常,违反里氏替换
    }
}

修正方案:拆分抽象层,定义Flyable接口,只有会飞的鸟实现该接口,鸵鸟不实现。

4. 组合/聚合复用原则(CRP)和继承的区别?为什么优先组合?

答案

维度 继承(is-a) 组合/聚合(has-a)
耦合程度 强耦合(父类变子类必变) 弱耦合(只需调整依赖)
复用灵活性 静态复用(编译期确定) 动态复用(运行时可替换)
代码冗余 易产生冗余(继承无关方法) 无冗余(按需依赖)

优先组合的原因

  1. 避免继承的“侵入性”:子类会继承父类所有公开方法,即使不需要;
  2. 降低耦合:组合可在运行时替换依赖的对象,继承一旦确定无法修改;
  3. 符合开闭原则:新增功能只需替换组合的对象,无需修改原有类。

进阶扩展(面试加分题)

5. 迪米特法则(最少知识原则)如何落地?举代码例子说明。

答案
迪米特法则核心:对象只和直接关联的对象通信,不访问“陌生人”的内部属性/方法,落地要点:

  1. 隐藏内部细节:通过公共方法封装内部依赖,不暴露成员变量;
  2. 减少间接调用:避免A.getB().getC().doSomething()的链式调用;
  3. 降低依赖范围:只依赖必要的对象,不依赖无关对象。

反例(违反迪米特)

// 订单类需要获取用户地址,直接访问用户的地址对象(间接依赖)
class User {
    private Address address;
    public Address getAddress() { return address; }
}
class Address {
    private String detail;
    public String getDetail() { return detail; }
}
class Order {
    private User user;
    // 违反:Order依赖了User的内部对象Address(陌生人)
    public String getUserAddress() {
        return user.getAddress().getDetail();
    }
}

正例(符合迪米特)

class User {
    private Address address;
    // 封装内部细节,对外提供直接方法
    public String getAddressDetail() {
        return address.getDetail();
    }
}
class Address {
    private String detail;
    public String getDetail() { return detail; }
}
class Order {
    private User user;
    // 只和直接朋友User通信,不接触Address
    public String getUserAddress() {
        return user.getAddressDetail();
    }
}

6. 设计原则之间的关联是什么?实际开发中如何取舍?

答案

  • 关联关系:开闭原则是最终目标,单一职责、接口隔离、依赖倒置是实现开闭原则的手段;里氏替换是继承的约束(保证抽象层可用);迪米特法则是降低耦合的补充;组合复用是减少继承耦合的具体方式。
  • 取舍原则
    1. 拒绝过度设计:小项目/简单场景无需严格遵循所有原则(如一个工具类无需拆分为多个接口);
    2. 核心优先:优先保证“单一职责+开闭原则+依赖倒置”,这三个是降低维护成本的核心;
    3. 平衡复杂度:抽象层不宜过多(如一个功能拆多层接口会增加理解成本);
    4. 结合场景:高频变更的模块严格遵循原则,稳定模块可简化实现。

二、七大设计原则Java代码实践(落地版)

1. 单一职责原则(SRP)

场景:用户模块拆分,避免一个类既处理用户CRUD,又处理用户权限验证。

// 职责1:用户基础信息管理(单一职责)
class UserManager {
    public void addUser(String username) {
        System.out.println("新增用户:" + username);
    }
    public void deleteUser(String username) {
        System.out.println("删除用户:" + username);
    }
}

// 职责2:用户权限验证(单一职责)
class UserAuth {
    public boolean checkPermission(String username) {
        System.out.println("验证" + username + "的权限");
        return true;
    }
}

// 调用示例
public class SRPTest {
    public static void main(String[] args) {
        UserManager userManager = new UserManager();
        UserAuth userAuth = new UserAuth();
        
        userManager.addUser("张三");
        userAuth.checkPermission("张三");
    }
}

2. 开闭原则(OCP)

场景:支付系统扩展,新增微信支付无需修改原有支付宝代码。

// 抽象层:支付接口(稳定)
interface PayService {
    void pay(double amount);
}

// 具体实现:支付宝支付(已上线,无需修改)
class AlipayPayService implements PayService {
    @Override
    public void pay(double amount) {
        System.out.println("支付宝支付:" + amount + "元");
    }
}

// 扩展实现:微信支付(新增,不修改原有代码)
class WechatPayService implements PayService {
    @Override
    public void pay(double amount) {
        System.out.println("微信支付:" + amount + "元");
    }
}

// 调用方:依赖抽象,支持扩展
public class OCPTest {
    public static void main(String[] args) {
        PayService alipay = new AlipayPayService();
        PayService wechat = new WechatPayService();
        
        alipay.pay(100); // 原有功能
        wechat.pay(200); // 新增功能
    }
}

3. 里氏替换原则(LSP)

场景:图形计算,子类矩形替换父类图形后逻辑不变。

// 父类:图形(定义通用行为)
abstract class Shape {
    public abstract double getArea();
}

// 子类:圆形(符合里氏替换)
class Circle extends Shape {
    private double radius;
    public Circle(double radius) { this.radius = radius; }
    @Override
    public double getArea() {
        return Math.PI * radius * radius;
    }
}

// 子类:矩形(符合里氏替换)
class Rectangle extends Shape {
    private double width;
    private double height;
    public Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }
    @Override
    public double getArea() {
        return width * height;
    }
}

// 调用方:依赖父类,替换子类后行为不变
public class LSPTest {
    public static void printArea(Shape shape) {
        System.out.println("图形面积:" + shape.getArea());
    }
    public static void main(String[] args) {
        printArea(new Circle(2)); // 替换为圆形,正常计算
        printArea(new Rectangle(3,4)); // 替换为矩形,正常计算
    }
}

4. 接口隔离原则(ISP)

场景:动物行为接口拆分,避免无关方法强制实现。

// 细粒度接口:只包含单一行为
interface EatAble {
    void eat();
}
interface FlyAble {
    void fly();
}
interface SwimAble {
    void swim();
}

// 鸟类:实现吃+飞(只依赖需要的接口)
class Bird implements EatAble, FlyAble {
    @Override
    public void eat() { System.out.println("鸟吃虫子"); }
    @Override
    public void fly() { System.out.println("鸟飞行"); }
}

// 鱼类:实现吃+游(只依赖需要的接口)
class Fish implements EatAble, SwimAble {
    @Override
    public void eat() { System.out.println("鱼吃虾米"); }
    @Override
    public void swim() { System.out.println("鱼游泳"); }
}

// 调用示例
public class ISPTest {
    public static void main(String[] args) {
        Bird bird = new Bird();
        Fish fish = new Fish();
        
        bird.eat();
        bird.fly();
        fish.eat();
        fish.swim();
    }
}

5. 依赖倒置原则(DIP)

场景:订单服务依赖数据层抽象,切换数据库无需修改业务代码。

// 抽象层:数据访问接口(高层/低层都依赖)
interface DataSource {
    void save(String data);
}

// 低层实现:MySQL数据源
class MysqlDataSource implements DataSource {
    @Override
    public void save(String data) {
        System.out.println("保存数据到MySQL:" + data);
    }
}

// 低层实现:Oracle数据源(扩展)
class OracleDataSource implements DataSource {
    @Override
    public void save(String data) {
        System.out.println("保存数据到Oracle:" + data);
    }
}

// 高层模块:订单服务(依赖抽象,不依赖具体实现)
class OrderService {
    private DataSource dataSource;
    // 构造器注入,解耦具体数据源
    public OrderService(DataSource dataSource) {
        this.dataSource = dataSource;
    }
    public void saveOrder(String orderId) {
        dataSource.save("订单ID:" + orderId);
    }
}

// 调用示例:切换数据源只需替换实现类
public class DIPTest {
    public static void main(String[] args) {
        // 切换为MySQL
        OrderService orderService1 = new OrderService(new MysqlDataSource());
        orderService1.saveOrder("001");
        
        // 切换为Oracle(无需修改OrderService)
        OrderService orderService2 = new OrderService(new OracleDataSource());
        orderService2.saveOrder("002");
    }
}

6. 迪米特法则(LOD)

代码示例见上文面试题5,核心是“隐藏内部细节,只和直接朋友通信”。

7. 合成复用原则(CRP)

场景:汽车功能复用,用组合替代继承,避免继承带来的耦合。

// 被复用的功能类:发动机
class Engine {
    public void start() {
        System.out.println("发动机启动");
    }
}

// 被复用的功能类:车轮
class Wheel {
    public void rotate() {
        System.out.println("车轮转动");
    }
}

// 组合复用:汽车依赖发动机和车轮(而非继承)
class Car {
    // 组合:持有依赖对象
    private Engine engine = new Engine();
    private Wheel wheel = new Wheel();
    
    public void run() {
        engine.start(); // 复用发动机功能
        wheel.rotate(); // 复用车轮功能
        System.out.println("汽车行驶");
    }
}

// 扩展:公交车复用汽车功能(继续组合)
class Bus {
    private Car car = new Car();
    public void run() {
        car.run(); // 复用汽车的run方法
        System.out.println("公交车载客行驶");
    }
}

// 调用示例
public class CRPTest {
    public static void main(String[] args) {
        Bus bus = new Bus();
        bus.run();
    }
}

三、总结

核心要点回顾

  1. SOLID是核心:单一职责(高内聚)、开闭原则(易扩展)、里氏替换(继承合理)、接口隔离(低耦合)、依赖倒置(面向接口)是面向对象设计的基石;
  2. 复用优先组合:组合/聚合比继承更灵活,能降低耦合,符合开闭原则;
  3. 原则是指导而非教条:实际开发中需平衡“设计合理性”和“实现复杂度”,小场景简化、高频变更场景严格遵循。

面试加分技巧

  • 回答设计原则问题时,先讲定义,再讲核心思想,最后举代码例子(面试官最看重落地能力);
  • 强调“设计原则的目标是降低变更成本”,而非“为了遵循原则而遵循”;
  • 结合项目经验:比如“我在XX项目中,用依赖倒置原则重构了支付模块,新增支付方式时只需加实现类,无需修改原有业务代码”。

如果需要针对某一个原则的更复杂场景(如Spring框架中设计原则的应用)做深入讲解,我可以补充对应的代码和分析。

posted @ 2026-03-12 15:52  七星6609  阅读(1)  评论(0)    收藏  举报