当前位置: 首页 > news >正文

面向对象编程的设计原则

SOLID是面向对象设计的五大原则的简称。SOLID原则是指导我们设计面向对象软件的5个基本原则。除了 SOLID 原则之外,还有其他的面向对象设计原则,比如迪米特法则(最少知识原则)、组合/聚合复用原则。

单一职责原则(SRP - Single Responsibility Principle)

  • 概述: 一个类应该只负责一个功能或责任,就一个类而言,应该只有一个引起它变化的原因。
  • 示例: 在一个银行应用程序中,可以将账户管理和交易记录管理分成两个不同的类,而不是将所有功能都放在同一个类中。这样可以让代码更加清晰、可维护。

代码示例:

// 不遵循单一职责原则
public class UserManager {
    getConnection();
    
    public void findUsers(){
        getConnection().query("SELECT * FROM users");
        //...
    }
    
    public void insertUser(){
        getConnection().insert("INSERT INTO users...");
        //...    
    }
        
    public void displayUsers(){
        findUsers();
        //...
    }
    public void generateUsers(){
        insertUser();
        //...
    }

}

// 遵循单一职责原则
public class UserManager {
    UserDao userDao;
    
    public UserManager(UserDao userDao) {
        this.userDao = userDao;
    }

    public void displayUsers(){
        userDao.findUsers();
        //...
    }
    public void generateUsers(){
        userDao.insertUser();
        //...
    }
}

public class UserDao {
    DBConnection connection;
    public void findUsers(){
        connection.query("SELECT * FROM users");
        //...
    }

    public void insertUser(){
        connection.insert("INSERT INTO users...");
        //...    
    }

}

public class DBConnection {
    getConnection();
}

优点:

  • 每个类都只负责一个单一的功能,职责更加清晰。
  • 代码更加模块化,易于理解和维护。
  • 如果需要修改某个功能,只需要修改相应的类,不会影响到其他功能。

缺点:

  • 不遵循单一职责原则的代码:
    • UserManager类负责太多的功能,增加了代码的复杂性和维护难度。
    • 如果需要修改某些功能,例如更改数据库的链接方式或查找用户的方式或者展示用户的方式,都需要修改UserManager类,违反了"单一职责"的原则。

开闭原则(OCP - Open/Closed Principle)

  • 概述: 软件实体(类、模块、函数等)应该对扩展开放,对修改关闭,应尽量在不修改原有代码的情况下进行扩展。
  • 示例: 假设需要在一个电商系统中增加新的折扣策略。可以创建一个抽象的"折扣策略"接口,然后实现不同的具体折扣策略类,而不需要修改原有的代码。这样就可以通过扩展新的类来增加新的功能,而不需要修改原有的代码。
    代码示例:
// 不遵循开闭原则
public class DiscountCalculator {
    public double calculateDiscount(Customer customer, double amount) {
        if (customer.getType().equals("regular")) {
            return amount * 0.9; // 9折
        } else if (customer.getType().equals("premium")) {
            return amount * 0.8; // 8折
        } else {
            return amount;
        }
    }
}

// 遵循开闭原则
public interface DiscountStrategy {
    double applyDiscount(double amount);
}

public class RegularCustomerDiscount implements DiscountStrategy {
    @Override
    public double applyDiscount(double amount) {
        return amount * 0.9; // 9折
    }
}

public class PremiumCustomerDiscount implements DiscountStrategy {
    @Override
    public double applyDiscount(double amount) {
        return amount * 0.8; // 8折
    }
}

public class DiscountCalculator {
    private DiscountStrategy discountStrategy;

    public void setDiscountStrategy(DiscountStrategy strategy) {
        this.discountStrategy = strategy;
    }

    public double calculateDiscount(double amount) {
        return discountStrategy.applyDiscount(amount);
    }
}

优点:

  • 遵循开闭原则的代码:
    • 通过使用抽象的DiscountStrategy接口,可以很容易地添加新的折扣策略,而无需修改DiscountCalculator类。
    • 代码更加灵活和可扩展,遵循"对扩展开放,对修改关闭"的原则。

缺点:

  • 不遵循开闭原则的代码:
    • DiscountCalculator类包含了折扣策略的具体实现,如果需要添加新的折扣策略,就需要修改这个类,违反了"对修改关闭"的原则。
    • 代码的扩展性较差,如果需要经常修改折扣策略,会很麻烦。

里氏替换原则(LSP - Liskov Substitution Principle)

  • 概述:
    • 里氏替换原则是对开闭原则的扩展。
    • 继承关系中子类应当能够替换其基类,并且在替换后,程序的行为不应发生改变,换句话说,所有引用基类的地方必须能够透明地使用其子类的对象。
    • 并且尽量使用基类来对对象定义,而在运行时再确定其子类类型,用子类对象来替换父类对象。
  • 示例: 在一个图形绘制程序中,有一个基类"图形"和两个子类"矩形"和"正方形"。如果你在某个地方使用了"图形"对象,那么你应该能够用"矩形"或"正方形"对象来替换它,而不会影响程序的行为。

代码示例:

// 不遵循里氏替换原则
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;
    }

    @Override
    public void setHeight(int height) {
        this.width = height;
        this.height = height;
    }
}

public class DrawManager {
    public void draw(Rectangle rectangle) {
        System.out.println("Drawing a rectangle");
    }
    public void draw(Square square) {
        System.out.println("Drawing a square");
    }
}

// 遵循里氏替换原则
public abstract class Shape {
    public abstract int getArea();
}

public class Rectangle extends Shape {
    protected int width;
    protected int height;

    public void setWidth(int width) {
        this.width = width;
    }

    public void setHeight(int height) {
        this.height = height;
    }

    @Override
    public int getArea() {
        return width * height;
    }
}

public class Square extends Shape {
    private int side;

    public void setSide(int side) {
        this.side = side;
    }

    @Override
    public int getArea() {
        return side * side;
    }
}

public class DrawManager {
    public void draw(Shape shape) {
        System.out.println("Drawing a " + shape.toString());
    }
}

优点:

  • 遵循里氏替换原则的代码:
    • 通过定义抽象的Shape类,并让Rectangle和Square类继承它,确保了子类可以替代父类使用。
    • DrawManager类中draw()方法传递参数时使用基类对象,在运行时再确定其子类类型,用子类对象来替换父类对象,确保了程序的行为不发生变化。
    • 代码更加灵活,易于扩展新的图形类型。

缺点:

  • 不遵循里氏替换原则的代码:
    • Square类中的setWidth和setHeight方法违反了Rectangle类的契约,这将导致程序在使用Rectangle和Square对象时出现问题。
    • DrawManager 中两个 draw() 中代码重复,而且增加新的图形还需要增加方法。
    • 这种设计违反了里氏替换原则,使得代码更加脆弱和难以维护。

子类违反父类的契约

为了遵守里氏替换原则,我们应该设计一个父类Shape, 然后让Rectangle和Square类继承它。而不是Square作为继承Rectangle类的子类。这样可以确保子类的行为与父类契约一致,保证可以安全地将父类对象替换为子类对象。

以上的例子中, Square类中的setWidth和setHeight方法违反了Rectangle类契约的原因:

// 不遵循里氏替换原则
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;
    }

    @Override
    public void setHeight(int height) {
        this.width = height;
        this.height = height;
    }
}

在这个例子中,Rectangle类定义了一个矩形,有宽度和高度两个属性。Square类是Rectangle类的子类,代表一个正方形。

问题在于,Square类中的setWidth和setHeight方法的实现违反了Rectangle类的契约。

Rectangle类的契约:

  1. 设置宽度时,高度保持不变。
  2. 设置高度时,宽度保持不变。

但是,Square类中的setWidth和setHeight方法违反了这个契约,因为在设置宽度或高度时,它们会同时修改宽度和高度,使其保持相等。

这意味着,我们无法将Square对象完全替代Rectangle对象使用,因为Square对象的行为与Rectangle类的契约不一致。这就违反了里氏替换原则。

如果我们将一个Rectangle对象替换为一个Square对象,原本正常工作的代码可能会出现问题。例如:

Rectangle rect = new Rectangle();
rect.setWidth(5);
rect.setHeight(10);
int area = rect.getArea(); // 50

// 将Rectangle替换为Square
Rectangle square = new Square();
square.setWidth(5);
square.setHeight(10);
int squareArea = square.getArea(); // 25, 而不是50

可以看到,使用Square对象时,getArea()方法返回的面积与预期不符。这就是里氏替换原则被违反的结果。

接口隔离原则(ISP - Interface Segregation Principle)

  • 概述: 客户端不应该依赖它不需要的接口,也就是说应该使用多个专门的、真正需要的接口,而不使用单一的总接口,为客户端提供尽可能多个具体的、细粒度的接口,避免使用肥接口(Fat Interfaces),即包含大量无关方法的接口。
  • 示例: 在一个机器人系统中,可以定义不同的接口,如"移动"、“抓取"和"识别”。这样客户端只需要依赖它实际需要使用的接口,而不需要依赖所有的接口。这可以减少耦合,提高灵活性。
// 不遵循接口隔离原则
public interface CustomerDataDisplay {
    void dataRead();
    void transformToExcel();
    void createReport();
    void displayReport();
}

public class CustomerDataDisplayImpl implements CustomerDataDisplay {

    @Override
    public void dataRead() {
        // 读取数据
    }

    @Override
    public void transformToExcel() {
        // 转换为Excel
    }

    @Override
    public void createReport() {
        // 生成报告
    }

    @Override
    public void displayReport() {
        // 显示报告
    }
}

// 遵循接口隔离原则
public interface ReportHandler {
    void createReport();
    void displayReport();
}

public interface ExcelHandler {
    void transformToExcel();
}

public interface DataHandler {
    void dataRead();
}

public class CustomerDataDisplayImpl implements ReportHandler, ExcelHandler, DataHandler {
    @Override
    public void dataRead() {
        // 读取数据
    }

    @Override
    public void transformToExcel() {
        // 转换为Excel
    }

    @Override
    public void createReport() {
        // 生成报告
    }

    @Override
    public void displayReport() {
        // 显示报告
    }
}

优点:

  • 遵循接口隔离原则的代码:
    • 通过定义更细粒度的接口,客户端只需依赖它实际需要的接口,而不需要依赖所有功能。
    • 代码更加模块化,易于维护和扩展。
    • 但是接口也不能太小,会导致系统中接口泛滥,不利于维护,例如,可以把 ReportHandler 中都是处理报告的方法实现放在一起

缺点:

  • 不遵循接口隔离原则的代码:
    • Machine接口包含了许多不相关的功能,这违反了接口隔离原则。
    • 如果客户端只需要某些功能,但必须依赖所有的功能,这会增加代码的复杂性和耦合度。

依赖倒置原则(DIP - Dependency Inversion Principle)

  • 概述:
    • 高层模块不应该依赖低层模块,两者都应该依赖抽象。即应该使用接口和抽象类进行变量类型声明、参数类型声明、方法返回类型声明,以及数据类型的转换等,而不要用具体类。
    • 抽象不应该依赖细节,细节应该依赖抽象。要针对接口编程,而不是针对实现编程。
  • 示例: 在一个支付系统中,可以定义一个"支付方式"接口,然后实现不同的支付方式类,如"支付宝"、"微信支付"等。这样支付系统的高层代码就不需要依赖具体的支付方式实现,而是依赖抽象的"支付方式"接口。当需要增加新的支付方式时,只需要实现新的支付方式类,而不需要修改支付系统的高层代码。
// 不遵循依赖倒置原则
public class EmailSender {
    public void sendEmail(String message) {
        // 使用 Gmail 发送邮件
        GmailClient client = new GmailClient();
        client.sendEmail(message);
    }
}

public class GmailClient {
    public void sendEmail(String message) {
        // Gmail 发送邮件的实现
    }
}

// 遵循依赖倒置原则
public interface EmailSender {
    void sendEmail(String message);
}

public class GmailEmailSender implements EmailSender {
    @Override
    public void sendEmail(String message) {
        // Gmail 发送邮件的实现
    }
}

public class OutlookEmailSender implements EmailSender {
    @Override
    public void sendEmail(String message) {
        // Outlook 发送邮件的实现
    }
}

public class EmailService {
    private EmailSender emailSender;

    public void setEmailSender(EmailSender emailSender) {
        this.emailSender = emailSender;
    }

    public void sendEmail(String message) {
        emailSender.sendEmail(message);
    }
}

优点:

  • 遵循依赖倒置原则的代码:
    • EmailService类不依赖具体的EmailSender实现,而是依赖抽象的EmailSender接口。
    • 这使得EmailService类更加灵活,可以轻松地切换不同的邮件发送实现。
    • 代码更加可测试和可扩展。

缺点:

  • 不遵循依赖倒置原则的代码:
    • EmailSender类直接依赖于GmailClient的具体实现,这违反了依赖倒置原则。
    • 如果需要切换到其他的邮件发送服务,就需要修改EmailSender类的代码,违反了"对修改关闭"的原则。

【高层模块】和【低层模块】的概念

在软件设计中,我们通常将系统划分为不同的层次或模块,这些层次或模块之间会存在依赖关系。

  1. 高层模块(High-level Module)

    • 高层模块代表了系统中更加抽象、面向业务逻辑的部分。
    • 这些模块包含了系统的核心功能和业务规则,通常位于系统架构的较高层次。
    • 在上面的示例中,EmailService类就是一个高层模块,它负责处理发送邮件的业务逻辑。
  2. 低层模块(Low-level Module)

    • 低层模块则代表了系统中更加具体、技术性的部分。
    • 这些模块实现了一些基础的功能或服务,通常位于系统架构的较低层次。
    • 在上面的示例中,GmailEmailSenderOutlookEmailSender类就是低层模块,它们负责具体的邮件发送实现。

依赖倒置原则要求,高层模块不应该直接依赖于低层模块的具体实现,而是应该依赖于抽象的接口或抽象类。这样做的好处是:

  1. 高层模块可以独立于低层模块的具体实现进行开发和测试,提高了代码的可测试性。
  2. 当需要切换低层模块的实现时,只需要修改低层模块的代码,而不需要修改高层模块的代码,提高了代码的可维护性。
  3. 高层模块和低层模块之间的耦合度降低,代码的模块化程度更高,提高了代码的可扩展性。

【抽象】和【细节】的含义

  1. 抽象(Abstraction)

    • 抽象指的是更高层次、更通用的概念、接口或抽象类。
    • 在代码设计中,抽象通常体现为接口(Interface)或抽象类(Abstract Class)。
    • 抽象定义了某个领域或系统的基本功能和行为,但不关心具体的实现细节。
  2. 细节(Details)

    • 细节指的是更低层次、更具体的实现。
    • 在代码设计中,细节通常体现为具体的实现类(Concrete Class)。
    • 细节负责实现抽象定义的功能和行为,关注具体的实现细节。

依赖倒置原则中的这句话的含义是:

  1. 抽象不应该依赖细节

    • 抽象层(接口或抽象类)应该定义基本的功能和行为,不应该依赖于具体的实现细节。
    • 这样可以确保抽象层的稳定性,并且提高代码的灵活性和可扩展性。
  2. 细节应该依赖抽象

    • 具体的实现类应该依赖于抽象层(接口或抽象类)定义的契约。
    • 这样可以确保实现细节与抽象层保持一致,并且提高代码的可测试性和可维护性。

举个例子,在上面的EmailSender示例中:

  • 抽象层是EmailSender接口,它定义了发送电子邮件的基本功能。
  • 细节层是GmailEmailSenderOutlookEmailSender这两个具体实现类,它们依赖于EmailSender接口的定义来实现具体的邮件发送功能。

这样设计可以确保:

  1. EmailService类(高层模块)只依赖于EmailSender接口(抽象),不依赖于具体的实现细节。
  2. GmailEmailSenderOutlookEmailSender这些具体实现类(细节)依赖于EmailSender接口(抽象)定义的契约。

迪米特法则(LOD - Law of Demeter)

  • 概述: 迪米特法则也称为【最少知识原则】(Principle of Least Knowledge, PoLK),一个软件实体应当尽可能少地与其他实体发生相互作用。这个原则的核心思想是减少对象之间的耦合度,即一个对象应当对其他对象保持最少的知识,只与它的“直接朋友”通信。
  • 示例: 当 A 对象需要使用 B 对象的某些功能时,A 对象应该直接调用 B 对象的方法,而不应该通过 C 对象来间接访问B对象。
// 不遵循迪米特法则
public class Car {
    private Engine engine;

    public void startCar() {
        engine.start();
    }

    public void stopCar() {
        engine.stop();
    }
}

public class Engine {
    public void start() {
        // 启动引擎的代码
    }
    
    public void revEngine() {
        // 转动引擎的代码
    }

    public void stop() {
        // 停止引擎的代码
    }
}

public class Driver {
    private Car car;

    public void driveCar() {
        car.startCar();
        car.engine.revEngine(); // 违反迪米特法则
        car.stopCar();
    }
}

在上面的例子中,Driver类直接访问了Car类的engine对象,这违反了迪米特法则。正确的做法应该是:

// 遵循迪米特法则
public class Car {
    private Engine engine;

    public void startCar() {
        engine.start();
    }

    public void revEngine() {
        engine.revEngine();
    }

    public void stopCar() {
        engine.stop();
    }
}

public class Driver {
    private Car car;

    public void driveCar() {
        car.startCar();
        car.revEngine();
        car.stopCar();
    }
}

遵循迪米特法则的优点:

  1. 降低了类之间的耦合度,提高了类的独立性和可重用性。
  2. 使得类的职责更加清晰,代码更加模块化和易于维护。
  3. 提高了代码的可读性和可测试性。

不遵循迪米特法则的缺点:

  1. 增加了类之间的耦合度,降低了系统的灵活性和可扩展性。
  2. 代码更难理解和维护,因为每个类都需要了解其他类的内部实现细节。
  3. 降低了代码的可测试性,因为测试一个类时需要考虑其他类的行为。

迪米特法则的规则

  • 一个对象应该只与它的直接朋友通信。
  • 一个对象的“直接朋友”包括:
    • 它自己(self)
    • 它的成员变量
    • 参数
    • 它的返回值
    • 它的父类
    • 它的子类
  • 不应该通过朋友去访问朋友的朋友,即避免链式调用。

组合/聚合复用原则(Composition/Aggregation Reuse Principle, CARP)

  • 概述: 组合/聚合复用原则的核心思想是尽量使用组合/聚合(has-a)的关系,而不是继承(is-a)的关系。在设计类的时候,应该优先考虑使用组合(Composition)或聚合(Aggregation),而不是继承(Inheritance),这样可以提高代码的灵活性和可重用性。
  • 示例: 当需要复用某些功能时,不应该让一个类继承另一个类,而应该让这个类包含(组合)或引用(聚合)另一个类的实例。

示例代码:

// 不遵循组合/聚合复用原则
abstract class Engine {
    void start();
}
//丰田引擎
public class Car extends Engine {

    @Override
    public void start() {
        System.out.println("Toyota Engine started.");
    }
}
// 切换本田引擎
public class Car extends Engine {

    @Override
    public void start() {
        System.out.println("Honda Engine started.");
    }
}


// 遵循组合/聚合复用原则
interface Engine {
    void start();
}

public class Car {
    private Engine engine;

    public setEngine(Engine engine) {
        this.engine = engine;
    }

    public void start() {
        engine.start();
    }
}

public class ToyotaEngine implements Engine {

    @Override
    public void start() {
        System.out.println("Toyota Engine started.");
    }
}

Car car = new Car();
car.setEngine(new ToyotaEngine());

遵循组合/聚合复用原则的优点:

  1. 动态切换引擎:运行时Car可以随时调用setEngine(Engine engine)更换引擎。
  2. 扩展方便:改变引擎只需修改Engine接口的实现类,无需修改Car类。
  3. 低耦合:Car不依赖具体的引擎实现类,只依赖接口。

不遵循组合/聚合复用原则的缺点:

  1. 每换一次引擎就要创建新子类或者改car类的代码,引擎和车强耦合,难以动态切换引擎。 限制了类的可复用性和扩展性,增加了代码的复杂性和维护成本。

组合和聚合的区别和联系

  1. 组合(Composition)
    • 组合是一种强依赖的关系,表示一个类包含另一个类的实例作为它的一部分。
    • 这种关系通常是"整体-部分"的关系,整体不能没有部分。部分对象无法独立于整体对象存在,当整体对象被销毁时,部分对象也会被销毁。例如,一辆汽车包含一个发动机。
      代码示例:
public class Car {
    private Engine engine;

    public Car() {
        engine = new Engine();
    }

    public void start() {
        engine.start();
    }
}

public class Engine {
    public void start() {
        System.out.println("Engine started.");
    }
}

在这个例子中,Car类包含Engine类的实例,它们之间存在强依赖关系。

  1. 聚合(Aggregation)
    • 聚合是一种弱依赖的关系,表示一个类包含另一个类的实例作为它的一部分,但两者之间并不强依赖。
    • 这种关系通常是"部分-整体"的关系,部分对象可以属于多个整体对象,也可以独立于整体对象存在。例如, 员工可以属于多个项目,即使项目结束,员工依然存在。

代码示例:

public class Item {
    private List<Employee> employees;

    public Item() {
        employees = new ArrayList<>();
    }

    public void addEmployee(Employee employee) {
        employees.add(employee);
    }
}

public class Employee {
    private String name;

    public Employee(String name) {
        this.name = name;
    }
}

在这个例子中,Item类包含Employee类的实例,它们之间存在弱依赖关系。

  1. 联系
    • 组合是聚合的一种特殊形式。
    • 组合关系要求部分对象不能独立于整体对象而存在,而聚合关系中部分对象可以独立于整体对象而存在。
    • 组合关系中,整体对象对部分对象有所有权和生命周期控制,而聚合关系中,整体对象只有所有权,不对部分对象的生命周期负责

为什么要优先使用组合/聚合而不是继承?

  1. 继承会创建紧耦合的类层次结构,增加了代码的复杂性和维护成本。
  2. 组合/聚合可以提高代码的可复用性,更灵活地组合和扩展功能。
  3. 组合/聚合可以降低类之间的耦合度,提高代码的可测试性和可维护性。

总结

面向对象7大设计原则总结

  • 单一职责原则(SRP): 类应该有且仅有一个引起它变化的原因。
  • 开闭原则(OCP): 软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。
  • 里氏替换原则(LSP): 子类对象必须能够替换其基类对象。
  • 接口隔离原则(ISP): 接口应该小而精,接口中的方法应该尽量少,客户端不应该依赖它不需要的接口。
  • 依赖倒置原则(DIP): 针对接口编程,而不是针对实现编程;依赖抽象,而不依赖于具体类。
  • 迪米特法则(LOD): 一个对象应该只与其直接朋友通信。
  • 组合/聚合复用原则(CARP): 优先使用组合/聚合,少用继承。

单一职责原则(SRP) 和 接口隔离原则(ISP) 的区别和联系

  1. SRP 针对的是类,ISP 针对的是接口:

    • SRP 要求一个类只负责一个职责或功能。
    • ISP 要求接口中的方法尽量少,仅包含相关的方法。
  2. 侧重点不同:

    • SRP 强调类的内聚性,避免一个类承担太多职责。
    • ISP 强调接口的小而美,避免客户端依赖它不需要的方法。
  3. 目的不太一样:

    • SRP 是为了让类更容易理解、维护和测试。
    • ISP 是为了让客户端只依赖它需要的部分,降低耦合度。
  4. 举例说明:
    假设我们有一个描述汽车的类 Vehicle,它提供以下功能:

    1. 启动和停止汽车引擎
    2. 显示汽车里程表和油表

    按照 SRP 原则:

    • 这个汽车类就违反了单一职责原则,因为它承担了太多职责(引擎管理、仪表显示)。
    • 我们应该将它拆分为三个独立的类:EngineController 和 InstrumentPanel。
    • 每个类只负责一项具体的功能,这样更容易维护和扩展。
      示例代码:
     // 不遵循 SRP 原则
     public class Vehicle {
         public void startEngine() {
             // 启动引擎的逻辑
         }
    
         public void stopEngine() {
             // 停止引擎的逻辑
         }
    
         public void displayMileage() {
             // 显示里程表的逻辑
         }
    
         public void displayFuelLevel() {
             // 显示油表的逻辑
         }
     }
    
     // 遵循 SRP 的重构后的类
     public class EngineController {
         public void startEngine() {
             // 启动引擎的逻辑
         }
    
         public void stopEngine() {
             // 停止引擎的逻辑
         }
     }
    
     public class InstrumentPanel {
         public void displayMileage() {
             // 显示里程表的逻辑
         }
    
         public void displayFuelLevel() {
             // 显示油表的逻辑
         }
     }
    

    再看 ISP 原则:

    • 假设我们设计了一个 Vehicle 接口,同样包含了上述三种功能。
    • 但是有些汽车可能只需要引擎和门窗控制,不需要仪表显示功能。
    • 这时按照 ISP 原则,我们应该将 Car 接口拆分为三个更小的接口:EngineControl 和 InstrumentDisplay。
    • 这样客户端就只需要依赖它需要的接口EngineControl,不会被不需要的方法所污染。
      示例代码:
    // 不遵循 ISP 原则
    public interface Vehicle {
        void startEngine();
        void stopEngine();
        void displayMileage();
        void displayFuelLevel();
    }
    // 遵循 ISP 原则
    public interface EngineControl {
        void startEngine();
        void stopEngine();
    }
    
    public interface InstrumentDisplay {
        void displayMileage();
        void displayFuelLevel();
    }
    
    public class Car1 implements EngineControl, InstrumentDisplay {
        public void startEngine() {
            // 启动引擎的逻辑
        }
    
        public void stopEngine() {
            // 停止引擎的逻辑
        }
    
        public void displayMileage() {
            // 显示里程表的逻辑
        }
    
        public void displayFuelLevel() {
            // 显示油表的逻辑
        }
    }
    
    public class Car2 implements EngineControl {
        public void startEngine() {
            // 启动引擎的逻辑
        }
    
        public void stopEngine() {
            // 停止引擎的逻辑
        }
    }
    

总之,SRP 强调类的内聚性,ISP 强调接口的小而美。两者都是为了提高代码的可维护性和扩展性。在实际开发中,我们需要兼顾这两个原则,设计出高内聚低耦合的面向对象系统。

相关文章:

  • 【鱼眼镜头12】Scaramuzza的鱼眼相机模型实操,不依赖于具体的相机几何结构,直接从图像数据出发,因此更具灵活性。
  • 爬虫瑞数5.5案例:某钢材交易官网(面向对象补环境)
  • pyrender smpl 渲染
  • 【C语言】动态内存管理
  • ElementUI表格表头自定义添加checkbox,点击选中样式不生效
  • SQL-leetcode—1661. 每台机器的进程平均运行时间
  • 在 Flutter 实现下拉刷新、上拉加载更多和一键点击回到顶部的功能
  • Vulhub靶机 ActiveMQ 反序列化漏洞(CVE-2015-5254)(渗透测试详解)
  • webpack和vite打包原理及比较
  • PostgreSQL的学习心得和知识总结(一百六十八)|深入理解PostgreSQL数据库之PostgreSQL 规划器开发与调试(翻译)
  • HCIA项目实践--静态路由的综合实验
  • MySQL单表存多大的数据量比较合适
  • Flask使用JWT认证
  • 计数排序
  • 用pytorch实现一个简单的图片预测类别
  • 字符设备驱动开发
  • SpringBoot Bug 日志
  • python_excel批量插入图片
  • 数据结构——队列、哈希存储(2025.2.11)
  • 【ISO 14229-1:2023 UDS诊断全量测试用例清单系列:第十二节】
  • 又一日军“慰安妇”制度受害者去世,大陆在世幸存者仅7人
  • 探索人类的心灵这件事,永远也不会过时
  • 甘肃省政府原党组成员、副省长杨子兴被提起公诉
  • 印度扩大对巴措施:封锁巴基斯坦名人账号、热门影像平台
  • 国际著名学者Charles M. Lieber全职受聘清华深圳国际研究生院
  • 首部关于民营经济发展的基础性法律,有何亮点?专家解读