面向对象编程的设计原则
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类的契约:
- 设置宽度时,高度保持不变。
- 设置高度时,宽度保持不变。
但是,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类的代码,违反了"对修改关闭"的原则。
【高层模块】和【低层模块】的概念
在软件设计中,我们通常将系统划分为不同的层次或模块,这些层次或模块之间会存在依赖关系。
-
高层模块(High-level Module)
- 高层模块代表了系统中更加抽象、面向业务逻辑的部分。
- 这些模块包含了系统的核心功能和业务规则,通常位于系统架构的较高层次。
- 在上面的示例中,
EmailService
类就是一个高层模块,它负责处理发送邮件的业务逻辑。
-
低层模块(Low-level Module)
- 低层模块则代表了系统中更加具体、技术性的部分。
- 这些模块实现了一些基础的功能或服务,通常位于系统架构的较低层次。
- 在上面的示例中,
GmailEmailSender
和OutlookEmailSender
类就是低层模块,它们负责具体的邮件发送实现。
依赖倒置原则要求,高层模块不应该直接依赖于低层模块的具体实现,而是应该依赖于抽象的接口或抽象类。这样做的好处是:
- 高层模块可以独立于低层模块的具体实现进行开发和测试,提高了代码的可测试性。
- 当需要切换低层模块的实现时,只需要修改低层模块的代码,而不需要修改高层模块的代码,提高了代码的可维护性。
- 高层模块和低层模块之间的耦合度降低,代码的模块化程度更高,提高了代码的可扩展性。
【抽象】和【细节】的含义
-
抽象(Abstraction)
- 抽象指的是更高层次、更通用的概念、接口或抽象类。
- 在代码设计中,抽象通常体现为接口(Interface)或抽象类(Abstract Class)。
- 抽象定义了某个领域或系统的基本功能和行为,但不关心具体的实现细节。
-
细节(Details)
- 细节指的是更低层次、更具体的实现。
- 在代码设计中,细节通常体现为具体的实现类(Concrete Class)。
- 细节负责实现抽象定义的功能和行为,关注具体的实现细节。
依赖倒置原则中的这句话的含义是:
-
抽象不应该依赖细节
- 抽象层(接口或抽象类)应该定义基本的功能和行为,不应该依赖于具体的实现细节。
- 这样可以确保抽象层的稳定性,并且提高代码的灵活性和可扩展性。
-
细节应该依赖抽象
- 具体的实现类应该依赖于抽象层(接口或抽象类)定义的契约。
- 这样可以确保实现细节与抽象层保持一致,并且提高代码的可测试性和可维护性。
举个例子,在上面的EmailSender
示例中:
- 抽象层是
EmailSender
接口,它定义了发送电子邮件的基本功能。 - 细节层是
GmailEmailSender
和OutlookEmailSender
这两个具体实现类,它们依赖于EmailSender
接口的定义来实现具体的邮件发送功能。
这样设计可以确保:
EmailService
类(高层模块)只依赖于EmailSender
接口(抽象),不依赖于具体的实现细节。GmailEmailSender
和OutlookEmailSender
这些具体实现类(细节)依赖于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();
}
}
遵循迪米特法则的优点:
- 降低了类之间的耦合度,提高了类的独立性和可重用性。
- 使得类的职责更加清晰,代码更加模块化和易于维护。
- 提高了代码的可读性和可测试性。
不遵循迪米特法则的缺点:
- 增加了类之间的耦合度,降低了系统的灵活性和可扩展性。
- 代码更难理解和维护,因为每个类都需要了解其他类的内部实现细节。
- 降低了代码的可测试性,因为测试一个类时需要考虑其他类的行为。
迪米特法则的规则
- 一个对象应该只与它的直接朋友通信。
- 一个对象的“直接朋友”包括:
- 它自己(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());
遵循组合/聚合复用原则的优点:
- 动态切换引擎:运行时Car可以随时调用setEngine(Engine engine)更换引擎。
- 扩展方便:改变引擎只需修改Engine接口的实现类,无需修改Car类。
- 低耦合:Car不依赖具体的引擎实现类,只依赖接口。
不遵循组合/聚合复用原则的缺点:
- 每换一次引擎就要创建新子类或者改car类的代码,引擎和车强耦合,难以动态切换引擎。 限制了类的可复用性和扩展性,增加了代码的复杂性和维护成本。
组合和聚合的区别和联系
- 组合(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类的实例,它们之间存在强依赖关系。
- 聚合(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类的实例,它们之间存在弱依赖关系。
- 联系
- 组合是聚合的一种特殊形式。
- 组合关系要求部分对象不能独立于整体对象而存在,而聚合关系中部分对象可以独立于整体对象而存在。
- 组合关系中,整体对象对部分对象有所有权和生命周期控制,而聚合关系中,整体对象只有所有权,不对部分对象的生命周期负责
为什么要优先使用组合/聚合而不是继承?
- 继承会创建紧耦合的类层次结构,增加了代码的复杂性和维护成本。
- 组合/聚合可以提高代码的可复用性,更灵活地组合和扩展功能。
- 组合/聚合可以降低类之间的耦合度,提高代码的可测试性和可维护性。
总结
面向对象7大设计原则总结
- 单一职责原则(SRP): 类应该有且仅有一个引起它变化的原因。
- 开闭原则(OCP): 软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。
- 里氏替换原则(LSP): 子类对象必须能够替换其基类对象。
- 接口隔离原则(ISP): 接口应该小而精,接口中的方法应该尽量少,客户端不应该依赖它不需要的接口。
- 依赖倒置原则(DIP): 针对接口编程,而不是针对实现编程;依赖抽象,而不依赖于具体类。
- 迪米特法则(LOD): 一个对象应该只与其直接朋友通信。
- 组合/聚合复用原则(CARP): 优先使用组合/聚合,少用继承。
单一职责原则(SRP) 和 接口隔离原则(ISP) 的区别和联系
-
SRP 针对的是类,ISP 针对的是接口:
- SRP 要求一个类只负责一个职责或功能。
- ISP 要求接口中的方法尽量少,仅包含相关的方法。
-
侧重点不同:
- SRP 强调类的内聚性,避免一个类承担太多职责。
- ISP 强调接口的小而美,避免客户端依赖它不需要的方法。
-
目的不太一样:
- SRP 是为了让类更容易理解、维护和测试。
- ISP 是为了让客户端只依赖它需要的部分,降低耦合度。
-
举例说明:
假设我们有一个描述汽车的类 Vehicle,它提供以下功能:- 启动和停止汽车引擎
- 显示汽车里程表和油表
按照 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 强调接口的小而美。两者都是为了提高代码的可维护性和扩展性。在实际开发中,我们需要兼顾这两个原则,设计出高内聚低耦合
的面向对象系统。