SOLID 是面向对象设计中的五个基础原则的缩写,分别是:
除了 SOLID 原则之外,还有一些其他常见的面向对象设计原则:
定义:一个类应该仅有一个引起它变化的原因,即一个类只应该有一个职责。
假设我们有一个类负责用户数据的管理以及日志记录,这样的类可能会变得非常复杂和难以维护。如果用户数据管理和日志记录发生变化,可能需要修改同一个类,这违背了单一职责原则。
// 违反单一职责原则
public class UserManager {
public void createUser(String username) {
// 创建用户逻辑
System.out.println("User " + username + " created.");
log("Created user: " + username);
}
private void log(String message) {
System.out.println("Log: " + message);
}
}
为了遵守单一职责原则,我们应该将日志记录和用户管理分离为两个独立的类:
// 遵守单一职责原则
public class UserManager {
private Logger logger;
public UserManager(Logger logger) {
this.logger = logger;
}
public void createUser(String username) {
// 创建用户逻辑
System.out.println("User " + username + " created.");
logger.log("Created user: " + username);
}
}
public class Logger {
public void log(String message) {
System.out.println("Log: " + message);
}
}
现在 UserManager
只负责管理用户,而 Logger
负责日志记录,符合单一职责原则。
定义:软件实体(类、模块、函数等)应该对扩展开放,对修改封闭。
这意味着在不修改现有代码的情况下,可以通过扩展来实现新功能。
假设我们有一个用于计算不同几何形状面积的类,如果我们需要添加新形状,那么就需要修改这个类,这违背了开放封闭原则。
// 违反开放封闭原则
public class AreaCalculator {
public double calculateArea(Object shape) {
if (shape instanceof Circle) {
Circle circle = (Circle) shape;
return Math.PI * circle.radius * circle.radius;
} else if (shape instanceof Rectangle) {
Rectangle rectangle = (Rectangle) shape;
return rectangle.width * rectangle.height;
}
return 0;
}
}
为了遵循开放封闭原则,我们可以使用抽象类或接口,这样我们在添加新形状时不需要修改现有的代码:
// 遵守开放封闭原则
public interface Shape {
double calculateArea();
}
public class Circle implements Shape {
double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public double calculateArea() {
return Math.PI * radius * radius;
}
}
public class Rectangle implements Shape {
double width, height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
public double calculateArea() {
return width * height;
}
}
public class AreaCalculator {
public double calculateArea(Shape shape) {
return shape.calculateArea();
}
}
通过这种方式,如果我们需要添加新形状,只需要添加一个实现 Shape
接口的新类,而无需修改 AreaCalculator
类。
定义:子类对象必须能够替换掉父类对象,并且程序逻辑不应因此而改变。
里氏替换原则强调继承时保持行为一致。如果子类不能替换父类,或者在使用子类时需要额外判断或处理,说明违反了该原则。
假设我们有一个矩形类和一个继承自矩形的正方形类,违反里氏替换原则的设计如下:
// 违反里氏替换原则
public class Rectangle {
protected double width, height;
public void setWidth(double width) {
this.width = width;
}
public void setHeight(double height) {
this.height = height;
}
public double getArea() {
return width * height;
}
}
public class Square extends Rectangle {
@Override
public void setWidth(double width) {
this.width = width;
this.height = width; // 保证正方形的边长一致
}
@Override
public void setHeight(double height) {
this.width = height;
this.height = height; // 保证正方形的边长一致
}
}
Square
违反了里氏替换原则,因为正方形的行为不同于矩形。为了解决这个问题,可以将矩形和正方形分为独立的类,而不是使用继承:
// 遵守里氏替换原则
public interface Shape {
double getArea();
}
public class Rectangle implements Shape {
protected double width, height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
public double getArea() {
return width * height;
}
}
public class Square implements Shape {
private double side;
public Square(double side) {
this.side = side;
}
@Override
public double getArea() {
return side * side;
}
}
现在,Square
和 Rectangle
不再共享不兼容的行为,符合里氏替换原则。
定义:客户端不应该被迫依赖它不使用的方法。换句话说,接口应该为特定的客户端设计,而不是通用的“大而全”接口。
如果一个接口包含过多的方法,而实现这个接口的类只需要部分方法,那么就违反了接口隔离原则:
// 违反接口隔离原则
public interface Worker {
void work();
void eat();
}
public class HumanWorker implements Worker {
@Override
public void work() {
System.out.println("Human working");
}
@Override
public void eat() {
System.out.println("Human eating");
}
}
public class RobotWorker implements Worker {
@Override
public void work() {
System.out.println("Robot working");
}
// Robot 不需要 eat 方法
@Override
public void eat() {
throw new UnsupportedOperationException();
}
}
RobotWorker
不需要 eat()
方法,但仍然必须实现它。这违反了接口隔离原则。为了解决这个问题,可以将接口分解为更小的、专注于特定职责的接口:
// 遵守接口隔离原则
public interface Workable {
void work();
}
public interface Eatable {
void eat();
}
public class HumanWorker implements Workable, Eatable {
@Override
public void work() {
System.out.println("Human working");
}
@Override
public void eat() {
System.out.println("Human eating");
}
}
public class RobotWorker implements Workable {
@Override
public void work() {
System.out.println("Robot working");
}
}
现在 RobotWorker
只需要实现 Workable
接口,而不需要 Eatable
接口,符合接口隔离原则。
定义:高层模块不应该依赖于低层模块,二者都应该依赖于抽象;抽象不应该依赖于细节,细节应该依赖于抽象。
依赖倒置原则的核心思想是通过依赖抽象(接口或抽象类)来降低模块之间的耦合度。
假设我们有一个类直接依赖于具体的数据库实现,这违反了依赖倒置原则:
// 违反依赖倒置原则
public class UserRepository {
private MySQLDatabase database;
public UserRepository() {
this.database = new MySQLDatabase();
}
public void save(User user) {
database.save(user);
}
}
UserRepository
直接依赖于 MySQLDatabase
,如果我们要切换到其他数据库,就需要修改 UserRepository
。为了遵守依赖倒置原则,可以引入抽象:
// 遵守依赖倒置原则
public interface Database {
void save(User user);
}
public class MySQLDatabase implements Database {
@Override
public void save(User user) {
System.out.println("Saving user to MySQL database");
}
}
public class MongoDBDatabase implements Database {
@Override
public void save(User user) {
System.out.println("Saving user to MongoDB database");
}
}
public class UserRepository {
private Database database;
public UserRepository(Database database) {
this.database = database;
}
public void save(User user) {
database.save(user);
}
}
通过依赖 Database
接口,UserRepository
不再依赖于具体的数据库实现,这使得我们能够轻松切换不同的数据库实现。
定义:合成复用原则主张应当尽量通过组合(或聚合)来复用代码,而不是通过继承。简单来说,它建议我们优先使用“有一个”(has-a)的关系,而不是“是一个”(is-a)的关系。
违背合成复用原则:
假设我们有两个类:Book
和 EBook
,它们之间使用继承关系。
// 违反合成复用原则
public class Book {
private String title;
private String author;
public void printBookInfo() {
System.out.println("Title: " + title + ", Author: " + author);
}
}
public class EBook extends Book {
private String downloadUrl;
public void download() {
System.out.println("Downloading from " + downloadUrl);
}
}
在这种设计中,EBook
继承了 Book
,但实际上,EBook
并不是 Book
的一种,而是一个扩展版本,违背了合成复用原则。
遵循合成复用原则:
可以将下载功能分离为单独的类,并通过组合实现代码复用:
// 遵守合成复用原则
public class Book {
private String title;
private String author;
public void printBookInfo() {
System.out.println("Title: " + title + ", Author: " + author);
}
}
public class Downloadable {
private String downloadUrl;
public void download() {
System.out.println("Downloading from " + downloadUrl);
}
}
public class EBook {
private Book book;
private Downloadable downloadable;
public EBook(Book book, Downloadable downloadable) {
this.book = book;
this.downloadable = downloadable;
}
public void printBookInfo() {
book.printBookInfo();
}
public void download() {
downloadable.download();
}
}
在这个设计中,EBook
通过组合 Book
和 Downloadable
类实现功能扩展,而不是通过继承来复用代码。这样既避免了强耦合,又提高了代码的复用性和灵活性。
定义:最少知识原则也称为“迪米特法则”,其核心思想是一个对象应当对其他对象有尽可能少的了解。通俗地讲,类与类之间的通信应当尽量简单,避免过多的依赖或嵌套调用。
具体来说,最少知识原则规定对象只能调用:
违反最少知识原则的设计:
public class Engine {
public void start() {
System.out.println("Engine started");
}
}
public class Car {
private Engine engine;
public Car() {
engine = new Engine();
}
public Engine getEngine() {
return engine;
}
}
public class Driver {
public void drive(Car car) {
car.getEngine().start(); // 违反了最少知识原则,Driver不应直接依赖Engine
}
}
在这个例子中,Driver
类不仅依赖于 Car
类,还依赖于 Engine
类,这违反了最少知识原则。
遵循最少知识原则的设计:
public class Engine {
public void start() {
System.out.println("Engine started");
}
}
public class Car {
private Engine engine;
public Car() {
engine = new Engine();
}
public void start() {
engine.start(); // 通过封装,将Engine的操作隐藏起来
}
}
public class Driver {
public void drive(Car car) {
car.start(); // 遵守最少知识原则,Driver只和Car交互
}
}
在这个设计中,Driver
只与 Car
交互,而不关心 Engine
的内部实现。通过封装细节,遵循了最少知识原则,减少了类之间的耦合。
定义:
违反高内聚,低耦合的设计:
假设我们有一个 OrderProcessor
类,负责订单的处理、支付、库存更新等多种功能。
// 违反高内聚,低耦合原则
public class OrderProcessor {
public void processOrder(Order order) {
validateOrder(order);
processPayment(order);
updateInventory(order);
generateInvoice(order);
}
private void validateOrder(Order order) {
// 验证订单
}
private void processPayment(Order order) {
// 处理付款
}
private void updateInventory(Order order) {
// 更新库存
}
private void generateInvoice(Order order) {
// 生成发票
}
}
在这个例子中,OrderProcessor
的职责过多,它负责订单的各个方面,包括验证、付款、库存更新和发票生成。这样做降低了内聚性,并且使得类的修改频率增加,增加了耦合性。
遵循高内聚,低耦合的设计:
我们可以将每个职责分离到独立的类中,以提高内聚性并降低耦合性。
// 遵循高内聚,低耦合原则
public class OrderValidator {
public void validate(Order order) {
// 验证订单
}
}
public class PaymentProcessor {
public void processPayment(Order order) {
// 处理付款
}
}
public class InventoryUpdater {
public void updateInventory(Order order) {
// 更新库存
}
}
public class InvoiceGenerator {
public void generateInvoice(Order order) {
// 生成发票
}
}
public class OrderProcessor {
private OrderValidator validator;
private PaymentProcessor paymentProcessor;
private InventoryUpdater inventoryUpdater;
private InvoiceGenerator invoiceGenerator;
public OrderProcessor(OrderValidator validator, PaymentProcessor paymentProcessor,
InventoryUpdater inventoryUpdater, InvoiceGenerator invoiceGenerator) {
this.validator = validator;
this.paymentProcessor = paymentProcessor;
this.inventoryUpdater = inventoryUpdater;
this.invoiceGenerator = invoiceGenerator;
}
public void processOrder(Order order) {
validator.validate(order);
paymentProcessor.processPayment(order);
inventoryUpdater.updateInventory(order);
invoiceGenerator.generateInvoice(order);
}
}
现在,每个类只负责一个特定的职责,从而提高了每个类的内聚性。而 OrderProcessor
类通过组合这些独立的类来实现订单处理,降低了耦合性。
合成复用原则 建议我们优先使用组合而非继承来实现代码复用,这样可以避免不必要的耦合,并提高系统的灵活性。
最少知识原则 强调尽量减少类与类之间的依赖,避免“链式调用”,从而降低系统的复杂性和维护成本。
高内聚低耦合 则是指导我们如何设计类和模块,使它们各自职责明确,彼此独立,从而提高代码的可维护性和可扩展性。
通过遵循这些原则,开发者可以设计出更加健壮、灵活、易于维护的系统。在实际开发中,时刻牢记这些原则,并根据实际场景灵活应用,会使代码质量得到显著提升。