单一职责原则(Single-Responsibility Principle)
内容:一个类最好只做一件事
提高可维护性
:当一个类只负责一个功能时,其实现通常更简单、更直接,这使得理解和维护变得更容易。
减少代码修改的影响:更改影响较小的部分,因此减少了对系统其他部分的潜在破坏。
开放封闭原则(Open-Closed principle)
内容:对扩展开放、对修改封闭
促进可扩展性
:可以在不修改现有代码的情况下扩展功能,这意味着新的功能可以添加,而不会影响旧的功能。
降低风险
:由于不需要修改现有代码,因此引入新错误的风险较低。
Liskov替换原则(Liskov-Substituion Principle)
内容:子类必须能够替换其基类
提高代码的可互换性
:能够用派生类的实例替换基类的实例,使得代码更加模块化,提高了其灵活性。
增加代码的可重用性
:遵循LSP的类和组件更容易被重用于不同的上下文。
依赖倒置原则(Dependency-Inversion Principle)
内容:程序要依赖于抽象接口,而不是具体的实现
提高代码的可测试性
:通过依赖于抽象而不是具体实现,可以轻松地对代码进行单元测试。
减少系统耦合
:系统的高层模块不依赖于低层模块的具体实现,从而使得系统更加灵活和可维护。
接口隔离原则(Interface-Segregation Principle)。
内容:使用多个小的专门的接口,而不要使用一个大的总接口
减少系统耦合
:通过使用专门的接口而不是一个大而全的接口,系统中的不同部分之间的依赖性减少了。
提升灵活性和稳定性
:更改一个小接口比更改一个大接口风险更低,更容易管理。
示例
以下是一些示例,通过代码的方式给大家介绍一下这几个原则具体的应用和实践。
单一职责原则:一个类最好只做一件事
假如有一个类用于日志消息的处理,但是这个类不仅仅负责创建日志消息,还负责将其写入文件。根据单一职责原则,我们应该将这两个职责分开,让一个类专注于创建日志消息,而另一个类专注于日志消息的存储。
// 负责日志消息的创建
class LogMessageCreator {
public String createLogMessage(String message, LogLevel level) {
// 创建和格式化日志消息
LocalDateTime now = LocalDateTime.now();
return now.toString() + " [" + level.toString() + "] " + message;
}
}
// 日志级别枚举
enum LogLevel {
INFO, WARNING, ERROR;
}
// 负责日志消息的存储
class LogFileWriter {
public void writeToFile(String message, String filename) {
// 将日志消息写入指定的文件
try {
Files.write(Paths.get(filename), message.getBytes(), StandardOpenOption.APPEND);
} catch (IOException e) {
// 处理文件写入异常
}
}
}
// 使用例子
public class Logger {
private LogMessageCreator messageCreator;
private LogFileWriter fileWriter;
public Logger() {
messageCreator = new LogMessageCreator();
fileWriter = new LogFileWriter();
}
public void log(String message, LogLevel level, String filename) {
String logMessage = messageCreator.createLogMessage(message, level);
fileWriter.writeToFile(logMessage, filename);
}
}
LogMessageCreator类只负责创建和格式化日志消息,而LogFileWriter类只负责将日志消息写入文件。这种分离确保了每个类只有一个改变的原因,遵循了单一职责原则。
开放封闭原则:对扩展开放、对修改封闭
假设有一个图形绘制应用程序,其中有一个Shape类。
在遵守开闭原则的情况下,如果要添加新的形状类型,应该能够扩展Shape类而无需修改现有代码。这可以通过创建继承自Shape的新类来实现,如Circle和Rectangle。
// 形状接口
interface Shape {
void draw();
}
// 圆形类
class Circle implements Shape {
public void draw() {
// 绘制圆形
}
}
// 矩形类
class Rectangle implements Shape {
public void draw() {
// 绘制矩形
}
}
// 图形绘制类
class GraphicEditor {
public void drawShape(Shape shape) {
shape.draw();
}
}
这样,当我们想要修改Circle的时候不会对Rectangle有任何影响。
里氏替换原则:子类必须能够替换其基类
假设有一个函数接受Bird对象作为参数。根据里氏替换原则,这个函数应该能够接受一个Bird的子类对象(如Sparrow或Penguin)而不影响程序运行。
// 鸟类
class Bird {
public void fly() {
// 实现飞行
}
}
// 麻雀类
class Sparrow extends Bird {
// 重写飞行行为
}
// 企鹅类
class Penguin extends Bird {
@Override
public void fly() {
throw new UnsupportedOperationException("Penguin can't fly");
}
}
public static void main(String[] args){
Penguin penguin = new Penguin();
makeItFly(penguin);
Sparrow sparrow = new Sparrow();
makeItFly(sparrow);
}
// 使用鸟类的函数
public static void makeItFly(Bird bird) {
bird.fly();
}
我们可以把任意一个Bird的实现传入到makeItFly方法中,实现了用子类替换父类
依赖倒置原则:程序要依赖于抽象接口,而不是具体的实现
在构建一个电商应用程序时,一个高层的“订单处理”模块不应该直接依赖于一个低层的“数据访问”模块。相反,它们应该依赖于抽象,例如一个接口。这样,数据访问的具体实现可以随时改变,而不会影响订单处理模块。
// 数据访问接口
interface DataAccess {
void saveOrder(Order order);
}
// 高层模块:订单处理
class OrderProcessingService {
private DataAccess dataAccess;
public OrderProcessingService(DataAccess dataAccess) {
this.dataAccess = dataAccess;
}
public void processOrder(Order order) {
// 订单处理逻辑
dataAccess.saveOrder(order);
}
}
// 低层模块:数据访问实现
class DatabaseDataAccess implements DataAccess {
public void saveOrder(Order order) {
// 数据库保存逻辑
}
}
这样底层的数据存储我们就可以任意更换,可以用MySQL,可以用Redis,可以用达梦,也可以用OceanBase,因为我们做到了依赖接口,而不是具体实现。
接口隔离原则:使用多个小的专门的接口,而不要使用一个大的总接口
如果有一个多功能打印机接口包含打印、扫描和复制功能,那么只需要打印功能的客户端应该不必实现扫描和复制的接口。这可以通过将大接口分解为更小且更具体的接口来实现。
// 打印接口
interface Printer {
void print();
}
// 扫描接口
interface Scanner {
void scan();
}
// 多功能打印机类
class MultiFunctionPrinter implements Printer, Scanner {
public void print() {
// 打印实现
}
public void scan() {
// 扫描实现
}
}
// 仅打印类
class SimplePrinter implements Printer {
public void print() {
// 打印实现
}
}