面向对象设计原则

七大面向对象设计原则

一、单一职责原则

定义

  • 单一职责原则(SRP)要求:一个类应该只有一个引起它变化的原因,即一个类只负责一项职责。
  • 如果一个类承担的职责过多,任何职责的变更都可能影响到其他职责的实现,导致类变得臃肿、难以维护。

实际应用

​ 假设我们有一个 UserService 类,它负责用户的数据操作日志记录。如果后期修改日志策略,会影响本该只处理业务逻辑的用户类。

1.不遵循单一职责原则的示例

public class UserService {
    public void createUser(String name) {
        // 处理用户创建业务逻辑
        System.out.println("用户 " + name + " 已创建");

        // 日志逻辑耦合在一起
        System.out.println("日志记录:用户 " + name + " 创建成功");
    }
}

这个类承担了用户业务逻辑日志记录两个职责。

2.遵循单一职责原则的示例

// 日志类,专门负责日志相关的逻辑
public class Logger {
    public void log(String message) {
        System.out.println("日志记录:" + message);
    }
}

// 用户服务类,专注于用户业务
public class UserService {
    private Logger logger = new Logger();

    public void createUser(String name) {
        // 专注用户创建逻辑
        System.out.println("用户 " + name + " 已创建");

        // 委托日志记录,不负责其内部细节
        logger.log("用户 " + name + " 创建成功");
    }
}

每个类只做一件事,逻辑清晰、职责明确。

实现单一职责原则的方法

  1. 识别变化原因:明确哪些功能可能因不同原因发生变化,将其拆分。
  2. 类的拆分:将承担多种职责的类,按功能拆分成多个小类。
  3. 接口分离:若某类实现多个接口,也可以考虑用接口分离原则解耦。

总结

单一职责原则是高内聚、低耦合的重要体现。遵循此原则的好处包括:

  • 降低类的复杂度;
  • 提高系统的可维护性;
  • 降低变更引起的风险;
  • 更易于测试和复用。

二、开闭原则

定义:

  • 开闭原则(Open-Closed Principle, OCP)是面向对象设计的核心原则之一,指的是:对扩展开放,对修改关闭
  • 对扩展开放:允许在不修改现有代码的基础上通过新增代码来扩展系统功能;
  • 对修改关闭:原有的代码不应被修改,以保障系统的稳定性和可靠性。

实际应用:

​ 假设我们开发一个图形绘制系统,起初只支持圆形,后续需要支持矩形、三角形等其他图形。

1.不遵循开闭原则的示例(使用条件分支)

public class Drawing {
    public void draw(String shapeType) {
        if (shapeType.equalsIgnoreCase("CIRCLE")) {
            // 绘制圆
        } else if (shapeType.equalsIgnoreCase("RECTANGLE")) {
            // 绘制矩形
        } else if (shapeType.equalsIgnoreCase("TRIANGLE")) {
            // 绘制三角形
        }
    }
}

​ 每新增一个图形,都需要修改 Drawing 类,违反“对修改关闭”的原则。

2.遵循开闭原则的示例(使用多态和抽象)

// 抽象形状接口
public interface Shape {
    void draw();
}

// 各种具体图形类
public class Circle implements Shape {
    public void draw() {
        System.out.println("绘制圆形");
    }
}

public class Rectangle implements Shape {
    public void draw() {
        System.out.println("绘制矩形");
    }
}

public class Triangle implements Shape {
    public void draw() {
        System.out.println("绘制三角形");
    }
}

// 绘图类
public class Drawing {
    public void draw(Shape shape) {
        shape.draw();
    }
}

​ 要扩展新的图形,只需新增一个实现了 Shape 接口的类,遵循开闭原则。

实现开闭原则的方法

  1. 使用接口或抽象类编程:将具体实现隐藏在接口之后。
  2. 利用多态机制:让父类引用调用子类实现,避免使用条件语句分支。
  3. 使用设计模式:如策略模式、工厂模式、装饰器模式等常用于实现开闭原则。
  4. 模块隔离:将变化集中在特定模块内,通过新增模块实现功能增强。

总结

开闭原则强调在需求变更时,通过新增而不是修改代码来实现功能扩展,它的核心价值在于:

  • 降低因修改引起的风险;
  • 增强系统的可扩展性;
  • 提高开发效率与软件质量;
  • 鼓励面向抽象设计。

三、里氏代换原则

定义

​ 里氏代换原则指出:子类对象必须能够替换其父类对象,并且在替换后程序的行为不会发生变化

​ 换句话说,程序中的对象应该可以在不改变程序正确性的前提下被它的子类对象所替换。

实际应用

​ 假设我们有一个基类 Bird,它有一个方法 fly()。如果我们有一个子类 Penguin(企鹅),按照里氏代换原则,Penguin 应该能够替换 Bird,但显然企鹅不会飞。因此,直接让 Penguin 继承 Bird 并实现 fly() 方法是不合适的。

1.不遵循里氏代换原则的示例

public class Bird {
    public void fly() {
        // 飞行的逻辑
    }
}

public class Penguin extends Bird {
    @Override
    public void fly() {
        throw new UnsupportedOperationException("企鹅不会飞");
    }
}

Penguin 继承了 Bird,但却无法实现 fly() 方法的语义,替换父类后破坏了系统的预期行为

2.遵循里氏代换原则的示例

// 抽象的鸟类
public abstract class Bird {
    public abstract void move();
}

// 飞行的鸟类
public class FlyingBird extends Bird {
    @Override
    public void move() {
        fly();
    }

    private void fly() {
        // 飞行的逻辑
    }
}

// 不会飞的鸟类
public class NonFlyingBird extends Bird {
    @Override
    public void move() {
        swim(); // 或者 walk() 等
    }

    private void swim() {
        // 游泳的逻辑
    }
}

// 企鹅类
public class Penguin extends NonFlyingBird {
    // 企鹅特有的逻辑
}

实现里氏代换原则的方法

  1. 合理设计类的层次结构:确保继承关系真正反映“是一个”关系。
  2. 保持方法行为的一致性:子类实现父类方法时,应保持或增强其行为,而不是改变。
  3. 使用接口和抽象类:通过接口和抽象类定义通用行为,具体实现由子类完成。

总结

里氏替换原则是对继承关系的正确使用约束,它保证:

  • 子类在继承父类时不能违背父类的契约;
  • 系统在使用父类时,也可以无缝使用其子类;
  • 增强代码的稳定性与可扩展性。

四、接口隔离原则

定义

  • 接口隔离:将大而全的接口拆分成多个小而专一的接口,使得客户端只依赖于它们需要的接口。
  • 最小依赖:客户端只依赖于它实际使用的接口,避免依赖不需要的方法。

实际应用

​ 假设我们有一个 Printer 接口,最初设计时包含了打印、扫描和复印三个方法:

public interface Printer {
    void print(Document document);
    void scan(Document document);
    void copy(Document document);
}

​ 现在,我们有一个 SimplePrinter 类只需要实现打印功能,

​ 而 AllInOnePrinter类需要实现打印、扫描和复印功能。如果遵循接口隔离原则,我们可以将

Printer 接口拆分成多个小接口:

1.不遵循接口隔离原则的示例

public class SimplePrinter implements Printer {
    @Override
    public void print(Document document) {
        // 打印逻辑
    }

    @Override
    public void scan(Document document) {
        throw new UnsupportedOperationException("不支持扫描功能");
    }

    @Override
    public void copy(Document document) {
        throw new UnsupportedOperationException("不支持复印功能");
    }
}

2.遵循接口隔离原则的示例

public interface Printable {
    void print(Document document);
}

public interface Scannable {
    void scan(Document document);
}

public interface Copyable {
    void copy(Document document);
}

实现接口隔离原则的方法:

  1. 拆分大接口:将大而全的接口拆分成多个小而专一的接口。
  2. 按需依赖:客户端只依赖于它实际使用的接口,避免依赖不需要的方法。
  3. 使用组合代替继承:通过组合多个小接口来实现复杂的功能,而不是通过继承一个大接口。

总结:

接口隔离原则强调“接口应该精简、职责单一”,它的核心目的是:

  • 降低类对接口的依赖;
  • 减少因接口修改带来的连锁反应;
  • 提高系统的灵活性、可读性和可维护性。

五、依赖倒转原则

定义:

依赖倒转原则包含两个主要方面:

1.高层模块不应该依赖低层模块,二者都应该依赖抽象:

  • 高层模块代表业务逻辑或核心功能,低层模块代表具体的实现细节(如数据库访问、文件系统操作等)。
  • 二者都不应直接依赖于具体的实现,而是依赖于抽象(如接口或抽象类)。

2.抽象不应该依赖细节,细节应该依赖抽象:

  • 抽象(接口或抽象类)定义了高层模块和低层模块之间的契约,不应依赖于具体的实现细节。
  • 具体的实现细节(低层模块)应该依赖于抽象,以满足抽象定义的契约。

实际应用:

​ 假设我们有一个高层次的 OrderProcessor 类,负责处理订单。最初的设计中,OrderProcessor 直接依赖于一个具体的 Database 类来保存订单信息:

1.不遵循依赖倒转原则示例

// 具体的数据库类
public class Database {
    public void saveOrder(Order order) {
        // 保存订单到数据库的逻辑
    }
}

// 高层次的订单处理类
public class OrderProcessor {
    private Database database;

    public OrderProcessor() {
        this.database = new Database();
    }

    public void processOrder(Order order) {
        // 处理订单逻辑
        database.saveOrder(order);
    }
}

​ 在这个设计中,OrderProcessor 直接依赖于具体的 Database 类。

​ 如果将来需要更换数据库或使用不同的存储机制,就需要修改 OrderProcessor 类,这违反了依赖倒转原则。

2.遵循依赖倒转原则的示例

​ 为了遵循依赖倒转原则,我们引入一个抽象的 OrderRepository 接口,并让 OrderProcessor 依赖于这个接口,而不是具体的 Database 类:

// 抽象的订单存储接口
public interface OrderRepository {
    void saveOrder(Order order);
}

// 具体的数据库实现类
public class DatabaseOrderRepository implements OrderRepository {
    @Override
    public void saveOrder(Order order) {
        // 保存订单到数据库的逻辑
    }
}

// 高层次的订单处理类
public class OrderProcessor {
    private OrderRepository orderRepository;

    // 通过构造函数注入依赖
    public OrderProcessor(OrderRepository orderRepository) {
        this.orderRepository = orderRepository;
    }

    public void processOrder(Order order) {
        // 处理订单逻辑
        orderRepository.saveOrder(order);
    }
}

实现依赖倒转原则的方法:

  1. 引入抽象层:通过接口或抽象类定义高层模块和低层模块之间的契约。
  2. 依赖注入(Dependency Injection):通过构造函数、方法参数或属性将依赖关系注入到类中,而不是在类内部创建依赖对象。
  3. 使用工厂模式或服务定位器:通过工厂模式或服务定位器动态获取依赖对象,进一步解耦模块之间的依赖关系。

总结:

依赖倒置原则的核心是通过抽象隔离高层和低层模块,它的价值在于:

  • 降低耦合度,提高灵活性;
  • 系统更易于扩展和测试;
  • 高层模块不再受限于底层实现;
  • 实现“开闭原则”的基础保障。

六、迪米特法则

定义:

  • 最少知识原则(Least Knowledge Principle):一个对象应当对其他对象有尽可能少的了解,只与直接的“朋友”通信,而不与“陌生人”发生联系。
  • 直接朋友:该对象的成员变量、方法的参数、方法的返回值、该对象本身创建的对象等。
  • 陌生人:通过链式调用间接获取到的对象,如 a.getB().getC().doSomething() 中的 C 对象。

实际应用:

​ 假设我们有一个学生类 Student 和一个班级类 Classroom,我们想获取学生的姓名。如果不遵循迪米特法则,School 类可能会通过班级获取学生,再通过学生获取姓名。

1.不遵循迪米特法则的示例

public class School {
    public void printStudentName(Classroom classroom) {
        String name = classroom.getStudent().getName(); // 多级调用,违反迪米特法则
        System.out.println(name);
    }
}

2.遵循迪米特法则的示例

public class Classroom {
    private Student student;

    public String getStudentName() {
        return student.getName(); // 将逻辑封装在本类中,避免外部直接访问 student
    }
}

public class School {
    public void printStudentName(Classroom classroom) {
        String name = classroom.getStudentName(); // 只与“直接朋友”通信
        System.out.println(name);
    }
}

实现迪米特法则的方法:

  1. 封装内部结构:不要让一个对象的内部结构暴露给其他对象。
  2. 中间转发:通过封装、封装再封装,让对象之间只通过必要的“接口”交互。
  3. 控制对象之间的依赖关系:尽量减少类之间的耦合,降低维护成本。

总结:

迪米特法则强调“低耦合、高内聚”,它的价值体现在:

  • 降低对象之间的耦合性;
  • 增强系统模块的独立性和可维护性;
  • 提高代码封装性和鲁棒性。

七、合成复用原则

定义:

  • 合成复用原则要求:在系统设计中,尽量使用对象组合(Composition)或聚合(Aggregation)来实现功能复用,而不是通过类继承(Inheritance)。
  • 组合优先于继承,原因在于:
    • 继承是强耦合关系,父类的改变会影响子类;
    • 组合是松耦合关系,可以在运行时动态改变行为,更加灵活。

实际应用:

​ 假设我们需要开发一个日志系统,最初系统使用控制台输出日志,后续希望扩展支持文件输出或远程服务器日志。

​ 如果使用继承,每新增一种日志输出方式就要创建一个新的子类,并且逻辑难以灵活调整。

1.不遵循合成复用原则的示例(使用继承)

public class Logger {
    public void log(String message) {
        System.out.println("Console log: " + message);
    }
}

public class FileLogger extends Logger {
    @Override
    public void log(String message) {
        // 将日志写入文件
    }
}

这种方式耦合性强,一旦 Logger 的逻辑变化,子类都会受到影响。

2.遵循合成复用原则的示例(使用组合)

// 抽象的日志策略接口
public interface LogStrategy {
    void log(String message);
}

// 控制台日志实现
public class ConsoleLogStrategy implements LogStrategy {
    public void log(String message) {
        System.out.println("Console log: " + message);
    }
}

// 文件日志实现
public class FileLogStrategy implements LogStrategy {
    public void log(String message) {
        // 将日志写入文件
    }
}

// 主日志类,使用组合来实现灵活扩展
public class Logger {
    private LogStrategy strategy;

    public Logger(LogStrategy strategy) {
        this.strategy = strategy;
    }

    public void log(String message) {
        strategy.log(message);
    }
}

​ 该方法新增日志方式只需新增策略类,原有逻辑无需修改,系统具备良好的扩展性。

实现合成复用原则的方法:

  1. 优先使用组合:通过将其他对象作为成员变量,实现功能复用。
  2. 封装变化:通过组合接口类型(如 LogStrategy),实现运行时的动态替换。
  3. 继承需谨慎:只有当子类真的是父类的一种类型(满足 is-a 关系)时,才使用继承。

总结:

合成复用原则强调:复用功能时应优先使用“组合”而非“继承”

它的好处包括:

  • 更低的耦合性;
  • 更高的灵活性;
  • 更容易遵循开闭原则(对扩展开放、对修改关闭);
  • 更符合模块化和面向接口编程的思想。

八、面向对象设计原则总结

​ 7 种设计原则,它们分别为单一职责原则开闭原则里氏替换原则、接口隔离原则依赖倒置原则迪米特法则合成复用原则

​ 这 7 种设计原则是软件设计模式必须尽量遵循的原则,是设计模式的基础。

​ 在实际开发过程中,并不是一定要求所有代码都遵循设计原则,而是要综合考虑人力、时间、成本、质量,不刻意追求完美,要在适当的场景遵循设计原则。这体现的是一种平衡取舍,可以帮助我们设计出更加优雅的代码结构。

​ 各种原则要求的侧重点不同,下面我们分别用一句话归纳总结软件设计模式的七大原则,如下表所示。

在这里插入图片描述

​ 实际上,这些原则的目的只有:降低对象之间的耦合,增加程序的可复用性、可扩展性和可维护性

记忆口诀访问加限制,函数要节俭,依赖不允许,动态加接口,父类要抽象,扩展不更改

在程序设计时,我们应该将程序功能最小化,每个类只干一件事。若有类似功能基础之上添加新功能,则要合理使用继承。对于多方法的调用,要会运用接口,同时合理设置接口功能与数量。最后类与类之间做到低耦合高内聚。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值