Java面向对象编程7大原则实战指南:从理论到代码落地

一、单一职责原则:一个类只做一件事

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

Java面向对象编程7大原则实战指南:从理论到代码落地

反例:职责混杂的“万能类”

// 反例:一个类同时处理用户和订单
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。

三、里氏替换原则:子类能无缝替换父类

定义:所有引用父类的地方,必须能透明地使用其子类的对象(子类不能破坏父类的契约)。
经典反例:正方形继承矩形——矩形有setWidthsetHeight,正方形的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直接访问OrderorderItems列表,导致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),UserServiceprintUserOrders就得跟着改——因为它直接访问了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

(0)